Trees
A tree is a hierarchical data structure made up of nodes connected by edges. Unlike arrays or linked lists — which are linear — a tree branches out from a single starting point, making it the natural choice for representing parent-child relationships, file systems, organisation charts, and the structure of code itself (the AST your compiler builds is a tree).
Why trees?
Linear structures force you to scan from one end to the other. Trees let you divide the search space with every step. A balanced tree of 1 000 000 nodes can be searched in about 20 comparisons — the same reason binary search is so powerful, but generalised to arbitrary hierarchies.
Hierarchical data — file systems, DOM, org charts, categories
Fast search / insert / delete — O(log n) on balanced trees
Ordering — BSTs keep data sorted for free
Priority queues — heaps are specialised trees
Prefix lookups — tries power autocomplete and spell-check
Compiler internals — ASTs represent parsed source code
Terminology
Anatomy of a tree
A ← root (no parent)
/ \
B C ← A is parent; B and C are children of A
/ \ \
D E F ← D, E, F are leaf nodes (no children)
Node A: depth 0, level 1
Node B: depth 1, level 2
Node D: depth 2, level 3 ← leaf
Height of tree = 2 (longest path root → leaf, counted in edges)
Height of node B = 1 (longest path from B to a leaf below it)Term | Definition |
|---|---|
Root | The single node with no parent; the entry point of the tree. |
Node | Any element in the tree. |
Edge | A link connecting a parent to a child. |
Parent | A node that has one or more children. |
Child | A node directly connected below a parent. |
Siblings | Nodes that share the same parent. |
Leaf | A node with no children (also called an external node). |
Internal node | Any non-leaf node. |
Depth of a node | Number of edges from the root to that node. |
Height of a node | Number of edges on the longest path from that node down to a leaf. |
Height of a tree | Height of the root node. |
Level | depth + 1 (root is at level 1). |
Subtree | A node together with all of its descendants. |
Degree | Number of children a node has. |
Path | A sequence of nodes connected by edges. |
Ancestor | Any node on the path from a node up to the root. |
Descendant | Any node reachable going downward from a given node. |
Trees as a recursive structure
The most important insight about trees: a tree is a root node whose children are themselves trees (subtrees). This recursive definition drives almost every algorithm — traversal, search, height calculation, and insertion all reduce to "do something at this node, then recurse into each child."
Generic tree node
class TreeNode {
constructor(value) {
this.value = value
this.children = [] // N-ary: any number of children
}
}
// Recursive height — height of a tree rooted at node
function height(node) {
if (!node) return -1 // empty tree: -1
if (node.children.length === 0) return 0 // leaf: height 0
return 1 + Math.max(...node.children.map(height))
}
// Recursive size — total number of nodes
function size(node) {
if (!node) return 0
return 1 + node.children.reduce((sum, child) => sum + size(child), 0)
}Types of trees
Binary Tree
Every node has at most 2 children, conventionally called left and right. The most common tree in interviews — see the Binary Trees page for a deep dive.
1
/ \
2 3
/ \
4 5Binary Search Tree (BST)
A binary tree with the BST property: for every node N, all values in N's left subtree are less than N's value, and all values in the right subtree are greater. This makes search, insert, and delete O(h) where h is the height — O(log n) when balanced.
BST property
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
left(3) < 8 < right(10) ✓
In-order traversal → 1 3 4 6 7 8 10 13 14 (sorted!)BST insert and search
class BSTNode {
constructor(val) { this.val = val; this.left = null; this.right = null }
}
function insert(root, val) {
if (!root) return new BSTNode(val)
if (val < root.val) root.left = insert(root.left, val)
else root.right = insert(root.right, val)
return root
}
function search(root, val) {
if (!root || root.val === val) return root
return val < root.val ? search(root.left, val) : search(root.right, val)
}
// Build BST from array
function buildBST(values) {
return values.reduce((root, v) => insert(root, v), null)
}
const bst = buildBST([8, 3, 10, 1, 6, 14, 4, 7, 13])
console.log(search(bst, 6)?.val) // 6
console.log(search(bst, 99)) // nullAVL Tree (Self-Balancing BST)
An AVL tree maintains the balance factor of every node: the difference in height between left and right subtrees must be at most 1. After each insert or delete, the tree performs rotations to restore balance, guaranteeing O(log n) for all operations.
Balance factor = height(left) - height(right)
Unbalanced (BF = -2): After left rotation:
3 5
\ / \
5 → 3 7
\
7
Balance factor must be in {-1, 0, 1} for every node.Heap
A complete binary tree satisfying the heap property. In a max-heap, every parent is greater than or equal to its children; in a min-heap, every parent is smaller. Heaps are stored efficiently in arrays and power priority queues. The root is always the max (or min) element — O(1) access.
Max-heap
90
/ \
75 85
/ \ / \
55 60 70 80
Array representation: [90, 75, 85, 55, 60, 70, 80]
Parent of index i → Math.floor((i - 1) / 2)
Left child of i → 2*i + 1
Right child of i → 2*i + 2Trie (Prefix Tree)
A trie stores strings character by character. Each node represents one character; paths from root to a marked node spell out words. Lookup and insert are O(L) where L is the string length — independent of how many words are stored.
Trie storing: "cat", "car", "card", "care", "bat"
root
/ \
c b
| |
a a
/ \ |
t r t*
* / \
d* e*
* = end of word
"car" shares "ca" with "cat" — prefix compression saves space.Trie implementation
class TrieNode {
constructor() {
this.children = {} // char → TrieNode
this.isEnd = false
}
}
class Trie {
constructor() { this.root = new TrieNode() }
insert(word) {
let node = this.root
for (const ch of word) {
if (!node.children[ch]) node.children[ch] = new TrieNode()
node = node.children[ch]
}
node.isEnd = true
}
search(word) {
let node = this.root
for (const ch of word) {
if (!node.children[ch]) return false
node = node.children[ch]
}
return node.isEnd
}
startsWith(prefix) {
let node = this.root
for (const ch of prefix) {
if (!node.children[ch]) return false
node = node.children[ch]
}
return true // prefix exists (word may or may not be complete)
}
}
const trie = new Trie()
;['cat', 'car', 'card', 'care', 'bat'].forEach(w => trie.insert(w))
console.log(trie.search('care')) // true
console.log(trie.search('ca')) // false (not a complete word)
console.log(trie.startsWith('ca')) // trueN-ary Tree
A generalisation of binary trees where each node may have any number of children. File systems (a directory can have many subdirectories), organisation hierarchies, and HTML/XML DOMs are all N-ary trees.
N-ary tree (file system)
/
├── home/
│ ├── user/
│ │ ├── docs/
│ │ └── pics/
│ └── guest/
└── etc/
└── nginx/Tree traversal overview
Traversal is visiting every node exactly once. The order in which you visit determines what you see. Binary trees support four canonical traversals; general trees add level-order (BFS).
Traversal | Order | Common uses |
|---|---|---|
Pre-order (DFS) | Root → Left → Right | Copy a tree, serialise, prefix expressions |
In-order (DFS) | Left → Root → Right | Get sorted values from a BST |
Post-order (DFS) | Left → Right → Root | Delete a tree, evaluate expressions, size |
Level-order (BFS) | Level by level, left to right | Shortest path, level-wise processing |
All four traversals
function preOrder(node, result = []) {
if (!node) return result
result.push(node.val) // Root
preOrder(node.left, result) // Left
preOrder(node.right, result) // Right
return result
}
function inOrder(node, result = []) {
if (!node) return result
inOrder(node.left, result) // Left
result.push(node.val) // Root
inOrder(node.right, result) // Right
return result
}
function postOrder(node, result = []) {
if (!node) return result
postOrder(node.left, result) // Left
postOrder(node.right, result) // Right
result.push(node.val) // Root
return result
}
function levelOrder(root) {
if (!root) return []
const result = [], queue = [root]
while (queue.length) {
const level = []
const len = queue.length // snapshot — important!
for (let i = 0; i < len; 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
}Time and space complexity summary
Tree type | Search | Insert | Delete | Space |
|---|---|---|---|---|
Binary Search Tree (balanced) | O(log n) | O(log n) | O(log n) | O(n) |
Binary Search Tree (worst) | O(n) | O(n) | O(n) | O(n) |
AVL Tree | O(log n) | O(log n) | O(log n) | O(n) |
Min/Max Heap | O(n) | O(log n) | O(log n) | O(n) |
Trie | O(L) | O(L) | O(L) | O(ALPHABET × L × n) |
N-ary Tree | O(n) | O(1) | O(n) | O(n) |
Choosing the right tree
Need sorted order + fast search/insert? Use a BST or AVL tree.
Need the minimum or maximum instantly? Use a heap (priority queue).
Need prefix-based string search / autocomplete? Use a trie.
Modelling a hierarchy with arbitrary branching? Use an N-ary tree.
Need to traverse a graph layer by layer? BFS on a tree is level-order.
Common interview patterns
DFS (recursion or explicit stack) — height, diameter, path sum, lowest common ancestor
BFS (queue) — level order, minimum depth, right-side view
In-order of BST — validate BST, kth smallest, recover BST
Post-order — bottom-up answers (diameter, balanced check, subtree sums)
Serialise / deserialise — convert tree to string and back (pre-order + sentinel)