Why Representation Matters
A tree is a mathematical object: a set of nodes with a parent-child relation that has no cycles. That definition tells you what a tree is, but it doesn't tell you how to put one inside a computer. The same abstract tree can live in memory in radically different layouts — some that crush every operation into a single index calculation, some that bloat by a factor of two but let the tree change shape freely, some that store nothing but parent references and still answer ancestor queries efficiently.
Picking the right representation is one of the most consequential decisions in tree-heavy code. The CPU heap-allocator, the file system inode table, the DOM in your browser, the AST your compiler walks, the priority queue inside a Linux scheduler — each picks a different layout because each cares about different operations being cheap. By the end of this section you should be able to look at a workload and pick the layout whose worst case never bites you.
The deeper truth. The abstract tree only constrains the parent-child relation. different physical encodings can satisfy the same relation. Choosing among them is an engineering decision driven by which operations dominate your workload, not a mathematical one.
What a Tree Must Actually Encode
Forget code for a moment. To fully describe a tree of nodes you must encode three things:
- Identity. Each node has some payload — a key, a value, a record. Without payload the tree has nothing to compute on.
- Structure. Which node is whose child. With nodes there are exactly parent-child edges in any tree, no more, no less — this is the tree's defining edge count.
- Order (sometimes). Whether the children of a node are ordered (left vs. right matters in a binary tree) or unordered (just a set of children, like in a file system directory).
Every representation we're about to study encodes those three things differently. Some merge identity and structure into a single index (array layout). Some split them apart and bolt them together with pointers (linked layout). Some throw away child information and keep only parent information (parent-pointer layout) — and somehow this is enough for huge classes of problems.
Counting the bits
Representation 1: Implicit Array (Heap Layout)
The most economical layout for a complete binary tree (one with every level full except possibly the last, which fills left to right) puts the nodes in a flat array and uses arithmetic to locate parents and children. No pointers. No structure bookkeeping at all.
The arithmetic
Number nodes in level order — root first, then everything at depth 1, then depth 2, and so on, left to right. With this numbering the relations between parent and children become arithmetic identities:
- Left child of :
- Right child of :
- Parent of :
Why does this work? Level contains exactly nodes (when full), so the levels start at indices — that is, at . Doubling and adding one steps you down one level; integer-dividing and subtracting one steps you up. The arithmetic is just the level structure made explicit.
What it costs and what it buys
Per node we pay just the size of the payload — zero pointer overhead. For a tree with integer keys, that's 40 MB instead of 120 MB for a linked layout (40 MB payload + 80 MB pointers). And because the array is contiguous, sequential access is cache-friendly: walking nodes in level order is a streaming read pattern, the easiest workload for prefetchers to optimize.
The cost: every "empty slot" in the conceptual tree still occupies space. A skinny tree of height with only nodes still requires a buffer of size — an exponential blow-up. This layout is therefore reserved for trees you can guarantee are balanced — the canonical example is a binary heap.
Representation 2: Linked Nodes (Pointer Layout)
The textbook representation. Each node lives in its own heap-allocated record holding a payload and pointers to its children. Pointers are when a child is absent.
1struct Node {
2 KeyType data;
3 struct Node *left;
4 struct Node *right;
5};Per-node footprint: . On 64-bit systems that's 16 bytes of pointer overhead per node. In exchange you can represent any binary tree shape — balanced, skinny, gnarled, deeply unbalanced — with no waste and no pre-declared capacity. Insertion and deletion at any position cost pointer swings.
Why two pointers, not three?
Some implementations add a parent pointer for a third 8 bytes per node. Whether to pay for it depends on access patterns:
| Operation | Without parent ptr | With parent ptr |
|---|---|---|
| walk root → leaf | O(h) | O(h) |
| walk leaf → root | O(n) (need to re-search!) | O(h) |
| delete a node given its address | O(h) | O(1) amortized |
| per-node memory | 16 bytes overhead | 24 bytes overhead |
Operations like the BST successor function and rotation in red-black trees walk both up and down the tree, so they typically pay for the parent pointer. Pure traversals like printing or size-counting only walk down and don't need it.
Representation 3: Parent-Pointer Array
Now the surprise: we can represent a tree using only the parent relationship, with no child pointers at all. Number the nodes and store an array where each entry is the index of node 's parent (with the root's parent set to by convention).
1int parent[n]; // parent[i] = index of i's parent, or -1 for root
2KeyType data[n]; // data[i] = payload of node iThat's the entire representation. Two arrays. Total memory: .
What can you do with just parent pointers? A surprising amount:
- Find any ancestor of a node in by walking parent links until you hit .
- Test if two nodes are in the same connected component by walking each to its root and comparing — the entire foundation of union-find (Kruskal's MST, image segmentation, equivalence classes) sits on this.
- Compute lowest common ancestor with two parent walks plus a depth array.
What you cannot easily do: ask "list the children of node " in better than , because you'd need to scan the entire array looking for entries equal to . So this layout is for upward queries only. When that's your workload — union-find is the prototypical case — the parent array is unbeatable: dense, branchless inner loop, perfect cache behavior.
A real-world cameo
Representation 4: Left-Child / Right-Sibling (LCRS)
Trees in real life often aren't binary. A directory has any number of files. A document's DOM tree has paragraphs with arbitrarily many spans inside them. An XML element has unbounded children. The naive approach — storing a vector of child pointers per node — works but uses variable space per node and complicates traversal code.
Left-Child / Right-Sibling encodes any n-ary tree using exactly the same struct layout as a binary tree:
1struct Node {
2 KeyType data;
3 struct Node *firstChild; // leftmost child, or NULL
4 struct Node *nextSibling; // next child of this node's parent, or NULL
5};Two pointers, regardless of how many children a node has. To list the children of a node, you follow , then chain through until you hit . It's effectively a linked list of children dangling off each parent.
The marvel: this representation is structurally isomorphic to a binary tree. If you treat as "left" and as "right", every algorithm written for binary trees works on n-ary trees encoded this way. You only need one set of code.
Representation 5: Adjacency List (Graph-Style)
A tree is a special case of a graph (an acyclic, connected one). So every general graph representation also represents trees. The most common is the adjacency list: for each node, store a list of its neighbors.
1children = {
2 "A": ["B", "C"],
3 "B": ["D", "E"],
4 "C": ["F", "G"],
5 "D": [], "E": [], "F": [], "G": [],
6}Children-only adjacency lists work when the tree is rooted and you know which direction edges point. For unrooted trees you store edges in both directions. This format is the natural one when your tree comes out of a graph algorithm (BFS / DFS spanning trees, Prim's tree, Dijkstra's shortest-path tree) and you want to keep using graph-shaped tools on it.
Compared to LCRS, an adjacency list is more flexible (you can store children in any order, append cheaply with a Python list or C++ vector, query in ) but uses one extra pointer per child for the dynamic-array bookkeeping.
Interactive: Four Views of One Tree
Below is the same 7-node tree shown four different ways. Click any node in the diagram to lock the highlight; the four panels below light up the same logical node in their own layout. Notice how each representation finds the active node through a completely different mechanism — index arithmetic, pointer dereference, parent walk, sibling chain.
- parent index = (4-1)/2 = 1
- left child index = 2·4+1 = 9 (out of range → no left child)
- right child index = 2·4+2 = 10 (out of range → no right child)
Same tree, four physical layouts. Notice how the array view costs zero pointers, the linked view spends two pointers per node, and the LCRS view spends two pointers regardless of branching factor — that's the trick that lets it represent any n-ary tree with the same per-node cost as a binary tree.
C: Array-Based Binary Tree
Let's make the array representation concrete. The implementation below stores any binary tree of bounded height in a fixed-capacity flat buffer using only the heap-math relations.
C: Linked Binary Tree
Now the pointer-based version. Notice how much more code there is for memory management — allocation, recursive freeing, NULL-checking — compared to the array version. Pointers buy flexibility but charge for it in book-keeping.
Python: Linked Binary Tree (From Scratch)
The Python version reads almost like pseudocode, but every operation has the same algorithmic content as its C counterpart. The differences are bookkeeping: Python garbage-collects so we don't write tree_free; replaces ; gives us the memory profile of a struct.
Python: Converting Between Representations
Real systems often need to switch between layouts. You might receive a tree as a flat JSON list (heap-order array), build a linked structure to operate on it, then re-flatten for transmission. The two conversion routines below let you round-trip:
Cost of Every Representation
With nodes and tree height , the operations break down as follows. Cells marked — mean the operation is not directly supported and would require building an auxiliary data structure first.
| Operation | Array | Linked | Parent-ptr | LCRS | Adj. list |
|---|---|---|---|---|---|
| access root | O(1) | O(1) | O(1) | O(1) | O(1) |
| go to left/right child | O(1) | O(1) | — | O(1) / O(deg) | O(1) |
| go to parent | O(1) | O(h) without ptr / O(1) with | O(1) | O(h) | — |
| enumerate all children | O(1) per slot | O(1) per child | O(n) | O(deg) | O(deg) |
| insert at known position | O(1) if slot free | O(1) | O(1) | O(1) | O(1) amort. |
| delete a leaf | O(1) | O(1) with parent ref | O(1) | O(deg) to unlink | O(deg) |
| BFS / level order | O(n) | O(n) with queue | O(n) | O(n) | O(n) |
| DFS | O(n) | O(n) with stack/recursion | O(n) | O(n) | O(n) |
| bytes per node (64-bit, char data) | 1 | 16-24 | 5 | 16 | 16-24 |
| wastes slots on unbalanced trees? | yes (exponentially) | no | no | no | no |
Read the bottom row carefully. The array layout's memory consumption is worst case , while every other representation uses . For a balanced tree of nodes, so and the array is fine. For a skinny tree (worst-case BST insertion order), and the array layout is catastrophically wasteful.
Memory Model: Cache, Pointers, and Locality
Big-O hides constants. On real hardware, two operations both labeled can differ by an order of magnitude depending on cache behavior. Tree representations differ wildly here.
The array layout is a streaming workload
When you traverse an array-based heap in level order, you read consecutive memory addresses. CPUs prefetch the next cache line before you ask for it; the inner loop runs at L1 speed. A modern CPU L1 cache hit costs about cycles, an L2 hit about , an L3 hit about , a DRAM access about . Sequential reads over a 40 MB array stay in L2/L3 most of the time.
The linked layout is a pointer-chase
Each node is independently malloc'd, so neighboring nodes in the tree are not neighboring in memory. Following is a load of an address you cannot predict, then a load of memory at that address. The prefetcher cannot help; every dereference is a potential L3 miss. For a tree that doesn't fit in cache, the constant factor on every "go to child" can be the array equivalent.
Where Each Representation Lives in Real Systems
Array layout in the wild
- Binary heap — the priority queue inside every operating-system scheduler, every Dijkstra implementation, every event-driven simulator. Insert and extract-min run in with no pointers and great cache behavior.
- Segment tree / Fenwick tree — competitive programming's favorite range-query structure, always built on a flat array of size or .
- NumPy/PyTorch internal storage of complete trees — reduction kernels on GPUs treat thread blocks as a balanced binary tree implicitly indexed by thread ID.
Linked layout in the wild
- Browser DOM — every is a heap-allocated object with parent and children references. The shape changes every frame, so flexibility wins over memory.
- std::map and std::set — libc++ and libstdc++ implement these as red-black trees with linked nodes (each node carries 3 pointers + color bit + payload).
- Compiler ASTs — clang, GCC, V8, the Rust compiler all build linked tree-of-nodes structures because syntax trees have wildly variable shape.
Parent-pointer layout in the wild
- Union-find / disjoint-set in Kruskal's MST, image-segmentation watersheds, and incremental connectivity. Path compression mutates the parent array itself.
- Process tree in the kernel — is a single pointer per process; children are a separate intrusive list, but the parent direction is what matters for signal delivery.
- Git's commit graph — each commit stores its parent commit hash(es), no list of children. Walking history goes backward only, exactly as expected.
LCRS in the wild
- Classic Unix directory entries — older filesystems chain children of a directory through linked sibling pointers.
- XML / SGML / HTML parsers emitting events without pre-allocating arrays.
- Compiler IR for languages with unbounded children per AST node (e.g. ) where IS the LCRS encoding.
Adjacency list in the wild
- Spanning trees from BFS / DFS — you already had the graph in adjacency-list form, so the spanning tree comes out the same way.
- Phylogenetic trees in bioinformatics — the Newick format and most analysis pipelines pass trees as labeled edge lists.
- Social-network friend hierarchies — reporting trees, recommendation subtrees, anything coming out of a graph database.
Pitfalls and Edge Cases
Quick Checks
Quick Check
A complete binary tree has 31 nodes (heights 0..4 all full). In the heap-array layout, what is the index of the parent of node 25?
Quick Check
You are implementing union-find on 10 million elements. Which representation should you pick?
Quick Check
What is the per-node memory overhead (in bytes, on a 64-bit machine) of an LCRS encoding versus a 5-ary tree that stores its children in a fixed-size array?
Quick Check
A tree has 8 nodes and is laid out as a heap-order array. Slot 7 contains 'X'. Where is X's parent in the array?
Quick Check
You are profiling a tree traversal and find that 80% of the time is spent on cache misses during pointer dereferences. The tree shape is roughly balanced and rarely changes. What is the best mitigation?