Node deletion is BST, python implementation - python

I'm curious about this implementation of a node deletion in BST (for additional code/context, see full implementation.) Here's how I understand it:
if val < self.data and elif val > self.data are both cases where
the current node isn't the node to be deleted, so they recursively
call the delete function on the appropriate child node.
else is the case where we have found the right node and need to perform the deletion.
a. if self.left is None and self.right is None: return None I'm unclear on what the goal is here. We've returned None but haven't reassigned the node value, itself, to None.
b. At this point, we've ruled out the possibility that both left and right don't exist and so elif self.left is None is a verbose way to write elif self.right, which returns right. But why? It doesn't reassign the value of the node, itself.
c. I'm unsure why this last control flow statement is elif self.right is None. Why the absence of an else statement?
This min_val, self.data, self.right dance occurs only when one of the above control flow statements from #2 is not conditionally executed, so I suppose that this is an implicit else statement.
a. This step really boils down to assigning self.data the minimum value down its right child then assigning self.right the output of a recursive function call, which might be left, right or None from #2.
def delete(self, val):
if val < self.data:
if self.left:
self.left = self.left.delete(val)
elif val > self.data:
if self.right:
self.right = self.right.delete(val)
else:
if self.left is None and self.right is None:
return None
elif self.left is None:
return self.right
elif self.right is None:
return self.left
min_val = self.right.find_min()
self.data = min_val
self.right = self.right.delete(min_val)
return self
To answer this question, please confirm or correct some of my doubts above.

The first cases of the algorithm are about deleting nodes from the tree: removing all references to them while maintaining the BST's key order.
Think about how you'd do this with pencil and paper.
If the deleted node has no children, redraw the parent's pointer to point to nothing (None). The deleted node is now "cut out" of the tree. BST order is maintained.
If the deleted node has exactly one child, replace the parent's pointer so it now points to that child. Again the deleted node is cut out of the tree, the BST key order is maintained.
Note the replacement of the parent pointers is happening at the recursive calls to delete. E.g. by returning None, the "no child" case is causing the parent to point to nothing. The "cut out" nodes will ultimately be garbage collected by Python.
Otherwise you have the more complex case: only one parent of the deleted node and two children. What to do? This particular code finds the next largest key wrt the deleted node and uses it to replace the key to be deleted. The node isn't cut out at all. Then it deletes that moved value from the right subtree, which does cause its node to be cut out. Again BST key order is maintained.
This way of implementing the third case makes clean code, and it probably makes sense in Python where all data access is via reference (pointer). But it's not the only choice. The alternative is to continue the same pattern as the other cases and actually cut out the node containing the deleted value and move the next largest node to that position (not just it's key data). This is a better choice if the node actually contains the data (not just a reference to it), and it's very large. It saves a copy of all that data. If the author had made that choice, there would be another return of that node for re-assignment of the parent's pointer.

Related

Printing linked-list in python

In my task first I need to make single linked-list from array.
My code:
class Node:
def __init__(self,data):
self.data = data
self.next = next
class Lista:
def __init__(self, lista=None)
self.head = None
def ispis(self):
printval = self.head
while printval .next is not None:
print(printval.next.data)
printval = printval.next
if __name__ == '__main__'
L = Lista ([2, "python", 3, "bill", 4, "java"])
ispis(L)
With function ispis I need to print elements of linked-list. But it says name "ispis" is not defined. Cannot change ispis(L) !
EDIT: removed next from and ispis(self) is moved outside Lista class
while printvla.next is not None:
EDIT2:
It shows that L is empty so thats why it won't print anything. Should I add elements to class Node ?
ispis is a method in a class. But you are calling the function as if it is a normal function outside the class.
Atleast you have created the object correctly. Below would be the correct way of calling the method inside the class.
L.ispis()
This question sounds suspiciously like a homework assignment. If the instructor is trying to teach you how to create linked lists, you need to go back to what you need to do:
A node when set up for the first time only needs the data. Typically the next pointer/value would be set to None (meaning no next member).
Your __init__ method for your Lista class needs to do something with its argument.
I believe if you need to use your ispls function to operate on a class, then the function probably isn't supposed to be a member of Lista.
I think your ispls loop shouldn't be testing its .next member. This would fail if you had a None to begin with. You should be testing the current instance rather than its next. That way, when you move on to the next node, if it's None, it gets out of the loop.
Be careful with the keyword next. I would avoid using it as a class attribute. Also the literal next would just give you the built-in command.
At the bare minimum, you would want to iterate over the lista argument in __init__, creating a Node for each one, saving the previous node for the next operation.
if lista is None:
self.head = None
return
prev = None
for data in lista:
node = Node(data)
if prev is None:
self.head = node
else:
prev.next = node
prev = node
But again, I believe that is what the instructor wanted you to figure out. Hope this helped.
--B

