Toolmingo
Guides13 min read

Binary Tree Data Structure: Complete Guide with Code Examples

Master binary trees — types, traversals, BST operations, AVL trees, heaps, time complexity, and the most common interview problems with working code in Python, JavaScript, Java, and Go.

A binary tree is a hierarchical data structure where each node has at most two children — called the left child and the right child. Binary trees underpin database indexes, file systems, compilers, and nearly every efficient search algorithm. They appear in roughly 25–30% of coding interview questions at major tech companies.

Quick reference

Operation Binary Search Tree (avg) BST (worst) Balanced BST (e.g. AVL)
Search O(log n) O(n) O(log n)
Insert O(log n) O(n) O(log n)
Delete O(log n) O(n) O(log n)
Min / Max O(log n) O(n) O(log n)
Traversal O(n) O(n) O(n)
Space O(n) O(n) O(n)

Worst case for a plain BST occurs when the tree degenerates into a linked list (all insertions in sorted order).


Tree terminology

Term Meaning
Root The topmost node (no parent)
Leaf A node with no children
Height Longest path from root to a leaf (edges)
Depth Distance from root to a given node (edges)
Level Set of nodes at the same depth
Subtree A node and all its descendants
Parent / Child Direct connection between two nodes
Sibling Nodes sharing the same parent
Ancestor / Descendant Any node on the path to root / any reachable child

Types of binary trees

Full binary tree

Every node has 0 or 2 children — never just one.

Complete binary tree

All levels are fully filled except possibly the last, which is filled from left to right.
→ Used in binary heaps.

Perfect binary tree

All internal nodes have exactly 2 children and all leaves are at the same level.
A perfect tree with height h has 2^(h+1) − 1 nodes.

Degenerate (skewed) tree

Every node has only one child — essentially a linked list.
This is the worst case for a plain BST.

Balanced binary tree

Height is kept at O(log n) by a balancing rule.
Examples: AVL trees, Red-Black trees (used in Java TreeMap, C++ std::map).


Node structure

# Python
class TreeNode:
    def __init__(self, val=0):
        self.val = val
        self.left = None
        self.right = None
// JavaScript
class TreeNode {
  constructor(val = 0) {
    this.val = val;
    this.left = null;
    this.right = null;
  }
}
// Java
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int val) { this.val = val; }
}
// Go
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

Binary Search Tree (BST)

A BST adds one invariant to the binary tree structure:

For every node N: all values in the left subtree < N.val < all values in the right subtree.

This invariant makes search, insert, and delete all O(log n) on average.

Insert

def insert(root, val):
    if root is None:
        return TreeNode(val)
    if val < root.val:
        root.left = insert(root.left, val)
    elif val > root.val:
        root.right = insert(root.right, val)
    return root
function insert(root, val) {
  if (!root) return new TreeNode(val);
  if (val < root.val) root.left = insert(root.left, val);
  else if (val > root.val) root.right = insert(root.right, val);
  return root;
}

Search

def search(root, val):
    if root is None or root.val == val:
        return root
    if val < root.val:
        return search(root.left, val)
    return search(root.right, val)

Delete

Deletion has three cases:

  1. Leaf node — simply remove it.
  2. One child — replace the node with its child.
  3. Two children — replace the node's value with its in-order successor (smallest value in the right subtree), then delete the successor.
def delete(root, val):
    if root is None:
        return None
    if val < root.val:
        root.left = delete(root.left, val)
    elif val > root.val:
        root.right = delete(root.right, val)
    else:
        # Case 1 & 2
        if root.left is None:
            return root.right
        if root.right is None:
            return root.left
        # Case 3: find in-order successor
        successor = root.right
        while successor.left:
            successor = successor.left
        root.val = successor.val
        root.right = delete(root.right, successor.val)
    return root

Tree traversals

Traversal order determines the sequence in which nodes are visited. There are four classic traversals.

Inorder (Left → Root → Right)

Visits nodes in ascending order for a BST.

def inorder(root, result=[]):
    if root:
        inorder(root.left, result)
        result.append(root.val)
        inorder(root.right, result)
    return result
function inorder(root, result = []) {
  if (root) {
    inorder(root.left, result);
    result.push(root.val);
    inorder(root.right, result);
  }
  return result;
}

Preorder (Root → Left → Right)

Useful for copying or serializing a tree.

def preorder(root, result=[]):
    if root:
        result.append(root.val)
        preorder(root.left, result)
        preorder(root.right, result)
    return result

Postorder (Left → Right → Root)

Useful for deleting a tree or evaluating expression trees (children before parent).

def postorder(root, result=[]):
    if root:
        postorder(root.left, result)
        postorder(root.right, result)
        result.append(root.val)
    return result

Level-order / BFS (Breadth-First)

