1

Most of the questions I've searched for regarding binary trees shows the implementation of binary search trees, but not binary trees. The terms of a complete binary tree are:

  • Either an empty tree or it has 1 node with 2 children, where each child is another Binary Tree.
  • All levels are full (except for possibly the last level)
  • All leaves on the bottom-most level are as far left as possible.

I've come up with a concept but it doesn't seem to running through the recursion properly -- Does anyone know what I'm doing wrong?

class Node():
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

    def add(self, key): 
        if self.key: 
            if self.left is None: 
                self.left = Node(key) 
            else: 
                self.left.add(key)

                if self.right is None: 
                    self.right = Node(key) 
                else: 
                    self.right.add(key) 
        else: 
            self.key = key
        return (self.key)
NikeDog
  • 33
  • 7
  • can you post an example of the code you ran to get your returned tree? – Teejay Bruno May 31 '20 at 21:12
  • Sounds like you need to implement something like a self balancing tree. Maybe start from there? – Jason Liu May 31 '20 at 21:15
  • https://stackoverflow.com/questions/34012886/print-binary-tree-level-by-level-in-python TeeJay, I used the code from the first answer on this question to help me visualize what was going on – NikeDog May 31 '20 at 21:18
  • One problem with your code is that after the top of the tree becomes full, you add keys recursively to *both* legs of the tree. – quamrana May 31 '20 at 21:29
  • what do you mean by this? don't I want to be adding to the left and right legs of the tree? – NikeDog May 31 '20 at 21:32
  • You always insert to the left, and if the insertion is not of a new node (but rather a recursive step), you **also** insert to the right. You should try inserting fresh to left, then fresh to right, then recursive left, and then recursive right. To know whether to insert recursive left or right, you should maintain some additional data in the nodes. – Amitai Irron May 31 '20 at 21:33
  • Recursion probably isn't the right way to do this. It might be easier if you think of this a BFS for the first node that isn't full. – Mark May 31 '20 at 22:00
  • it has to be recursion, unfortunately. – NikeDog May 31 '20 at 22:26

3 Answers3

4

The problem in your code is that you are adding the same value multiple times. You add the node, and then still recurse deeper, where you do the same.

The deeper problem is that you don't really know where to insert the node before you have reached the bottom level of the tree, and have detected where that level is incomplete. Finding the correct insertion point may need a traversal through the whole tree... which is defeating the speed gain you would expect to get from using binary trees in the first place.

I provide here three solutions, starting with the most efficient:

1. Using a list as tree implementation

For complete trees there is a special consideration to make: if you number the nodes by level, starting with 0 for the root, and within each level from left to right, you notice that the number of a node's parent is (k-1)/2 when its own number is k. In the other direction: if a node with number k has children, then its left child has number k*2+1, and the right child has a number that is one greater.

Because the tree is complete, there will never be gaps in this numbering, and so you could store the nodes in a list, and use the indexes of that list for the node numbering. Adding a node to the tree now simply means you append it to that list. Instead of a Node object, you just have the tree list, and the index in that list is your node reference.

Here is an implementation:

class CompleteTree(list):
    def add(self, key):
        self.append(key)
        return len(self) - 1

    def left(self, i):
        return i * 2 + 1 if i * 2 + 1 < len(self) else -1

    def right(self, i):
        return i * 2 + 2 if i * 2 + 2 < len(self) else -1            

    @staticmethod
    def parent(i):
        return (i - 1) // 2

    def swapwithparent(self, i):
        if i > 0:
            p = self.parent(i)
            self[p], self[i] = self[i], self[p]

    def inorder(self, i=0):
        left = self.left(i)
        right = self.right(i)
        if left >= 0:
            yield from self.inorder(left)
        yield i
        if right >= 0:
            yield from self.inorder(right)

    @staticmethod
    def depth(i):
        return (i + 1).bit_length() - 1

Here is a demo that creates your example tree, and then prints the keys visited in an in-order traversal, indented by their depth in the tree:

tree = CompleteTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
    print("  " * tree.depth(node), tree[node])

Of course, this means you have to reference nodes a bit different from when you would use a real Node class, but the efficiency gain pays off.

