Home/Coding Guide/Trees & Graphs — Advanced
⌂ Main Page
Coding Interview Preparation Guide

Trees & Graphs — Advanced

Harder tree and graph challenges: balancing, path problems, and non-trivial graph algorithms.

Challenges on this page

TREE SOLUTIONS

Python
# iterative max_height()
#def max_height(root):
#    if not root:
#        return 0
#    height = 0
#    queue = [root]
#    while queue:
#        height += 1
#        level = []
#        while queue:
#            node = queue.pop(0)
#            if node.left:
#                level.append(node.left)
#            if node.right:
#                level.append(node.right)
#        queue = level
#    return height

Check if a given array can represent Preorder Traversal of Binary Search Tree

Efficient solution w/time c. O(n). The idea is to use a stack. This problem is similar to Next (or closest) Greater Element problem. Using stack, we find the next greater element after which, if we find a smaller element, then return false

Python
INT_MIN = -2**32
  
def canRepresentBST(pre): 
  
    # stack 
    s = [] 

    root = INT_MIN 
  
    for value in pre:  

        if value < root: 
            return False 

        # If value(pre[i]) is in right subtree of stack top,  
        # Keep removing items smaller than value 
        # and make the last removed item new root 
        while(len(s) > 0 and s[-1] < value) : 
            root = s.pop() 
          
        # Either stack is empty or value < root => push value 
        s.append(value) 
  
    return True
  
# Driver Program 
pre1 = [40 , 30 , 35 , 80 , 100] 
print('True' if canRepresentBST(pre1) else 'False')
pre2 = [40 , 30 , 35 , 20 ,  80 , 100] 
print('True' if canRepresentBST(pre2) else 'False')
Output
True
False

Remove nodes on root-to-leaf paths of length < k

If node X is on multiple root-to-leaf paths and if any of the paths >= k, then X is not deleted => node is deleted iff all paths going through it have lengths < k

Solution: do post order traversal. Before removing a node, check if all its children in the shorter path are removed. Two cases:
i) This node becomes a leaf node in which case it needs to be deleted.
ii) This node has another child on a path with length >= k. In that case it needs not to be deleted

Time c. O(n) where n = # nodes

Python
'''
               1
           /      \
         2          3
      /     \         \
    4         5        6
  /                   /
 7                   8 
Input: Root of above Binary Tree
       k = 4

Output: The tree should be changed to following  
           1
        /     \
      2          3
     /             \
   4                 6
 /                  /
7                  8

There are 3 paths 
i) 1->2->4->7      path length = 4
ii) 1->2->5        path length = 3
iii) 1->3->6->8    path length = 4 

There is only one path " 1->2->5 " of length smaller than 4.  
The node 5 is the only node that lies only on this path, so 
node 5 is removed.
Nodes 2 and 1 are not removed as they are parts of other paths
of length 4 as well.

If k is 5 or greater than 5, then whole tree is deleted. 

If k is 3 or less than 3, then nothing is deleted
'''
pass
Python
class Node:
        
    def __init__(self, data):
                
        self.data = data  
        self.left = self.right = None
          
# remove nodes on that are not on pathLen >= k 
def util_method(root, level, k) : 
  
    
    if (root == None):                                                               # base case  
        return None
  
    # postorder fashion traversal: if a leaf node path length < k,
    # then this node and all its ancestors until a node on another path are removed
    root.left = util_method(root.left, level + 1, k)
    root.right = util_method(root.right, level + 1, k)
  
    # if root = leaf node & its level < k, then remove it
    # check ancestor nodes until finding a node that's  part of another path
    if (root.left == None and
        root.right == None and level < k) :  
        return None
      
    # Return root  
    return root  
  

def remove_short_path_nodes(root, k) : 
    return util_method(root, 1, k)
  

def in_order(root) : 
  
    if (root):
                
        in_order(root.left)
        print(root.data, end = " " )
        in_order(root.right)

        
k = 4
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)
root.left.left.left = Node(7)
root.right.right = Node(6)
root.right.right.left = Node(8)
print('Inorder traversal of initial tree' )  
in_order(root)
print()
print('Inorder traversal of modified tree')  
res = remove_short_path_nodes(root, k)
in_order(res)
Output
Inorder traversal of initial tree
7 4 2 5 1 3 8 6 
Inorder traversal of modified tree
7 4 2 1 3 8 6
Python
### Remove nodes on root-to-leaf paths of length < k
If node X is on multiple root-to-leaf paths and if any of the paths >= k, then X is not deleted => node is deleted iff all paths going through it have lengths < k