Do the dots in linked lists represent nodes or pointers from one node to another?

I have the following code that reverses a linked list (I know it is correct). I am relatively new to linked list, and I had a few questions about the code itself:
def reverseList(self, head: ListNode) -> ListNode:
if head == None:
return None
if head.next == None:
return head
prev = head
curr = head
nex = head.next
while nex:
curr = nex
nex = nex.next
curr.next = prev
prev = curr
head.next = None
return curr
I understand the code relating to the two 'if' statements.
Questions:
The 'prev' and 'curr' - am I right in saying that those are pointers?
In 'nex = head.next' is 'head.next' referring to the node after head, or the pointer that points from head to the next node?
(Also, what is a 'pointer'- is this what connects one node to another in a linked list?- I understand linked lists, it is just the references in the code I am trying to figure out)
In the while statement:
we do 'curr = nex' to move the curr pointer from head to nex (=head.next) - am I reading this correctly?
'nex = nex.next' what does this do, are we simply assigning nex to nex.next , or are we establishing a connection/address from the node after the head to the node after this node?
The code within the while loop I am struggling to understand, if someone can explain maybe one or two lines that would be really helpful, as I assume most hold the same meaning. Many thanks
Python doesn't really have the concept of "pointers" as distinct from "values" the same way that a language like C does. It might be accurate to say that every variable in Python is a "pointer", since variables in Python are always references to objects; reassigning a variable does not change the underlying object (rather, it reassigns it to point at some other object).
The 'prev' and 'curr' - am I right in saying that those are pointers?
Per the above disclaimer -- yes.
In 'nex = head.next' is 'head.next' referring to the node after head, or the pointer that points from head to the next node?
Both. To say that head.next "refers to" the node after head is equivalent to saying that it "points to" the next node. It's the same concept.
(Also, what is a 'pointer'- is this what connects one node to another in a linked list?- I understand linked lists, it is just the references in the code I am trying to figure out)
A pointer is a thing that "points", which is the same thing as "referring" or "connecting".
we do 'curr = nex' to move the curr pointer from head to nex (=head.next) - am I reading this correctly?
Yes; when you reassign a variable using the assignment operate (=), that variable now points to the thing you just assigned it to.
'nex = nex.next' what does this do, are we assigning nex to nex.next , or are we establishing a connection/address from the node after the head to the node after this node?
The variable nex is being reassigned to point at whatever nex.next points to. In the context of a linked list, it means you're moving the nex pointer one spot forward in the list.

Purpose of base case in binary search tree deletion