2. Using an extra property

If you know how many nodes there are in a (sub)tree, then from the bit representation of that number, you can know where exactly the next node should be added.

For instance, in your example tree you have 5 nodes. Imagine you want to add a 6 to that tree. The root node would tell you that you currently have 5 and so you need to update it to 6. In binary that is 110. Ignoring the left-most 1-bit, the rest of the bits tell you whether to go left or right. In this case, you should go right (1) and then finally left (0), creating the node in that direction. You can do this iteratively or recursively.

Here is an implementation with recursion:

class Node():
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None
        self.count = 1

    def add(self, key):
        self.count += 1
        if self.left is None:
            self.left = Node(key)
        elif self.right is None:
            self.right = Node(key)
        # extract from the count the second-most significant bit:
        elif self.count & (1 << (self.count.bit_length() - 2)):
            self.right.add(key)
        else:
            self.left.add(key)

    def inorder(self):
        if self.left:
            yield from self.left.inorder()
        yield self
        if self.right:
            yield from self.right.inorder()

tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
for node in tree.inorder():
    print(node.key)

3. Without extra property

If no property can be added to Node objects, then a more extensive search is needed to find the right insertion point:

class Node():
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

    def newparent(self):
        # Finds the node that should serve as parent for a new node
        # It returns a tuple:
        #   if parent found: [-1, parent for new node]
        #   if not found: [height, left-most leaf]
        # In the latter case, the subtree is perfect, and its left-most  
        # leaf is the node to be used, unless self is a right child 
        # and its sibling has the insertion point.
        if self.right:
            right = self.right.newparent()
            if right[0] == -1: # found inbalance
                return right
            left = self.left.newparent()
            if left[0] == -1: # found inbalance
                return left
            if left[0] != right[0]:
                return [-1, right[1]] # found inbalance
            # temporary result in perfect subtree
            return [left[0]+1, left[1]]
        elif self.left:
            return [-1, self] # found inbalance
        # temporary result for leaf
        return [0, self]

    def add(self, key):
        _, parent = self.newparent()
        if not parent.left:
            parent.left = Node(key)
        else:
            parent.right = Node(key)

    def __repr__(self):
        s = ""
        if self.left:
            s += str(self.left).replace("\n", "\n  ")
        s += "\n" + str(self.key)
        if self.right:
            s += str(self.right).replace("\n", "\n  ")
        return s

tree = Node(1)
tree.add(2)
tree.add(3)
tree.add(4)
tree.add(5)
print(tree)

This searches recursively the tree from right to left, to find the candidate parent of the node to be added.

For large trees, this can be improved a bit, by doing a binary-search among paths from root to leaf, based on the length of those paths. But it will still not be as efficient as the first two solutions.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • unfortunately I can't use any lists – NikeDog Jun 01 '20 at 00:44
  • Why not? If you have restrictions, please mention those in your question. Like: is the `Node` class fixed? Can you alter it? Must you use such a class? Can you use dictionaries, tuples, sets, lambdas, loops, ...etc, ...etc? – trincot Jun 01 '20 at 05:58
  • Anyway, I added two alternatives to my answer. – trincot Jun 01 '20 at 09:37
  • I'm tried to implement #2, but I got an syntax error on the line "elif self.count & (1 << (self.count.bit_length() - 2)):". I tried using my own method as the elif statement to differentiate between the left or right branch, but I'm unable to run the "for node in tree.inorder():" line. Where did you get the variable tree? – NikeDog Jun 01 '20 at 13:15
  • I don't get a syntax error. See it run [here](https://repl.it/@trincottrincots/stackoverflowcomq621224195459839). The variable `tree` gets its value with `tree = Node(1)`. It is the root node. Any chance you will answer the above questions? – trincot Jun 01 '20 at 13:34
  • oh sorry, I didn't see the above question. The class can be altered, a class must be used, any type of data storage is not allowed but loops are allowed. – NikeDog Jun 01 '20 at 15:47
  • also, I must have missed the tree value, sorry about that, the code does work. although, i'm thinking that the for loop is not allowed outside of the functions. is there are way to implement it inside the function? – NikeDog Jun 01 '20 at 15:49
  • The `for` loop is just for outputting the tree. You don't need it for the question you asked? – trincot Jun 01 '20 at 15:50
  • just realized that, thank you so much for your help. do you think that the elif statement can only be run using the bit representation? or are there other methods? I've tried keeping track of the levels along with the number of nodes per level, etc. – NikeDog Jun 01 '20 at 15:56
  • You can try another way, but the bit representation seems to be the one that comes most natural to me. Not for nothing it is called a *binary* tree. You *can* use a different expression to get the same result though: `bin(self.count)[3] == "1"`. This performs a conversion of the number to a string in the format "0b....", and skips that "0b" prefix and the first digit to read the second digit. – trincot Jun 01 '20 at 16:10
  • how would I return the root node? I tried adding an if statement: if the count of nodes is at 1 return the key, but it doesn't seem to like that – NikeDog Jun 01 '20 at 18:52
  • You mean that the `add` method should return the root node? In that case, add `return self`. The return value of the recursive calls is ignored any way, and the top-level call is made on the root node, so that is `self`. – trincot Jun 01 '20 at 18:59
  • Are there possibly some edge cases that could be missing? – NikeDog Jun 03 '20 at 21:39
  • 1
    actually this works perfectly, I think it was just some things I changed a little bit while working out the rest of the code. Thanks so much for your help :) – NikeDog Jun 04 '20 at 18:44
0

You can use the sklearn Decision trees, as they are able to be set up as binary decision trees as well. link to the documentation here.

Victor Sim
  • 358
  • 1
  • 13
0

You really need to augment your tree in some way. Since this is not a binary search tree, the only real information you have about each node is whether or not it has a left and right child. Unfortunately, this isn't helpful in navigating a complete binary tree. Imagine a complete binary tree with 10 levels. Until the 9th level, every single node has both a left child and a right child, so you have no way of knowing which path to take down to the leaves. So the question is, what information do you add to each node? I would add the count of nodes in that tree.

Maintaining the count is easy, since every time you descend down a subtree you know to add one to the count at that node. What you want to recognize is the leftmost imperfect subtree. Every perfect binary tree has n = 2^k - 1, where k is the number of levels and n is the number of nodes. There are quick and easy ways to check if a number is 1 less than a power of two (see the first answer to this question), and in fact in a complete binary tree every node has at most one child that isn't the root of a perfect binary tree. Follow a simple rule to add nodes:

  1. If the left child is None, set root.left = Node(key) and return
  2. Else if the right child is None, set root.right = Node(key) and return
  3. If one of the children of the current node is the root of an imperfect subtree, make that node the current node (descend down that subtree)
  4. Else if the sizes are unequal, make the node with the smaller subtree the current node.
  5. Else, make the left child the current node.

By augmenting each node with the size of the subtree rooted there, you have all the information you need at every node to build a recursive solution.

Charles Gleason
  • 416
  • 5
  • 8
  • do you know of any resources where I can check to see if the number is less than the power of two? – NikeDog Jun 01 '20 at 00:06
  • I added a link, it's really just a neat trick with bitwise operations. That's for checking powers of two, so to check one less you just do x & (x + 1) == 0 instead of x & (x - 1) == 0 – Charles Gleason Jun 01 '20 at 01:00
  • how would you descend down the subtree after figuring that out? – NikeDog Jun 01 '20 at 01:17
  • Step four in this algorithm is not always correct. Imagine this tree, listed in BFS order, level by level: [1], [2, 3], [4, 5, null, null]. Now the algo starts at 1: both its children are the root of a perfect subtree, yet the node should be added under the right child, not the left. – trincot Jun 01 '20 at 13:43
  • Ah, you're very correct. I was trying to avoid doing the bit encoding to make a more recursive solution but now it's starting to get crowded. I would say smaller than leftmost should handle it, thank you! – Charles Gleason Jun 01 '20 at 16:21
  • @NikeDog you would just call `self.left.add(key)` to descend down the left subtree (similarly for the right) – Charles Gleason Jun 01 '20 at 16:25