Solution: do post order traversal. Before removing a node, check if all its children in the shorter path are removed. Two cases:  
i) This node becomes a leaf node in which case it needs to be deleted.  
ii) This node has another child on a path with length >= k. In that case it needs not to be deleted

Time c. O(n) where n = # nodes

'''
               1
           /      \
         2          3
      /     \         \
    4         5        6
  /                   /
 7                   8 
Input: Root of above Binary Tree
       k = 4

Output: The tree should be changed to following  
           1
        /     \
      2          3
     /             \
   4                 6
 /                  /
7                  8

There are 3 paths 
i) 1->2->4->7      path length = 4
ii) 1->2->5        path length = 3
iii) 1->3->6->8    path length = 4 

There is only one path " 1->2->5 " of length smaller than 4.  
The node 5 is the only node that lies only on this path, so 
node 5 is removed.
Nodes 2 and 1 are not removed as they are parts of other paths
of length 4 as well.

If k is 5 or greater than 5, then whole tree is deleted. 

If k is 3 or less than 3, then nothing is deleted
'''
pass

class Node:
        
    def __init__(self, data):
                
        self.data = data  
        self.left = self.right = None
          
# remove nodes on that are not on pathLen >= k 
def util_method(root, level, k) : 
  
    
    if (root == None):                                                               # base case  
        return None
  
    # postorder fashion traversal: if a leaf node path length < k,
    # then this node and all its ancestors until a node on another path are removed
    root.left = util_method(root.left, level + 1, k)
    root.right = util_method(root.right, level + 1, k)
  
    # if root = leaf node & its level < k, then remove it
    # check ancestor nodes until finding a node that's  part of another path
    if (root.left == None and
        root.right == None and level < k) :  
        return None
      
    # Return root  
    return root  
  

def remove_short_path_nodes(root, k) : 
    return util_method(root, 1, k)
  

def in_order(root) : 
  
    if (root):
                
        in_order(root.left)
        print(root.data, end = " " )
        in_order(root.right)

        
k = 4
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)
root.left.left.left = Node(7)
root.right.right = Node(6)
root.right.right.left = Node(8)
print('Inorder traversal of initial tree' )  
in_order(root)
print()
print('Inorder traversal of modified tree')  
res = remove_short_path_nodes(root, k)
in_order(res)

APPENDIX

Algorithms from https://bradfieldcs.com/algos/graphs/ + Udemy

Python
# WORD LADDER - FOOL, POOL, POLL, POLE, PALE, SALE, SAGE
# Load wrods file and build graph
from collections import defaultdict
from itertools import product
import os


def build_graph(words):
    buckets = defaultdict(list)
    graph = defaultdict(set)

    for word in words:
        for i in range(len(word)):
            bucket = '{}_{}'.format(word[:i], word[i + 1:])
            buckets[bucket].append(word)

    # add vertices and edges for words in the same bucket
    for bucket, mutual_neighbors in buckets.items():
        for word1, word2 in product(mutual_neighbors, repeat=2):
            if word1 != word2:
                graph[word1].add(word2)
                graph[word2].add(word1)

    return graph


def get_words(vocabulary_file):
    for line in open(vocabulary_file, 'r'):
        yield line[:-1]  # remove newline character


vocabulary_file = 'vocabulary.txt' #os.path.join(os.path.dirname(__file__), 'vocabulary.txt')
word_graph = build_graph(get_words(vocabulary_file))
Python
# BREADTH FIRST SEARCH IN GRAPH
from collections import deque


def traverse(graph, starting_vertex):
    visited = set()
    queue = deque([[starting_vertex]])
    while queue:
        path = queue.popleft()
        vertex = path[-1]
        yield vertex, path
        for neighbor in graph[vertex] - visited:
            visited.add(neighbor)
            queue.append(path + [neighbor])

if __name__ == '__main__':
    for vertex, path in traverse(word_graph, 'FOOL'):
        if vertex == 'SAGE':
            print(' -> '.join(path))
Output
FOOL -> WOOL -> WOOS -> WOGS -> WAGS -> WAGE -> SAGE