Visits nodes level by level using a queue. Essential for finding shortest paths and many interview problems.

from collections import deque

def level_order(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)
    return result
function levelOrder(root) {
  if (!root) return [];
  const result = [], queue = [root];
  while (queue.length) {
    const levelSize = queue.length;
    const level = [];
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        result.add(level);
    }
    return result;
}
func levelOrder(root *TreeNode) [][]int {
    if root == nil { return nil }
    result := [][]int{}
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        size := len(queue)
        level := []int{}
        for i := 0; i < size; i++ {
            node := queue[0]
            queue = queue[1:]
            level = append(level, node.Val)
            if node.Left != nil { queue = append(queue, node.Left) }
            if node.Right != nil { queue = append(queue, node.Right) }
        }
        result = append(result, level)
    }
    return result
}

Traversal comparison

Traversal Order Typical use case
Inorder L → Root → R Get sorted values from BST
Preorder Root → L → R Copy / serialize tree
Postorder L → R → Root Delete tree, evaluate expression tree
Level-order Top to bottom, left to right Shortest path, print by level

Iterative traversals (stack-based)

Recursion uses the call stack implicitly. You can replicate this with an explicit stack.

Iterative inorder

def inorder_iterative(root):
    result, stack = [], []
    current = root
    while current or stack:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        result.append(current.val)
        current = current.right
    return result

Height and depth

def height(root):
    if root is None:
        return -1  # use 0 if counting nodes instead of edges
    return 1 + max(height(root.left), height(root.right))

def depth(root, target, d=0):
    if root is None:
        return -1
    if root.val == target:
        return d
    left = depth(root.left, target, d + 1)
    return left if left != -1 else depth(root.right, target, d + 1)

AVL tree (balanced BST)

An AVL tree is a self-balancing BST where for every node, the height difference between left and right subtrees (the balance factor) is at most 1.

Balance factor = height(left subtree) − height(right subtree)
Valid values: −1, 0, 1

When an insertion or deletion violates this, the tree performs rotations to restore balance.

Imbalance case Fix
Left-Left Single right rotation
Right-Right Single left rotation
Left-Right Left rotation on child, then right rotation
Right-Left Right rotation on child, then left rotation

AVL trees guarantee O(log n) for all operations. They are used in databases and file systems where lookups dominate.


Binary heap

A binary heap is a complete binary tree that satisfies the heap property:

  • Min-heap: every parent ≤ its children (root is the minimum)
  • Max-heap: every parent ≥ its children (root is the maximum)

Heaps are commonly stored in an array (no pointers needed):

For node at index i:
  Left child  → 2i + 1
  Right child → 2i + 2
  Parent      → (i − 1) // 2
Operation Time
Insert O(log n)
Delete min/max O(log n)
Peek min/max O(1)
Build heap O(n)

Used in: priority queues, Dijkstra's algorithm, heap sort.


Common interview problems

1. Maximum depth of a binary tree

def max_depth(root):
    if root is None:
        return 0
    return 1 + max(max_depth(root.left), max_depth(root.right))

Time: O(n) | Space: O(h) where h is the height.


2. Check if a binary tree is balanced

A tree is balanced if for every node, |height(left) − height(right)| ≤ 1.

def is_balanced(root):
    def check(node):
        if node is None:
            return 0
        left = check(node.left)
        right = check(node.right)
        if left == -1 or right == -1 or abs(left - right) > 1:
            return -1
        return 1 + max(left, right)
    return check(root) != -1

3. Lowest Common Ancestor (LCA)

def lca(root, p, q):
    if root is None or root == p or root == q:
        return root
    left = lca(root.left, p, q)
    right = lca(root.right, p, q)
    if left and right:
        return root  # p and q are in different subtrees
    return left or right

LCA in a BST (exploits BST property):

def lca_bst(root, p, q):
    while root:
        if p.val < root.val and q.val < root.val:
            root = root.left
        elif p.val > root.val and q.val > root.val:
            root = root.right
        else:
            return root

4. Diameter of a binary tree

The diameter is the longest path between any two nodes (may not pass through the root).

def diameter(root):
    max_d = [0]
    def depth(node):
        if node is None:
            return 0
        left, right = depth(node.left), depth(node.right)
        max_d[0] = max(max_d[0], left + right)
        return 1 + max(left, right)
    depth(root)
    return max_d[0]

5. Validate a Binary Search Tree

def is_valid_bst(root, low=float('-inf'), high=float('inf')):
    if root is None:
        return True
    if root.val <= low or root.val >= high:
        return False
    return (is_valid_bst(root.left, low, root.val) and
            is_valid_bst(root.right, root.val, high))

6. Serialize and deserialize a binary tree

