2

So I have a Binary Search Tree and I am trying to get the height of said tree. I have a self.height attribute that increments whenever I do insert_element(self, value), and decrements when remove_element(self, value) happens. However, I notice that this increments and decrements every time when one of the methods occur and doesn't take into account if node was in the same height which wouldn't change the height.

class Binary_Search_Tree:

    class __BST_Node:

        def __init__(self, value):
            self.value = value
            self.lchild = None
            self.rchild = None

    def __init__(self):
        self.__root = None
        self.height = 0

    def insert_element(self, value):
        self._insert(self.__root, value)

    def _insert(self, root, value):
        node = Binary_Search_Tree.__BST_Node(value)
        if root == value:
            raise ValueError("Value already exists")
        if self.__root is None:
            self.__root = Binary_Search_Tree.__BST_Node(value)
            self.height += 1
            return self.__root
        else:
            if root.value >  node.value:
                if root.lchild is None:
                    root.lchild = node 
                    self.height += 1
                    return root.lchild 
                else:
                    self._insert(root.lchild, value)
            elif root.value < node.value:
                if root.rchild is None:
                    root.rchild = node
                    self.height += 1
                    return root.rchild
                else:
                    self._insert(root.rchild, value)
        return root

    def remove_element(self, value):
        self.__root = self._remove(self.__root, value)
        self.height -= 1
        return self.__root

    def _remove(self, root, value):
        if self.__root is None:
            raise ValueError("Tree is empty")
        if root.value != value:
            raise ValueError("No such value")
        if root.value == value:
            if root.lchild is None and root.rchild is None:
                root = None
                self.height -= 1
                return root
            elif root.lchild is None or root.rchild is None:
                if root.lchild is None:
                    self.height -= 1
                    return root.rchild
                if root.rchild is None:
                    self.height -= 1
                    return root.lchild
            elif root.lchild and root.rchild:
                parent = root
                successor = root.rchild
                while successor.lchild:
                    parent = successor
                    successor = successor.lchild
                root.value = successor.value
                if parent.lchild == successor:
                    parent.lchild = successor.rchild
                    self.height -= 1
                    return parent.lchild
                else:
                    parent.rchild = successor.rchild
                    self.height -= 1
                    return parent.rchild
        else:
            if root.value > value:
                if root.lchild:
                root.lchild = self._remove(root.lchild, value)
        elif root.value < value:
            if root.rchild:
                root.rchild = self._remove(root.rchild, value)
        return root

    def in_order(self):
        if self.__root is not None:
            self._in(self.__root)

    def _in(self, root):
        if root is None:
            return
        if root is not None:
            self._in(root.lchild)
            print(str(root.value)) 
            self._in(root.rchild)

    def get_height(self):
        print(str(self.height))

    def __str__(self):
        return self.in_order()


if __name__ == '__main__':
    pass
mapofemergence
  • 458
  • 3
  • 7
oppa76
  • 21
  • 4
  • 1
    Has an answer [here](https://stackoverflow.com/a/15193346/3350448). – PidgeyUsedGust Jun 28 '17 at 08:38
  • For one thing, you handle already existing values wrong, with `if root == value:` in insert. Also, you should not really create the node element before having evaluated where it should be by comparing values. – Pax Vobiscum Jun 28 '17 at 09:23

1 Answers1

0

Here is a version of your tree which should do the job. I tried to keep it as similar as possible to your original code but had to change it a bit, while trying to figure out how to make the example work.

Hopefully it is still close enough, for you to recognize the key parts and re-use it as you need.

class Binary_Search_Tree:

    class __BST_Node:

        def __init__(self, value, height):
            self.value = value
            self.height = height
            self.lchild = None
            self.rchild = None

    def __init__(self):
        self.__root = None
        self.__height = 0

    def _insert(self, knot, value):
        if knot is None:
            self.__root = Binary_Search_Tree.__BST_Node(value, 1)
            result = self.__root
        elif knot.value == value:
            # replace error with WARNING, to handle exception
            # raise ValueError("Value already exists")
            print 'WARNING: value', value, 'already exists; skipped'
            return knot
        elif knot.value > value:
            if knot.lchild is None:
                knot.lchild = Binary_Search_Tree.__BST_Node(value, knot.height + 1) 
                result = knot.lchild 
            else:
                result = self._insert(knot.lchild, value)
        else: # knot.value < value
            if knot.rchild is None:
                knot.rchild = Binary_Search_Tree.__BST_Node(value, knot.height + 1)
                result = knot.rchild
            else:
                result = self._insert(knot.rchild, value)
        self.__height = max(self.__height, result.height)
        return result

    def _remove(self, knot, value):
        """delete the knot with the given value
        based on: https://stackoverflow.com/a/33449471/8200213
        """
        if knot.value == value: # found the node we need to delete
            successor = None
            if knot.rchild and knot.lchild: 
                # get the successor node and its parent 
                successor, parent = knot.rchild, knot
                while successor.lchild:
                    successor, parent = successor.lchild, successor
                # splice out successor (the parent must do this) 
                if parent.lchild == successor:
                    parent.lchild = successor.rchild
                else:
                    parent.rchild = successor.rchild
                # reset the left and right children of the successor
                successor.lchild = knot.lchild
                successor.rchild = knot.rchild
                self._update_heights(successor, knot.height)
                return successor
            # else (not knot.rchild or/and not knot.lchild)
            if knot.lchild:     # promote the left subtree
                self._update_heights(knot.lchild, knot.height)
                return knot.lchild
            elif knot.rchild:   # promote the right subtree 
                self._update_heights(knot.rchild, knot.height)
                return knot.rchild
            # else: no children
            self._update_heights(knot, knot.height)
            return
        # else: keep traversing
        if knot.value > value and knot.lchild is not None:
            # value should be in the left subtree
            knot.lchild = self._remove(knot.lchild, value)
        elif knot.value < value and knot.rchild is not None:
            # value should be in the right subtree
            knot.rchild = self._remove(knot.rchild, value)
        # else: the value is not in the tree
        return knot

    def _update_heights(self, knot, height, maxheight=0):
        # print 'update from knot value', knot.value, 'with height', knot.height, 'to height', height
        maxheight = max(maxheight, knot.height)
        knot.height = height
        if knot.lchild is not None:
            self._update_heights(knot.lchild, knot.height + 1, maxheight)
        if knot.rchild is not None:
            self._update_heights(knot.rchild, knot.height + 1, maxheight)
        if maxheight == self.__height:
            # the max height of the whole tree might have changed; re-compute
            self.__height = -1

    def _recompute_height(self, knot):
        if not knot:
            return
        self.__height = max(self.__height, knot.height)
        if knot.lchild is not None:
            self._recompute_height(knot.lchild)
        if knot.rchild is not None:
            self._recompute_height(knot.rchild)

    def _get_ordered(self, knot, pre=False, nodelist=None):
        nodelist = nodelist or []
        if knot is None:
            return nodelist
        if not pre:
            nodelist = self._get_ordered(knot.lchild, pre, nodelist)
        nodelist.append(knot.value)
        if pre:
            nodelist = self._get_ordered(knot.lchild, pre, nodelist)
        nodelist = self._get_ordered(knot.rchild, pre, nodelist)
        return nodelist

    def insert_element(self, value):
        self._insert(self.__root, value)
        # print self.__height

    def remove_element(self, value):
        self.__root = self._remove(self.__root, value)
        # print self.get_height()

    def get_pre_order(self):
        return self._get_ordered(self.__root, True)

    def get_in_order(self):
        return self._get_ordered(self.__root)

    def get_height(self):
        if self.__height == -1:
            # self.__height was marked "dirty" 
            # re-computing tree height, as it might have changed
            self._recompute_height(self.__root)
        return self.__height

def __str__(self):
        return ', '.join(['%d' % val for val in self.get_in_order()])

As suggested in the link posted by PidgeyUsedGust, you probably want to store the height in the nodes, so you can easily compute both the height of any newly inserted node and the overall height of the tree.

Keeping the height up-to-date after removing nodes is less trivial. What I came out with, is a semi-naive implementation which aims to a decent compromise between code simplicity and performance (which means time is generally acceptable but not constant; if anybody knows better solutions, I'd be more than happy to hear their take).

Here's the idea:

  • every time you remove a node, you want to update the height of that node and all its descendants, leaving untouched the rest of the tree. This is what the _update_heights method does
  • while doing so, every time we hit a node, the height of which (before updating) equals the overall height, we mark the tree attribute "dirty" (self.__height = -1)
  • next time we'll call get_height on the tree, if self.__height == -1 we'll know its height might have changed and needs re-computing, by calling the _recompute_height method

The reason why _recompute_height is triggered only by get_height, rather than by _update_heights, is to avoid re-computing the attribute every time an element is removed, if that's not required (ie. you might need to remove 1000 elements and check the new height of the tree only once, after that).

Here a few lines to test the code (the drawtree module is used: you'll need to install it, if you didn't already):

if __name__ == '__main__':

    bst = Binary_Search_Tree()
    values = [7, 2, 2, 5, 1, 8, 3, 6, 9, 8, 4, 11, 10, 12]
    print 'insert values', values
    for val in values:
        bst.insert_element(val)
    print 'tree successfully built\nheight: %d' % bst.get_height()
    print 'ordered: %s' % (bst, )
    print 'pre-ordered:', bst.get_pre_order(), '\n'

    import drawtree
    drawtree.draw_bst(bst.get_pre_order())

    values = [4, 2, 7, 4]
    for val in values:
        print '\n\nremove value %d\n' % val
        bst.remove_element(val)
        drawtree.draw_bst(bst.get_pre_order())

    print '\n\nnew height: %d' % bst.get_height()

And here's the result you should get:

insert values [7, 2, 2, 5, 1, 8, 3, 6, 9, 8, 4, 11, 10, 12]
WARNING: value 2 already exists; skipped
WARNING: value 8 already exists; skipped
tree successfully built
height: 5
ordered: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
pre-ordered: [7, 2, 1, 5, 3, 4, 6, 8, 9, 11, 10, 12] 

     7
    / \
   /   \
  2     8
 / \     \
1   5     9
   / \     \
  3   6    11
   \       / \
    4     /   \
         10   12


remove value 6

    7
   / \
  2   8
 / \   \
1   5   9
   /     \
  3      11
   \     / \
    4   /   \
       10   12


remove value 2

    7
   / \
  3   8
 / \   \
1   5   9
   /     \
  4      11
         / \
        /   \
       10   12


remove value 7

     8
    / \
   /   \
  3     9
 / \     \
1   5    11
   /     / \
  4     /   \
       10   12


new height: 4
mapofemergence
  • 458
  • 3
  • 7