Knight’s Tour

https://bradfieldcs.com/algos/graphs/knights-tour/ (time permits)
Find a sequence of moves that allow the knight to visit every square on the chess board exactly once

DFS - Another Solution

Python
# DEPTH FIRST TRAVERSAL
# {'discovery': m, 'finish': n}, where the m and n are integers obtained by incrementing a counter before
# and after each time a new vertex is traversed

simple_graph = {
    'A': ['B', 'D'],
    'B': ['C', 'D'],
    'C': [],
    'D': ['E'],
    'E': ['B', 'F'],
    'F': ['C']
}


def depth_first_search(graph, starting_vertex):
    visited = set()
    counter = [0]
    traversal_times = defaultdict(dict)

    def traverse(vertex):
        visited.add(vertex)
        counter[0] += 1
        traversal_times[vertex]['discovery'] = counter[0]

        for next_vertex in graph[vertex]:
            if next_vertex not in visited:
                traverse(next_vertex)

        counter[0] += 1
        traversal_times[vertex]['finish'] = counter[0]

    # in this case start with just one vertex, but we could equally
    # dfs from all_vertices to produce a dfs forest
    traverse(starting_vertex)
    return traversal_times

traversal_times = depth_first_search(simple_graph, 'A')
traversal_times
Output
defaultdict(dict,
            {'A': {'discovery': 1, 'finish': 12},
             'B': {'discovery': 2, 'finish': 11},
             'C': {'discovery': 3, 'finish': 4},
             'D': {'discovery': 5, 'finish': 10},
             'E': {'discovery': 6, 'finish': 9},
             'F': {'discovery': 7, 'finish': 8}})

Other implementations of topological sorting

Python
# source = https://www.studytonight.com/python-programs/python-program-for-topological-sorting
from collections import defaultdict
class Graph:
    def __init__(self, directed=False):
        self.graph = defaultdict(list)
        self.directed = directed

    def Edge(self, frm, to):
        self.graph[frm].append(to)

        if self.directed is False:
            self.graph[to].append(frm)
        else:
            self.graph[to] = self.graph[to]

    def visit(self, s, visited, sortlist):
        visited[s] = True

        for i in self.graph[s]:
            if not visited[i]:
                self.visit(i, visited, sortlist)

        sortlist.insert(0, s)

    def topological_Sort(self):
        visited = {i: False for i in self.graph}

        sortlist = []
       
        for v in self.graph:
            if not visited[v]:
                self.visit(v, visited, sortlist)

        print(sortlist)

#driver code
if __name__ == '__main__':
 
    g = Graph(directed=True)

    g.Edge(1, 2)
    g.Edge(2, 3)
    g.Edge(3, 4)
    g.Edge(4, 5)
    g.Edge(5, 6)
    g.Edge(6, 7)
    g.Edge(7, 8)
    
    print("The Result after Topological Sort:")
    g.topological_Sort()
Output
The Result after Topological Sort:
[1, 2, 3, 4, 5, 6, 7, 8]

Topological Sorting vs Depth First Traversal (DFS) In DFS, we print a vertex and then recursively call DFS for its adjacent vertices. In topological sorting, we need to print a vertex before its adjacent vertices. For example, in the given graph, the vertex ‘5’ should be printed before vertex ‘0’, but unlike DFS, vertex ‘4’ should also be printed before vertex ‘0’. So Topological sorting is different from DFS. For example, a DFS of the shown graph is “5 2 3 1 0 4”, but it is not a topological sorting.

The comparison below is taken from here: https://stackoverflow.com/questions/47192626/deceptively-simple-implementation-of-topological-sorting-in-python

Python
def iterative_dfs(graph, start):
        
    seen , q = set(), [start]                               # efficient set to look up nodes in
    path = []                                               # there was no good reason for this to be an argument in your code
    
    while q:
        v = q.pop()                                         # no reason not to pop from the end, where it's fast
        if v not in seen:
            path.append(v)
            seen.add(v)
            q.extend(graph[v])                              # this will add the nodes in a slightly different order
                                                            # if you want the same order, use reversed(graph[v])

    return path


def iterative_topological_sort(graph, start):
        
    seen = set()
    q = [start]
    
    stack = []                                               # path variable is gone, stack and order are new
    order = []                                               # order will be in reverse order at first
    
    while q:
        v = q.pop()
        if v not in seen:
            seen.add(v)                                      # no need to append to path any more
            q.extend(graph[v])

            while stack and v not in graph[stack[-1]]:       # new stuff here!
                order.append(stack.pop())
            stack.append(v)

    return stack + order[::-1]                               # new return value!
Python
def recursive_dfs(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                result.append(neighbor)                       # this line will be replaced below
                seen.add(neighbor)
                recursive_helper(neighbor)

    recursive_helper(node)
    return result


def recursive_topological_sort(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                seen.add(neighbor)
                recursive_helper(neighbor)
        result.insert(0, node)                                 # this line replaces the result.append line

    recursive_helper(node)
    return result


graph = {
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['d'],
    'd': ['e'],
    'e': []
}
print(iterative_dfs(graph, 'a'))
print(iterative_topological_sort(graph, 'a'))
#print(recursive_dfs(graph, 'a'))                              # both recursive implementations provide results
#print(recursive_topological_sort(graph, 'a'))                 # different than iterative - why?
Output
['a', 'c', 'd', 'e', 'b']
['b', 'd', 'e', 'c']
['a', 'b', 'c', 'd', 'e']
['a', 'c', 'b', 'd', 'e']

More Elaborate Example of Graph Data Structure: https://bradfieldcs.com/algos/graphs/ + Udemy

Graph() creates a new, empty graph.
addVertex(vert) adds an instance of Vertex to the graph.
addEdge(fromVert, toVert) Adds a new, directed edge to the graph that connects two vertices.
addEdge(fromVert, toVert, weight) Adds a new, weighted, directed edge to the graph that connects two vertices.
getVertex(vertKey) finds the vertex in the graph named vertKey.
getVertices() returns the list of all vertices in the graph.
in returns True for a statement of the form vertex in graph, if the given vertex is in the graph, False otherwise

Python
# OBJECT-BASED ADJACENCY LIST
# https://bradfieldcs.com/algos/graphs/representing-a-graph/ + Udemy
# neighbors = dict of vertices connected to this one and weights of edges to them; use set if no weights
class Vertex:
    def __init__(self, key):
        self.key = key
        self.neighbors = {}

    # add a connection from this vertex to another
    def add_neighbor(self, neighbor, weight=0):
        self.neighbors[neighbor] = weight

    def __str__(self):
        return '{} neighbors: {}'.format(
            self.key,
            [x.key for x in self.neighbors]
        )

    # get all of the vertices in the adjacency list
    def get_connections(self):
        return self.neighbors.keys()

    # get weight of the edge from this vertex to neighbor
    def get_weight(self, neighbor):
        return self.neighbors[neighbor]
Python
# dict to map vertex names to vertex objects
class Graph(object):
    def __init__(self):
        self.verticies = {}

    # adding vertex to graph 
    def add_vertex(self, vertex):
        self.verticies[vertex.key] = vertex


    def get_vertex(self, key):
        try:
            return self.verticies[key]
        except KeyError:
            return None

    def __contains__(self, key):
        return key in self.verticies

    # connect vertices
    def add_edge(self, from_key, to_key, weight=0):
        if from_key not in self.verticies:
            self.add_vertex(Vertex(from_key))
        if to_key not in self.verticies:
            self.add_vertex(Vertex(to_key))
        self.verticies[from_key].add_neighbor(self.verticies[to_key], weight)
    
    # names of all vertices in graph
    def get_vertices(self):
        return self.verticies.keys()

    # iterate over all the vertex objects (or names by using get_vertices())
    def __iter__(self):
        return iter(self.verticies.values())
Python
g = Graph()

for i in range(6):
    g.add_vertex(Vertex(i))
print(g.verticies)

g.add_edge(0, 1, 5)
g.add_edge(0, 5, 2)
g.add_edge(1, 2, 4)
g.add_edge(2, 3, 9)
g.add_edge(3, 4, 7)
g.add_edge(3, 5, 3)
g.add_edge(4, 0, 1)
g.add_edge(5, 4, 8)
g.add_edge(5, 2, 1)
for v in g:
    for w in v.get_connections():
        print('{} -> {}'.format(v.key, w.key))
Output
{0: <__main__.Vertex object at 0x000001AFEF9F4FD0>, 1: <__main__.Vertex object at 0x000001AFEF9F4F60>, 2: <__main__.Vertex object at 0x000001AFEF9F4EB8>, 3: <__main__.Vertex object at 0x000001AFEF9F42E8>, 4: <__main__.Vertex object at 0x000001AFEF9F4240>, 5: <__main__.Vertex object at 0x000001AFEF9F4D68>}
0 -> 1
0 -> 5
1 -> 2
2 -> 3
3 -> 4
3 -> 5
4 -> 0
5 -> 4
5 -> 2

More on binary trees

Check if BST (another solution)

Python
"""
Given a binary tree, determine if it is a valid binary search tree (BST).

Assume a BST is defined as follows:

The left subtree of a node contains only nodes
with keys less than the node's key.
The right subtree of a node contains only nodes
with keys greater than the node's key.
Both the left and right subtrees must also be binary search trees.
Example 1:
    2
   / \
  1   3
Binary tree [2,1,3], return true.
Example 2:
    1
   / \
  2   3
Binary tree [1,2,3], return false.
"""


def is_bst(root):
    """
    :type root: TreeNode
    :rtype: bool
    """
    if not root:
        return True
    stack = []
    pre = None
    while root and stack:
        while root:
            stack.append(root)
            root = root.left
        root = stack.pop()
        if pre and root.val <= pre.val:
            return False
        pre = root
        root = root.right
    return True

Balanced binary tree: AVL tree (others: red/black, splay - longer)

Height-balanced BT = left and right subtrees of every node differ in height by no more than 1
AVL tree = self-balancing BST (if heights of left and right subtrees differ by more than one, rebalancing is done)

Python
#   Author: Tim Rijavec
#           tim@coder.si
#           http://coder.si

class avlnode(object):
    """
    A node in an avl tree.
    """

    def __init__(self, key):
                
        self.key = key
        self.left = None
        self.right = None
        
class avltree(object):    
    
    
    def __init__(self):
                
        self.node = None        
        self.height = -1
        # Balance factor 
        self.balance = 0
        
    def insert(self, key):
        """
        Insert new key into node
        """
        # Create new node
        n = avlnode(key)

        # Initial tree
        if not self.node:
            self.node = n
            self.node.left = avltree()
            self.node.right = avltree()
        # Insert key to the left subtree
        elif key < self.node.key:
            self.node.left.insert(key)
        # Insert key to the right subtree
        elif key > self.node.key:
            self.node.right.insert(key)
            
        # Rebalance tree if needed
        self.rebalance()
        
    def rebalance(self):
        """
        Rebalance tree. After inserting or deleting a node, 
        it is necessary to check each of the node's ancestors for consistency with the rules of AVL
        """

        # Check if we need to rebalance the tree
        #   update height
        #   balance tree
        self.update_heights(recursive=False)
        self.update_balances(False)

        # For each node checked, 
        #   if the balance factor remains −1, 0, or +1 then no rotations are necessary.
        while self.balance < -1 or self.balance > 1: 
            # Left subtree is larger than right subtree
            if self.balance > 1:

                # Left Right Case -> rotate y,z to the left
                if self.node.left.balance < 0:
                    #     x               x
                    #    / \             / \
                    #   y   D           z   D
                    #  / \        ->   / \
                    # A   z           y   C
                    #    / \         / \
                    #   B   C       A   B
                    self.node.left.rotate_left()
                    self.update_heights()
                    self.update_balances()

                # Left Left Case -> rotate z,x to the right
                #       x                 z
                #      / \              /   \
                #     z   D            y     x
                #    / \         ->   / \   / \
                #   y   C            A   B C   D 
                #  / \ 
                # A   B
                self.rotate_right()
                self.update_heights()
                self.update_balances()
            
            # Right subtree is larger than left subtree
            if self.balance < -1:
                
                # Right Left Case -> rotate x,z to the right
                if self.node.right.balance > 0:
                    #     y               y
                    #    / \             / \
                    #   A   x           A   z
                    #      / \    ->       / \
                    #     z   D           B   x
                    #    / \                 / \
                    #   B   C               C   D
                    self.node.right.rotate_right() # we're in case III
                    self.update_heights()
                    self.update_balances()

                # Right Right Case -> rotate y,x to the left
                #       y                 z
                #      / \              /   \
                #     A   z            y     x
                #        / \     ->   / \   / \
                #       B   x        A   B C   D 
                #          / \ 
                #         C   D
                self.rotate_left()
                self.update_heights()
                self.update_balances()

    def update_heights(self, recursive=True):
        """
        Update tree height

        Tree height is max height of either left or right subtrees +1 for root of the tree
        """
        if self.node: 
            if recursive: 
                if self.node.left: 
                    self.node.left.update_heights()
                if self.node.right:
                    self.node.right.update_heights()
            
            self.height = 1 + max(self.node.left.height, self.node.right.height)
        else: 
            self.height = -1

    def update_balances(self, recursive=True):
        """
        Calculate tree balance factor

        The balance factor is calculated as follows: 
            balance = height(left subtree) - height(right subtree). 
        """
        if self.node:
            if recursive:
                if self.node.left:
                    self.node.left.update_balances()
                if self.node.right:
                    self.node.right.update_balances()

            self.balance = self.node.left.height - self.node.right.height
        else:
            self.balance = 0 

            
    def rotate_right(self):
        """
        Right rotation
            set self as the right subtree of left subree
        """
        new_root = self.node.left.node
        new_left_sub = new_root.right.node
        old_root = self.node

        self.node = new_root
        old_root.left.node = new_left_sub
        new_root.right.node = old_root

    def rotate_left(self):
        """
        Left rotation
            set self as the left subtree of right subree
        """
        new_root = self.node.right.node
        new_left_sub = new_root.left.node
        old_root = self.node

        self.node = new_root
        old_root.right.node = new_left_sub
        new_root.left.node = old_root

    def delete(self, key):
        """
        Delete key from the tree

        Let node X be the node with the value we need to delete, 
        and let node Y be a node in the tree we need to find to take node X's place, 
        and let node Z be the actual node we take out of the tree.

        Steps to consider when deleting a node in an AVL tree are the following:

            * If node X is a leaf or has only one child, skip to step 5. (node Z will be node X)
                * Otherwise, determine node Y by finding the largest node in node X's left sub tree 
                    (in-order predecessor) or the smallest in its right sub tree (in-order successor).
                * Replace node X with node Y (remember, tree structure doesn't change here, only the values). 
                    In this step, node X is essentially deleted when its internal values were overwritten with node Y's.
                * Choose node Z to be the old node Y.
            * Attach node Z's subtree to its parent (if it has a subtree). If node Z's parent is null, 
                update root. (node Z is currently root)
            * Delete node Z.
            * Retrace the path back up the tree (starting with node Z's parent) to the root, 
                adjusting the balance factors as needed.
        """
        if self.node != None:
            if self.node.key == key:
                # Key found in leaf node, just erase it
                if not self.node.left.node and not self.node.right.node:
                    self.node = None
                # Node has only one subtree (right), replace root with that one
                elif not self.node.left.node:                
                    self.node = self.node.right.node
                # Node has only one subtree (left), replace root with that one
                elif not self.node.right.node:
                    self.node = self.node.left.node
                else:
                    # Find  successor as smallest node in right subtree or
                    #       predecessor as largest node in left subtree
                    successor = self.node.right.node  
                    while successor and successor.left.node:
                        successor = successor.left.node

                    if successor:
                        self.node.key = successor.key

                        # Delete successor from the replaced node right subree
                        self.node.right.delete(successor.key)

            elif key < self.node.key:
                self.node.left.delete(key)

            elif key > self.node.key:
                self.node.right.delete(key)

            # Rebalance tree
            self.rebalance()

    def inorder_traverse(self):
        """
        Inorder traversal of the tree
            Left subree + root + Right subtree
        """
        result = []

        if not self.node:
            return result
        
        result.extend(self.node.left.inorder_traverse())
        result.append(self.node.key)
        result.extend(self.node.right.inorder_traverse())

        return result

    def display(self, node=None, level=0):
        if not node:
            node = self.node

        if node.right.node:
            self.display(node.right.node, level + 1)
            print ('\t' * level), ('    /')

        print ('\t' * level), node

        if node.left.node:
            print ('\t' * level), ('    \\')
            self.display(node.left.node, level + 1)

# Demo    
if __name__ == "__main__": 
    tree = avltree()
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

    from random import randrange
    for key in data:  
        tree.insert(key)

    for key in [4,3]:
        tree.delete(key)
        
    print(tree.inorder_traverse())
    tree.display()
Output
[1, 2, 5, 6, 7, 8, 9, 10, 11]

Another implementation of BT

Python
"""
Methods:
    1. Insert
    2. Search
    3. Size
    4. Traversal (Preorder, Inorder, Postorder)
"""

import unittest

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

class BST(object):
    def __init__(self):
        self.root = None

    def get_root(self):
        return self.root

    """
        Get the number of elements
        Using recursion. Complexity O(logN)
    """
    def size(self):
        return self.recur_size(self.root)

    def recur_size(self, root):
        if root is None:
            return 0
        else:
            return 1 + self.recur_size(root.left) + self.recur_size(root.right)

    """
        Search data in bst
        Using recursion. Complexity O(logN)
    """
    def search(self, data):
        return self.recur_search(self.root, data)

    def recur_search(self, root, data):
        if root is None:
            return False
        if root.data == data:
            return True
        elif data > root.data:     # Go to right root
            return self.recur_search(root.right, data)
        else:                      # Go to left root
            return self.recur_search(root.left, data)

    """
        Insert data in bst
        Using recursion. Complexity O(logN)
    """
    def insert(self, data):
        if self.root:
            return self.recur_insert(self.root, data)
        else:
            self.root = Node(data)
            return True

    def recur_insert(self, root, data):
        if root.data == data:      # The data is already there
            return False
        elif data < root.data:     # Go to left root
            if root.left:          # If left root is a node
                return self.recur_insert(root.left, data)
            else:                  # left root is a None
                root.left = Node(data)
                return True
        else:                      # Go to right root
            if root.right:         # If right root is a node
                return self.recur_insert(root.right, data)
            else:
                root.right = Node(data)
                return True

    """
        Preorder, Postorder, Inorder traversal bst
    """
    def preorder(self, root):
        if root:
            print(str(root.data), end = ' ')
            self.preorder(root.left)
            self.preorder(root.right)

    def inorder(self, root):
        if root:
            self.inorder(root.left)
            print(str(root.data), end = ' ')
            self.inorder(root.right)

    def postorder(self, root):
        if root:
            self.postorder(root.left)
            self.postorder(root.right)
            print(str(root.data), end = ' ')

"""
    The tree is created for testing:

                    10
                 /      \
               6         15
              / \       /   \
            4     9   12      24
                 /          /    \
                7         20      30
                         /
                       18
"""

class TestSuite(unittest.TestCase):
    def setUp(self):
        self.tree = BST()
        self.tree.insert(10)
        self.tree.insert(15)
        self.tree.insert(6)
        self.tree.insert(4)
        self.tree.insert(9)
        self.tree.insert(12)
        self.tree.insert(24)
        self.tree.insert(7)
        self.tree.insert(20)
        self.tree.insert(30)
        self.tree.insert(18)

    def test_search(self):
        self.assertTrue(self.tree.search(24))
        self.assertFalse(self.tree.search(50))

    def test_size(self):
        self.assertEqual(11, self.tree.size())

#if __name__ == '__main__':
unittest.main()
Output
E
======================================================================
ERROR: C:\Users\anedilko\AppData\Roaming\jupyter\runtime\kernel-2e709d0e-8e3c-47b6-baba-4e40753cba32 (unittest.loader._FailedTest)
----------------------------------------------------------------------
AttributeError: module '__main__' has no attribute 'C:\Users\anedilko\AppData\Roaming\jupyter\runtime\kernel-2e709d0e-8e3c-47b6-baba-4e40753cba32'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
An exception has occurred, use %tb to see the full traceback.

SystemExit: True
C:\Users\anedilko\AppData\Local\Continuum\anaconda3\envs\ot\lib\site-packages\IPython\core\interactiveshell.py:3304: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
  warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

Everything about binary heaps

BST Iterator

Python
class BSTIterator:
    def __init__(self, root):
        self.stack = []
        while root:
            self.stack.append(root)
            root = root.left

    def has_next(self):
        return bool(self.stack)

    def next(self):
        node = self.stack.pop()
        tmp = node
        if tmp.right:
            tmp = tmp.right
            while tmp:
                self.stack.append(tmp)
                tmp = tmp.left
        return node.val