Data Structures and Algorithms: A Simple Guide — Binary Trees

Sumedha J
6 min readJul 16, 2023

--

Data structures and algorithms give you a solid understanding of programming. These are the fundamentals which should be learnt by every software engineer. Additionally, they are very interesting and quite honestly, simple! All you need is a proper approach to understand them. In this DSA series, I’ll try to delve into the most common and important topics and hopefully make them a cakewalk!

Binary Trees

What exactly is a Binary Tree? Well, we need to check for three conditions. These are amazingly simple.

A Binary tree has only one root node, each node can have utmost 2 children (0, 1 or 2 children), and finally, each node should have a unique path from the root.

What are the different tree traversals?

There are two types — Depth first and breadth first. Depth first traversal is implemented by using stack data structure and that of breadth first by queue. That is, starting from the root of the tree, if you implement a stack data structure, depth first is automatically implemented and the same goes to queue and breadth first. Isn’t that amazing?

An exmaple of a binary tree.
a
/\
b c
/\ \
d e f

Depth first traversal - a, b, d, e, c, f
Breadth first traversal - a, b, c, d, e, f

Depth first gets more interesting. Since it uses stack, it can also be implemented using recursion! The underlying call stack of the recursion is utilised. Anything which can be solved using stack data structure can also be solved using recursion. This is a good point to keep in mind!

Another thing to notice about depth first is, there are three types of depth first traversals — In order, pre order and post order. The names refer to the position of the root value.

An exmaple of a binary tree.
a
/\
b c
/\ \
d e f

Depth first traversals -
Pre order(root, left subtree, right subtree) - a, b, d, e, c, f
In order(left subtree, root, right subtree) - d, b, e, a, c, f
Post order(left subtree, right subtree, root) - d, e, b, f, c, a

How to implement depth first traversal (iteratively ie., using stack)?

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

depthFirst(root):
# Iterative version, using a stack
stack = [root]
values = []
while (len(stack) != 0):
current = stack.pop()
values.append(current.val)
if current.right != None:
stack.append(current.right)
if current.left != None: # Last in is the first popped out!
stack.append(current.left)
print(values)

a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')
a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

# a
# /\
# b c
# /\ \
# d e f

depthFirst(a)
# Prints ['a', 'b', 'd', 'e', 'c', 'f']

How to implement breadth first traversal (iteratively ie., using queue)?

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


from collections import deque

def breadthFirst(root):
queue = deque([root])
values = []
while len(queue) != 0:
current = queue.popleft() # Equivalent to pop(0) - Remove first element, but with O(1) time complexity
values.append(current.val)
if current.left != None:
queue.append(current.left)
if current.right != None:
queue.append(current.right)
print(values)

a = Node('a')
b = Node('b')
c = Node('c')
d = Node('d')
e = Node('e')
f = Node('f')

a.left = b
a.right = c
b.left = d
b.right = e
c.right = f

# a
# / \
# b c
# / \ \
# d e f

breadthFirst(a)
# Prints ['a', 'b', 'c', 'd', 'e', 'f']

How to implement depth first traversal using recursion?

Let’s write a program to find the sum of all tree nodes (assume the values of all tree nodes are numbers). In recursion, a return value is computed from each node of the tree. These return values are classified into two cases. The base case (when you hit end of a tree path) and the recursive case (All cases other than base case).

Firstly, let’s write the base case. That is, when a root is None. This is seen when we access the left or right child of a node in the last level of the tree (ie., leaf nodes). Since the parent node itself is the last level, it’s left and right children will be None.

if root == None:
return 0

Then we write the recursive case return values. If a node has to return a value, firstly, it needs to collect the return values from it’s left and right children. With that information and it’s own ‘val’ information, it will create an appropriate return value.

leftTree = treeSum(root.left)
rightTree = treeSum(root.right)
return (root.val + leftTree + rightTree)
# Note that left and right trees return numbers, so you can directly add them.
# No need of 'val'

So the final program becomes,

def tree_sum(root):
if root == None:
return 0
leftTree = tree_sum(root.left)
rightTree = tree_sum(root.right)
return root.val + leftTree + rightTree

So, all you need to care is about the the very last level of the tree and the level before that. Because the very last level represents the base case and the last but one level represents the recursive case (which holds good for all the remaining above levels).

Recursion is as simple as that!

I have taken tree sum as an example problem here. You can apply the same approach to solve any of the traversal problems which can be solved by depth first traversals. And I mean any (such as, finding the min most value in a binary tree, finding the height of a binary tree, checking if a given node exists in a tree, etc). In fact, in non-tree traversal problems also, recursion is implemented in the exact same way. We build something called recursion tree for those problems. We’ll see about that in a future article.

Alrighty! With the above concepts, you can solve most tree related programming questions:)

There is one more kind of tree problem which is interesting. This is like a bonus information:) Suppose there is a question to print out the levels of a binary tree in a 2D array format, how do you implement that? The first row of the 2D matrix should contain the first tree level elements, the second row should contain second level tree elements and so on and so forth.

#      a ------------------ 0 level
# / \
# b c --------------- 1 level
# / \ \
# d e f ------------- 2 level

||
\/
# [
# ['a'], ---------------- 0 index
# ['b', 'c'], ----------- 1 index
# ['d', 'e', 'f'] ------- 2 index
# ]

The key lies in keeping track of the level whenever we are dealing with a node. It doesn’t matter whether you use a stack or a queue data structure. Instead of adding only the nodes to the chosen data structure, add a tuple like this (node, level). Once you have the level information of a node, you can easily assign the row number in the 2D matrix for that node.

# Input: root node of the tree, having the fields left, right and val
# Output: A 2D matrix 'levels'
def tree_levels(root):
levels = []
stack = [(root, 0)]
while len(stack) != 0:
current, level_index = stack.pop()

# The below if-else condition is where the magic happens!
if len(levels) == level_index:
levels.append([current.val])
else:
levels[level_index].append(current.val)

if current.right != None:
stack.append((current.right, level_index + 1))

if current.left != None:
stack.append((current.left, level_index + 1))

return levels

# (a,0) 0: [a]
# / \
# (b,1) (c,1) 1: [b, c]
# / \ \
# (d,2)(e,2) (f,2) 2: [d, e, f]
  • Time complexity: O(n), n = number of nodes
  • Space complexity: O(n), n = number of nodes

And that’s all you need to know about trees! Any programming question based on trees can be solved after you know the above methods!

To solidify the newly learnt concepts and apply them to problems, check out this article -

--

--