I am taking a course on algorithms and data structures in Python 3 and my instructor recently introduced us to the binary search tree. However, I am having trouble understanding the deletion algorithm. Below is the implementation we were taught, however when I initially wrote my own rendition, I did not include a "base case" and it still worked:
def remove(self, data):
if self.root:
self.root = self.remove_node(data, self.root)
def remove_node(self, data, node):
if node is None:
return node
if data < node.data:
node.leftChild = self.remove_node(data, node.leftChild)
elif data > node.data:
node.rightChild = self.remove_node(data, node.rightChild)
else:
if not node.rightChild and not node.leftChild:
print('removing leaf node')
del node
return None
if not node.leftChild:
print('removing node with single right child')
tempNode = node.rightChild
del node
return tempNode
elif not node.rightChild:
print('removing node with single left child')
tempNode = node.leftChild
del node
return tempNode
print('removing node with two children')
tempNode = self.get_predecessor(node.leftChild)
node.data = tempNode.data
node.leftChild = self.remove_node(tempNode.data, node.leftChild)
return node
Now, all of this makes sense to me except the statement below:
if node is None:
return node
When we previously learned about base cases, we were taught that they were essentially the exit points for our algorithms. However, I do not understand how this is the case in the given code. For one, I do not see how a node could ever be empty and even if it was, why would we return an empty node? As far as I can see, this check serves no purpose in the overall recursion because we do not seem to "recur towards it" as we would in any other recursive function. I would greatly appreciate an explanation!
Base case(s), in general, serve one or more purposes, these include;
preventing the function from recursing infinitely
preventing the function from throwing errors on corner cases
compute/return a value/result to callers higher up in the recursion tree
With tree deletion, the first point isn't really a concern (because the recursion tree will only have a finite number of nodes - same as the tree you recurse over). You will be concerned with points 2 and 3 here.
In your function, you do have a base case - in fact, you have two (thanks to #user2357112) -
The value-not-found portion, specified by
if node is None:
return node
and,
The value-found portion, specified by your code inside the else statement, which performs the actual deletion.
To keep the behaviour consistent with the recursive cases, the value-not-found base case returns None. As you see, the first base case is consistent performs the second function of a generic base case outlined above, while the second base case performs the third.

How to write pop(item) method for unsorted list

I'm implementing some basic data structures in preparation for an exam and have come across the following issue. I want to implement an unsorted linked list, and have already implemented a pop() method, however I don't know, either syntactically or conceptually, how to make a function sometimes take an argument, sometimes not take an argument. I hope that makes sense.
def pop(self):
current = self.head
found = False
endOfList = None
while current != None and not found:
if current.getNext() == None:
found = True
endOfList = current.getData()
self.remove(endOfList)
self.count = self.count - 1
else:
current = current.getNext()
return endOfList
I want to know how to make the statement unsortedList.pop(3) valid, 3 being just an example and unsortedList being a new instance of the class.
The basic syntax (and a common use case) for using a parameter with a default value looks like this:
def pop(self, index=None):
if index is not None:
#Do whatever your default behaviour should be
You then just have to identify how you want your behaviour to change based on the argument. I am just guessing that the argument should specify the index of the element that should be pop'ed from the list.
If that is the case you can directly use a valid default value instead of None e.g. 0
def pop(self, index=0):
First, add a parameter with a default value to the function:
def pop(self, item=None):
Now, in the code, if item is None:, you can do the "no param" thing; otherwise, use item. Whether you want to switch at the top, or lower down in the logic, depends on your logic. In this case, item is None probably means "match the first item", so you probably want a single loop that checks item is None or current.data == item:.
Sometimes you'll want to do this for a parameter that can legitimately be None, in which case you need to pick a different sentinel. There are a few questions around here (and blog posts elsewhere) on the pros and cons of different choices. But here's one way:
class LinkedList(object):
_sentinel = object()
def pop(self, item=_sentinel):
Unless it's valid for someone to use the private _sentinel class member of LinkedList as a list item, this works. (If that is valid—e.g., because you're building a debugger out of these things—you have to get even trickier.)
The terminology on this is a bit tricky. Quoting the docs:
When one or more top-level parameters have the form parameter = expression, the function is said to have “default parameter values.”
To understand this: "Parameters" (or "formal parameters") are the things the function is defined to take; "arguments" are things passed to the function in a call expression; "parameter values" (or "actual parameters", but this just makes things more confusing) are the values the function body receives. So, it's technically incorrect to refer to either "default parameters" or "parameters with default arguments", but both are quite common, because even experts find this stuff confusing. (If you're curious, or just not confused yet, see function definitions and calls in the reference documentation for full details.)
Is your exam using Python specifically? If not, you may want to look into function overloading. Python doesn't support this feature, but many other languages do, and is a very common approach to solving this kind of problem.
In Python, you can get a lot of mileage out of using parameters with default values (as Michael Mauderer's example points out).
def pop(self, index=None):
prev = None
current = self.head
if current is None:
raise IndexError("can't pop from empty list")
if index is None:
index = 0 # the first item by default (counting from head)
if index < 0:
index += self.count
if not (0 <= index < self.count):
raise IndexError("index out of range")
i = 0
while i != index:
i += 1
prev = current
current = current.getNext()
assert current is not None # never happens if list is self-consistent
assert i == index
value = current.getData()
self.remove(current, prev)
##self.count -= 1 # this should be in self.remove()
return value

How to delete all nodes of a Binary Search Tree

I am trying to write a code to delete all nodes of a BST (each node has only three attributes, left, right and data, there are no parent pointers). The following code is what I have come up with, it deletes only the right half of the tree, keeping the left half intact. How do I modify it so that the left half is deleted as well (so that ultimately I am left with only the root node which has neither left or right subtrees)?
def delete(root):
global last
if root:
delete(root.left)
delete(root.right)
if not (root.left or root.right):
last = root
elif root.left == last:
root.left = None
else:
root.right = None
And secondly, can anybody suggest an iterative approach as well, using stack or other related data structure?
Blckknght is right about garbage collection, but in case you want to do some more complex cleanup than your example suggests or understand why your code didn't work, i'll provide an additional answer:
Your problem seems to be the elif node.left == last check.
I'm not sure what your last variable is used for or what the logic is behind it.
But the problem is that node.left is almost never equal to last (you only assign a node to the last variable if both children are already set to None, which they aren't for any of the interesting nodes (those that have children)).
If you look at your code, you'll see that in that if node.left isn't equal to last only the right child gets set to None, and thus only the right part of the subtree is deleted.
I don't know python, but this should work:
def delete(node):
if node:
# recurse: visit all nodes in the two subtrees
delete(node.left)
delete(node.right)
# after both subtrees have been visited, set pointers of this node to None
node.left = None
node.right = None
(I took the liberty of renaming your root parameter to node, since the node given to the function doesn't have to be the root-node of the tree.)
If you want to delete both subtrees, there's no need to recurse. Just set root.left and root.right to None and let the garbage collector take care of them. Indeed, rather than making a delete function in the first place, you could just set root = None and be done with it!
Edit: If you need to run cleanup code on the data values, you might want to recurse through the tree to get to all of them if the GC doesn't do enough. Tearing down the links in the tree shouldn't really be necessary, but I'll do that too for good measure:
def delete(node):
if node:
node.data.cleanup() # run data value cleanup code
delete(node.left) # recurse
delete(node.right)
node.data = None # clear pointers (not really necessary)
node.left = None
none.right = None
You had also asked about an iterative approach to traversing the tree, which is a little more complicated. Here's a way to an traversal using a deque (as a stack) to keep track of the ancestors:
from collections import deque
def delete_iterative(node):
stack = deque()
last = None
# start up by pushing nodes to the stack until reaching leftmost node
while node:
stack.append(node)
node = node.left
# the main loop
while stack:
node = stack.pop()
# should we expand the right subtree?
if node.right && node.right != last: # yes
stack.append(node)
node = node.right
while node: # expand to find leftmost node in right subtree
stack.append(node)
node = node.left
else: # no, we just came from there (or it doesn't exist)
# delete node's contents
node.data.cleanup()
node.data = None # clear pointers (not really necessary)
node.left = None
node.right = None
# let our parent know that it was us it just visited
last = node
An iterative post-order traversal using a stack could look like this:
def is_first_visit(cur, prev):
return prev is None or prev.left is cur or prev.right is cur
def visit_tree(root):
if root:
todo = [root]
previous = None
while len(todo):
node = todo[-1]
if is_first_visit(node, previous):
# add one of our children to the stack
if node.left:
todo.append(node.left)
elif node.right:
todo.append(node.right)
# now set previous to ourself and continue
elif previous is node.left:
# we've done the left subtree, do right subtree if any
if node.right:
todo.append(node.right)
else:
# previous is either node.right (we've visited both sub-trees)
# or ourself (we don't have a right subtree)
do_something(node)
todo.pop()
previous = node
do_something does whatever you want to call "actually deleting this node".
You can do it a bit more simply by setting an attribute on each node to say whether it has had do_something called on it yet, but obviously that doesn't work so well if your nodes have __slots__ or whatever, and you don't want to modify the node type to allow for the flag.
I'm not sure what you're doing with those conditions after the recursive calls, but I think this should be enough:
def delete(root):
if root:
delete(root.left)
delete(root.right)
root = None
As pointed out in comments, Python does not pass parameters by reference. In that case you can make this work in Python like this:
def delete(root):
if root:
delete(root.left)
delete(root.right)
root.left = None
root.right = None
Usage:
delete(root)
root = None
As for an iterative approach, you can try this. It's pseudocode, I don't know python. Basically we do a BF search.
delete(root):
make an empty queue Q
Q.push(root)
while not Q.empty:
c = Q.popFront()
Q.push(c.left, c.right)
c = None
Again, this won't modify the root by default if you use it as a function, but it will delete all other nodes. You could just set the root to None after the function call, or remove the parameter and work on a global root variable.

Categories

Resources