Tree
A branching structure where each node can have children. The root sits at the top, leaves sit at the bottom with no children.
Think about your computer’s folders. A folder can have files and other folders inside it, and those folders can have even more folders inside them. That branching, “one thing leads to many things” shape is exactly what a tree looks like in programming. It is one of the most important shapes data can take, and this guide walks you through everything you need to know, from the basics all the way to a fun bit of math that ties it all together.
A tree is a way of organizing data that branches out, kind of like a real tree, except it usually gets drawn upside down with the root at the top.
You have probably used trees without even realizing it:
Before going further, let us learn the words people use when talking about trees. Here is a small tree to look at:
A / \ B C / \ \ D E FA.A is the parent of B and C.B and C are children of A.D, E, and F.A binary tree is simply a tree where every node has at most two children, usually called the left child and the right child. Binary trees come in a few recognizable shapes, and knowing their names helps a lot when reading about data structures.
A tree is called “full” when every single node has either zero children or exactly two children. Nobody is allowed to have just one child.
A / \ B C / \ / \ D E F GThis one is a valid full binary tree, since every node has 0 or 2 children.
This one is not valid:
A / BB has only one child slot used and one missing, so this breaks the “full” rule.
A neat little fact about full binary trees: if you count the number of nodes that have children (internal nodes), the total number of nodes always works out to 2 times internal_nodes + 1. Full binary trees show up a lot in things like expression trees (the kind used to represent math expressions like (2 + 3) * 4).
A tree is called “complete” when every level is totally full, except possibly the very last level, and on that last level, nodes are filled in strictly left to right with no gaps.
A / \ B C / \ / D E FThis matters a lot in practice because heaps (a structure used for priority queues) are built on exactly this shape. The nice thing about complete binary trees is that you can store them directly inside a plain array with no wasted space, and that gives really good performance because arrays are friendly to your computer’s memory cache.
Traversal just means visiting every node in the tree exactly once, in some specific order. There are three classic ways to do this, and they only differ in when you “visit” (read or print) the current node compared to its children.
A / \ B C / \ D EOrder you would visit: D, B, E, A, C
def inorder(node): if not node: return inorder(node.left) print(node.val) inorder(node.right)The reason this one is special: if you run inorder traversal on a binary search tree, you get all the values back out in perfectly sorted order. We will come back to this a bit later.
Using the same tree, the order becomes: A, B, D, E, C
def preorder(node): if not node: return print(node.val) preorder(node.left) preorder(node.right)This is handy when you want to copy a tree or save it to a file (serialize it), because visiting the root first means you always know where each subtree starts.
Order: D, E, B, C, A
def postorder(node): if not node: return postorder(node.left) postorder(node.right) print(node.val)This order is great for deleting a tree safely, since you clean up all the children before removing the parent, so you never lose your reference to a node you still need to delete.
Here is a fun puzzle: if someone gives you the traversal results, can you figure out the exact original tree? It turns out the answer depends entirely on which traversals you have.
This combination always works because of one simple fact: preorder tells you the root first, and inorder tells you everything to the left of the root and everything to the right of the root.
Let us walk through an example.
Preorder: A B D E C FInorder: D B E A C FFind the root. The first value in preorder is always the root, so the root here is A.
Split the inorder list around the root. Find A in the inorder list. Everything before it is the left subtree, everything after it is the right subtree.
Left inorder = D B ERight inorder = C FRepeat the same process on each half, treating them as their own smaller trees, until there is nothing left to split.
Here is the code that does exactly this:
def buildTree(preorder, inorder): if not preorder or not inorder: return None
root_val = preorder[0] root = TreeNode(root_val)
i = inorder.index(root_val)
root.left = buildTree(preorder[1:i+1], inorder[:i]) root.right = buildTree(preorder[i+1:], inorder[i+1:])
return rootSame idea, just flipped. This time, the root is the last value in postorder instead of the first.
def buildTree(postorder, inorder): if not postorder or not inorder: return None
root_val = postorder[-1] root = TreeNode(root_val)
i = inorder.index(root_val)
root.left = buildTree(postorder[:i], inorder[:i]) root.right = buildTree(postorder[i:-1], inorder[i+1:])
return rootThis one trips people up, so let us slow down. Preorder tells you the order “root, left, right.” Postorder tells you “left, right, root.” Notice that neither one tells you where the left subtree ends and the right subtree begins. Without that boundary, more than one tree shape can produce the exact same preorder and postorder output.
Take this small example:
Preorder: A B CPostorder: C B AAll three of these trees give you that exact same preorder and postorder:
Tree 1: Tree 2: Tree 3: A A A / \ / B B B / \ \C C CSo just having preorder and postorder leaves you guessing.
If you write the rebuilding code exactly as shown above, it actually runs in O(n squared) time, because every step searches through the inorder list with .index(), and that search itself takes time.
There is a neat trick to fix this: build a hashmap once at the very start that instantly tells you the position of any value in the inorder list. That turns every lookup into an instant O(1) operation, bringing the whole thing down to O(n).
def build(preorder, inorder): index = {v: i for i, v in enumerate(inorder)} pre_i = 0
def helper(left, right): nonlocal pre_i if left > right: return None
root_val = preorder[pre_i] pre_i += 1 root = TreeNode(root_val)
mid = index[root_val]
root.left = helper(left, mid - 1) root.right = helper(mid + 1, right) return root
return helper(0, len(inorder) - 1)This little optimization is something interviewers love to see, since it shows you understand why the slow version is slow, not just that a faster version exists.
For traversals (just visiting every node once):
h is the height of the tree, because that is how deep your recursive calls goFor building a tree from traversals:
Now that you know what a regular tree looks like, let us meet a special, very useful version of it: the Binary Search Tree, or BST for short.
A BST follows one golden rule at every single node:
Everything in the left subtree < this node's value < everything in the right subtreeLet us insert the values 50, 30, 70, 20, 40, 60, 80 one at a time and see what we get:
50 / \ 30 70 / \ / \ 20 40 60 80Notice how anything smaller than a node always lives somewhere in its left branch, and anything bigger always lives somewhere in its right branch. That single rule is what makes a BST so useful.
A BST gives you fast searching, fast inserting, and fast deleting, kind of like binary search, except it works as a living, changeable structure instead of a fixed sorted list. This is why BSTs show up everywhere: databases, file systems, compilers, and a huge chunk of coding interview questions.
| Operation | Average Case | Worst Case |
|---|---|---|
| Search | O(log n) | O(n) |
| Insert | O(log n) | O(n) |
| Delete | O(log n) | O(n) |
That worst case shows up when the tree becomes “skewed,” meaning it basically turns into a straight line instead of branching out evenly:
10 \ 20 \ 30 \ 40This looks and behaves exactly like a linked list, which means every operation slows down to O(n). Balanced BSTs avoid this problem, but that is a topic for another day.
Every node just needs to hold a value and point to its left and right children:
class Node: def __init__(self, data): self.data = data self.left = None self.right = NoneThe logic is simple: if the tree is empty, the new value becomes the root. Otherwise, compare the new value against the current node. Go left if it is smaller, go right if it is bigger, and keep going until you find an empty spot.
function insert(root, value): if root is NULL: return new Node(value)
if value < root.data: root.left = insert(root.left, value) else if value > root.data: root.right = insert(root.right, value)
return rootHere is what inserting 25 looks like, step by step:
Before: After inserting 25: 30 30 / / 20 20 \ 25And the actual Python code:
def insert(root, data): if root is None: return Node(data)
if data < root.data: root.left = insert(root.left, data) elif data > root.data: root.right = insert(root.right, data)
return rootSearching follows the exact same comparison logic as inserting. Compare, then decide which direction to go.
function search(root, value): if root is NULL: return False
if root.data == value: return True
if value < root.data: return search(root.left, value) else: return search(root.right, value)Searching for 60 in our earlier tree would look like this:
50 \ 70 / 60 <- found itdef search(root, data): if root is None: return False
if root.data == data: return True elif data < root.data: return search(root.left, data) else: return search(root.right, data)Remember inorder traversal from earlier? Run it on a BST, and you automatically get every value back in perfectly sorted order, with zero extra sorting work needed.
Inorder result: 20, 30, 40, 50, 60, 70, 80def inorder(root): if root: inorder(root.left) print(root.data, end=" ") inorder(root.right)Deleting from a BST has three different situations to handle, depending on how many children the node you are deleting has.
Case 1: The node has no children (it is a leaf)
Just remove it. Nothing else to do.
50 \ 70 <- delete this, it has no childrenCase 2: The node has exactly one child
Just connect the node’s parent directly to that one child, skipping over the deleted node.
50 \ 70 \ 80Case 3: The node has two children
This is the tricky one. Here is the trick that makes it work:
Find the inorder successor - this just means the smallest value in the node’s right subtree. You find it by going as far left as possible inside that right subtree.
Copy that successor’s value into the node you wanted to delete.
Now delete the successor itself from its original spot. Since the successor is guaranteed to have at most one child, this step always falls back into Case 1 or Case 2.
50 / \ 30 70 / \ 60 80Finding the minimum value is straightforward, just keep walking left:
70 /60 <- this is the minimum, walk left until you hit Nonedef find_min(root): while root.left: root = root.left return root.data
def delete(root, data): if root is None: return root
if data < root.data: root.left = delete(root.left, data) elif data > root.data: root.right = delete(root.right, data) else: # zero or one child if root.left is None: return root.right elif root.right is None: return root.left
# two children successor = find_min(root.right) root.data = successor root.right = delete(root.right, successor)
return rootThink of a BST like a paper dictionary. Words that come earlier alphabetically live toward the front (left), words that come later live toward the back (right), and that ordering is exactly what lets you flip straight to roughly the right page instead of reading from page one every time.
| Structure | Search Speed |
|---|---|
| Array | O(n) |
| BST (average) | O(log n) |
| HashMap | O(1) |
| Linked List | O(n) |
A BST trades away the blazing O(1) speed of a hashmap, but in exchange, it keeps everything in sorted order, which a hashmap cannot do. That trade-off is exactly why both structures exist side by side.
If you are prepping for interviews, these are the classic problems worth trying:
Here is an interesting question that connects trees to a surprising bit of math: if you have n distinct values, how many different BST shapes can you build using exactly those values?
It turns out there is a clean, famous formula for this, built around something called Catalan numbers.
n : 0 1 2 3 4C : 1 1 2 5 14So with 2 values you get 2 possible BSTs, with 3 values you get 5 possible BSTs, and with 4 values you get 14 possible BSTs.
Say your values are {1, 2}. There are exactly 2 valid BST shapes:
1 2 \ / 2 1That matches C(2) = 2.
With values {1, 2, 3}, there are exactly 5 valid shapes:
1 1 2 3 3 \ \ / \ / / 2 3 1 3 2 1 \ / \ \ 3 2 1 2That matches C(3) = 5.
Here is the core insight that explains where this formula comes from. For any group of values, picking any one value as the root automatically decides everything else:
So if you pick the i-th smallest value as your root out of n total values:
Left subtree gets (i - 1) valuesRight subtree gets (n - i) valuesAnd here is the neat part: the left subtree and right subtree are built completely independently of each other. Whatever shape the left subtree takes does not affect what shapes are possible for the right subtree. Whenever two choices are independent like this, you multiply their possibilities together.
Number of BSTs with root i = (BST shapes possible on the left) times (BST shapes possible on the right)Since the root could be any of the n values, you need to add up the result for every possible choice of root:
C(n) = sum of [ C(i - 1) * C(n - i) ] for i = 1 up to nAnd we need one starting point to make the whole thing work:
C(0) = 1This formula translates almost word for word into code:
def catalan(n): if n == 0 or n == 1: return 1
result = 0 for i in range(1, n + 1): result += catalan(i - 1) * catalan(n - i)
return result
print(catalan(3)) # Output: 5Say n = 4, with values 1, 2, 3, 4, and we choose 2 as the root.
Left subtree = {1} -> 1 valueRight subtree = {3, 4} -> 2 valuesNumber of BSTs possible with 2 as root:
C(1) * C(2) = 1 * 2 = 2You would repeat this same calculation for every possible root from 1 to 4, then add all those results together to get the final C(4) = 14.
The recursive version above is great for understanding why the formula works, but it is painfully slow for large n because it recalculates the same smaller values over and over again. Luckily, there is also a direct mathematical shortcut:
C(n) = (2n)! / ((n + 1)! * n!)import math
def catalan_math(n): return math.factorial(2 * n) // (math.factorial(n + 1) * math.factorial(n))
print(catalan_math(4)) # Output: 14Counting BST shapes is far from the only place this sequence appears. The exact same numbers also count:
A very common wrong guess is assuming Catalan numbers follow the same pattern as Fibonacci:
C(n) = C(n - 1) + C(n - 2) This is WRONGThe actual, correct relationship is:
C(n) = sum of [ C(i - 1) * C(n - i) ] for i = 1 to nOther things people often get wrong:
C(0) = 1 is the base case that makes everything else workTree
A branching structure where each node can have children. The root sits at the top, leaves sit at the bottom with no children.
Traversal
A way of visiting every node exactly once. Inorder, preorder, and postorder differ only in when the current node gets visited.
Rebuilding a Tree
Preorder + inorder, or postorder + inorder, always rebuild a unique tree. Preorder + postorder alone is ambiguous unless the tree is full.
Binary Search Tree
A binary tree where left children are always smaller and right children are always bigger than their parent. Gives fast search, insert, and delete.
BST Delete
Three cases: no children (just remove), one child (skip over it), two children (swap with the inorder successor, then delete the successor).
Catalan Numbers
Counts how many different BST shapes are possible for n distinct values. Built by picking each possible root and multiplying left and right possibilities.