def serialize(root):
    """Preorder serialization."""
    if root is None:
        return 'N'
    return f"{root.val},{serialize(root.left)},{serialize(root.right)}"

def deserialize(data):
    vals = iter(data.split(','))
    def build():
        val = next(vals)
        if val == 'N':
            return None
        node = TreeNode(int(val))
        node.left = build()
        node.right = build()
        return node
    return build()

7. Right side view of a binary tree

Return the value of the rightmost node at each level.

from collections import deque

def right_side_view(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        for i in range(len(queue)):
            node = queue.popleft()
            if i == len(queue):   # last node in level
                result.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
    return result

8. Kth smallest element in a BST

Inorder traversal yields sorted order — take the kth element.

def kth_smallest(root, k):
    stack, current = [], root
    while True:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        k -= 1
        if k == 0:
            return current.val
        current = current.right

Interview problems cheat sheet

Problem Key insight Traversal Time
Max depth Recursion: 1 + max(left, right) DFS O(n)
Is balanced Check height returns -1 on imbalance DFS O(n)
Is valid BST Pass min/max bounds through recursion DFS O(n)
LCA Split point where p and q diverge DFS O(n)
LCA in BST Use BST property, go left/right DFS O(h)
Diameter Max of left_depth + right_depth at each node DFS O(n)
Serialize/deserialize Preorder + null markers DFS O(n)
Right side view Last node at each BFS level BFS O(n)
Level order Queue + level-size trick BFS O(n)
Kth smallest in BST Inorder traversal, count DFS O(h+k)
Path sum Subtract target as you recurse DFS O(n)
Invert binary tree Swap left and right recursively DFS O(n)
Symmetric tree Mirror check: left.left vs right.right DFS O(n)
Count nodes 2^h trick for complete trees DFS O(log²n)

Binary tree vs other data structures

Structure Search Insert Delete Ordered Notes
Array O(n) O(n) O(n) Optional Cache-friendly
Hash table O(1) avg O(1) avg O(1) avg No No ordering
Linked list O(n) O(1) O(1) No Sequential access
BST (avg) O(log n) O(log n) O(log n) Yes Degrades without balance
AVL / RBT O(log n) O(log n) O(log n) Yes Always balanced
B-tree O(log n) O(log n) O(log n) Yes Used in databases (disk)

When to use a binary tree

Use a BST / balanced BST when:

  • You need data sorted and need fast lookups, insertions, and deletions.
  • You need range queries ("find all values between 10 and 50").
  • You need in-order iteration over sorted elements.

Use a heap when:

  • You only care about the minimum or maximum element.
  • You need a priority queue.

Use a hash table instead when:

  • You need O(1) average lookups and don't care about ordering.

Common mistakes

Mistake Why it matters Fix
Forgetting to return updated root in recursive insert/delete Mutates nothing — tree unchanged Always return root at the end
Using == to compare nodes instead of .val Two different nodes can have the same value Compare by reference for LCA, by value for BST operations
Assuming BST without checking invariant Leads to wrong algorithm Validate with is_valid_bst or accept problem guarantee
Not handling None root NullPointerException / AttributeError Base case: if root is None: return ...
DFS on a very deep tree causing stack overflow Recursion depth ~= tree height Use iterative + explicit stack for skewed trees
Mutating the default mutable argument result=[] Shared state across calls in Python Use result=None and init inside, or pass explicitly

FAQ

What is the difference between a binary tree and a binary search tree?
A binary tree is any tree where nodes have at most two children. A BST adds the ordering invariant: left < node < right. All BSTs are binary trees, but not all binary trees are BSTs.

What is the time complexity of tree traversal?
All four traversals (inorder, preorder, postorder, level-order) visit every node exactly once: O(n) time and O(h) space where h is the height (O(n) worst case for a skewed tree, O(log n) for a balanced tree).

When should I use DFS vs BFS on a tree?
Use DFS (recursive or iterative with stack) for problems involving depth, paths, subtrees, or when memory is a concern on wide trees. Use BFS (queue) for level-by-level processing, finding shortest paths, or when the answer is likely near the root.

What is the difference between height and depth?
Height is measured from a node downward to its deepest leaf. Depth is measured from the root down to a node. The height of the tree = height of the root = depth of the deepest leaf.

Is a balanced binary tree the same as an AVL tree?
No. An AVL tree is a specific implementation of a balanced BST. A "balanced binary tree" is a general concept. Red-Black trees and B-trees are also balanced, but with different balancing rules than AVL.

Why is O(n) insert possible in a BST?
If you insert elements in sorted order (1, 2, 3, 4, …), every new node goes to the right, creating a chain — a degenerate BST identical to a linked list. This makes all operations O(n). Use an AVL or Red-Black tree to prevent this.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools