Tree terminology, binary trees and BSTs, in/pre/post/level-order traversals, balanced trees (red-black, AVL), B-trees in database indexes, and real-world trees in .NET.
A tree is a hierarchy of nodes with one root, where each node has children and no cycles. A binary tree limits each node to two children (left, right). A binary search tree (BST) adds an ordering rule: left subtree < node < right subtree, so search, insert and delete take O(height).
If the tree is balanced, height is O(log n); if you insert sorted data into a plain BST it degenerates into a linked list with O(n) height. Self-balancing trees (red-black, AVL) keep O(log n); .NET's SortedSet and SortedDictionary are red-black trees.
Traversals: in-order (sorted output for a BST), pre-order (copy/serialise), post-order (delete/evaluate), level-order (BFS).
Trees are everywhere: database B+ tree indexes, file systems, JSON/XML/DOM, Roslyn syntax trees, expression trees, org charts.
[50] β root (depth 0)
/ \
[30] [70] β internal nodes
/ \ / \
[20] [40] [60] [80] β leaves (depth 2)
height = 2, n = 7, BST property holds
| Term | Meaning |
|---|---|
| Root / leaf | Top node / node with no children |
| Depth / height | Edges from root / longest path to a leaf |
| Full binary tree | Every node has 0 or 2 children |
| Complete binary tree | All levels full except possibly the last, filled left to right (heaps use this) |
| Balanced | Heights of subtrees differ by a bounded amount, so height is O(log n) |
| Operation | Balanced | Degenerate (sorted inserts) |
|---|---|---|
| Search / insert / delete | O(log n) | O(n) |
| Min / max | O(log n) | O(n) |
| In-order traversal | O(n) | O(n) |
| Order | Visit sequence for the tree above | Use |
|---|---|---|
| In-order (L, N, R) | 20 30 40 50 60 70 80 | Sorted output |
| Pre-order (N, L, R) | 50 30 20 40 70 60 80 | Copy, serialise |
| Post-order (L, R, N) | 20 40 30 60 80 70 50 | Delete, evaluate expressions |
| Level-order (BFS) | 50 30 70 20 40 60 80 | Shortest depth, printing by level |
SQL Server, PostgreSQL and most databases store indexes as B+ trees: each node is a page (8 KB in SQL Server) holding hundreds of keys, so the tree is very wide and shallow. A table with 100 million rows typically has an index only 3β4 levels deep, so a seek reads 3β4 pages.
A category tree for an e-commerce site (12,000 categories, up to 9 levels) was loaded by calling the database once per node to fetch its children: 12,000 queries per cache rebuild. We loaded all rows in one query, built the tree in memory with a Dictionary<int, Node> keyed by ID (O(n)), and computed breadcrumbs with a parent walk. The rebuild went from 45 seconds to 200 ms.
Checks tree fundamentals and the ability to connect them to real systems.
Green flags
SortedSet<T>, expression and syntax trees).Red flags
Insert 10, 20, 30, 40, 50 into a plain BST:
[10]
\
[20]
\
[30] height = n - 1 β every operation O(n)
\
[40]
\
[50]
Self-balancing trees fix this with rotations, which re-hang nodes in O(1) while keeping the BST order:
Left rotation at 10 (right-heavy):
[10] [20]
\ / \
[20] βββΊ [10] [30]
\
[30]
| Tree | Balance rule | How it rebalances | Notes |
|---|---|---|---|
| AVL | Subtree heights differ by β€ 1 at every node | Tracks a height or balance factor; single or double rotations after insert/delete | Shallower, so faster lookups; more rotations on writes |
| Red-black | Every node red or black; no red node has a red child; every root-to-leaf path has the same number of black nodes | Recolouring plus at most 2 rotations per insert, 3 per delete | Height β€ 2 logβ(n + 1); used by .NET SortedSet<T> and SortedDictionary, Java TreeMap, and the Linux scheduler's run queue |
| B-tree / B+ tree | Many keys per node, all leaves at the same depth | Split full nodes upwards, merge or borrow on delete | Disk- and page-friendly: database indexes, file systems |
[ 100 | 500 | 900 ] root page
/ | | \
[10|40|70] [200|300|400] [600|700|800] [950|...] intermediate pages
... ... ... ...
leaf pages hold keys + row locators, linked leftβright for range scans
Leaves are linked, so WHERE OrderDate BETWEEN ... seeks once then scans sideways. This is why index key order and "sargable" predicates matter.
| Traversal | Time | Extra memory | Practical use |
|---|---|---|---|
| In-order | O(n) | O(height) stack | Sorted output from a BST, range reports |
| Pre-order | O(n) | O(height) stack | Serialising a tree, copying, printing an outline (folders, JSON) |
| Post-order | O(n) | O(height) stack | Deleting children before parents, evaluating expression trees, computing folder sizes |
| Level-order | O(n) | O(width) queue | Breadcrumb levels, shortest depth, rendering by level |
SyntaxTree, SyntaxNode) for analysers and source generators.Expression<Func<T, bool>>) that EF Core walks and translates to SQL.JsonNode, JsonDocument, XDocument), and the HTML DOM.IConfiguration sections); DI registrations form a dependency graph (a DAG rather than a tree).hierarchyid and adjacency-list tables (ParentId) for org charts and categories, loaded with recursive CTEs.Recursive traversals are elegant, but call depth equals tree height. For degenerate or user-supplied trees, use an explicit stack (DFS) or queue (BFS) to avoid StackOverflowException, which cannot be caught.
SortedSet<int> timings.SET STATISTICS IO ON and query by an indexed column; note logical reads (about the index depth).public sealed class Node(int value)
{
public int Value { get; } = value;
public Node? Left { get; set; }
public Node? Right { get; set; }
}
// BST insert and search: O(height)
public static Node Insert(Node? root, int value)
{
if (root is null) return new Node(value);
if (value < root.Value) root.Left = Insert(root.Left, value);
else if (value > root.Value) root.Right = Insert(root.Right, value);
return root;
}
public static bool Contains(Node? node, int value)
{
while (node is not null)
{
if (value == node.Value) return true;
node = value < node.Value ? node.Left : node.Right;
}
return false;
}
// Validate a BST with bounds (checking only parent/child is a classic bug)
public static bool IsValidBst(Node? node, long min = long.MinValue, long max = long.MaxValue) =>
node is null ||
(node.Value > min && node.Value < max &&
IsValidBst(node.Left, min, node.Value) &&
IsValidBst(node.Right, node.Value, max));
// In-order traversal without recursion
public static IEnumerable<int> InOrder(Node? root)
{
var stack = new Stack<Node>();
var current = root;
while (current is not null || stack.Count > 0)
{
while (current is not null) { stack.Push(current); current = current.Left; }
current = stack.Pop();
yield return current.Value;
current = current.Right;
}
}
// Build a category tree from flat rows in O(n)
public sealed record CategoryRow(int Id, int? ParentId, string Name);
public sealed class Category(int id, string name)
{
public int Id { get; } = id;
public string Name { get; } = name;
public List<Category> Children { get; } = [];
}
public static List<Category> BuildTree(IReadOnlyList<CategoryRow> rows)
{
var byId = rows.ToDictionary(r => r.Id, r => new Category(r.Id, r.Name));
var roots = new List<Category>();
foreach (var row in rows)
{
if (row.ParentId is int parentId) byId[parentId].Children.Add(byId[row.Id]);
else roots.Add(byId[row.Id]);
}
return roots;
}
Mistake: Assuming a BST is always O(log n).
Why it is wrong: Inserting sorted or nearly sorted data into a BST without self-balancing makes it a linked list with O(n) operations.
Instead: Use a self-balancing tree (SortedSet<T>, SortedDictionary<TKey, TValue>), or shuffle inputs when building simple trees.
Mistake: Validating a BST by comparing each node only with its direct children. Why it is wrong: A deeper node can break the rule against an ancestor while every parent-child pair looks fine. Instead: Pass down min/max bounds as you recurse.
Mistake: Loading hierarchical data one level per query. Why it is wrong: It causes N+1 queries, one per node, and rebuilds become slow and hammer the database. Instead: Load all rows (or use a recursive CTE) and build the tree in memory with a dictionary in O(n).
Mistake: Recursing over deep or user-supplied trees. Why it is wrong: Stack depth equals tree height, and a stack overflow kills the process. Instead: Use iterative traversal with an explicit stack or queue.
Mistake: Confusing binary trees with B-trees when reasoning about indexes. Why it is wrong: A binary tree over 100 million rows would be about 27 levels deep; a B+ tree is 3β4, which is why seeks are cheap. Instead: Think of indexes as wide, shallow page trees with linked leaves for range scans.
Trees matter less as something I implement and more as a mental model: database indexes (B+ trees), ordered collections (red-black trees), syntax and expression trees, and every hierarchical domain model. Knowing that an index seek is a walk down a shallow B+ tree, or that SortedDictionary is O(log n) because it is a balanced tree, lets me predict performance without benchmarking everything.
| Need | Structure I reach for | Why |
|---|---|---|
| Ordered in-memory data with frequent changes | SortedSet<T> / SortedDictionary |
Red-black tree, O(log n) guaranteed |
| Persistent ordered lookups and ranges | Database B+ tree index | Few page reads, linked leaves for ranges |
| Hierarchical domain data (categories, org chart) | Adjacency list in SQL + in-memory tree built in O(n) | Simple writes, fast reads once cached |
| Code analysis or query translation | Roslyn syntax trees, expression trees | Tooling already provides traversal and rewriting |
| Prefix search | Trie or a database prefix index | Cost depends on key length, not item count |
The recurring production issues are balance (degenerate structures) and access patterns for hierarchies (N+1 loading, deep recursion). I load hierarchies in bulk, build them with dictionaries in O(n), and traverse iteratively when depth is not under my control.
A heap is a complete tree stored in an array that keeps the heap property: in a min-heap every parent β€ its children, so the smallest item is always at the root.
.NET's PriorityQueue<TElement,TPriority> (.NET 6+) is an array-backed min-heap (4-ary internally). Enqueue, Dequeue, TryPeek, EnqueueDequeue; not stable for equal priorities; no decrease-key (Remove was added in .NET 9). For a max-heap, pass a reversed comparer.
Top K from a stream: keep a min-heap of size K; for each item, if it beats the heap's minimum, replace it. That is O(n log K) time and O(K) memory.
(Not to be confused with the managed heap, which is memory.)
A trie is a tree where each edge is a character and each path from the root spells a prefix. Nodes mark where a complete word ends.
syd"): walk L nodes, then collect the subtree.Great for autocomplete, spell-check, word games and longest-prefix matching (IP routing). Downside: memory (one node per character, plus child maps). Compressed (radix) tries merge single-child chains.
Alternatives: a sorted array + binary search for prefixes (compact, often fast enough), a database LIKE 'syd%' on an index, or a search engine's suggesters for fuzzy, ranked autocomplete at scale.
A graph is a set of vertices (nodes) connected by edges. Edges can be directed or undirected, weighted or not, and the graph may contain cycles. Trees are a special case (connected, no cycles).
Representations:
Dictionary<TNode, List<TNode>>): O(V + E) memory, best for sparse graphs (most real ones).bool[V,V]): O(VΒ²) memory, O(1) edge check, for small dense graphs.Backend graphs: service and package dependencies, DI containers, build systems (project references), workflow/job DAGs, permission inheritance, routing, social links.
For dependencies you need a DAG: find an execution order with topological sort (Kahn's algorithm, O(V + E)); if not all nodes can be ordered, there is a cycle.
How to tell it (STAR)
**S:** Rebuilding our [category tree] cache took [45 seconds] and hammered the database. **T:** I was asked to make it fast and cheap. **A:** Profiling showed [12,000] queries, one per node. I loaded all rows in one query and built the tree in memory with a dictionary keyed by ID, linking children to parents in a single pass. **R:** The rebuild dropped to [200 ms], database load from it disappeared, and we added a query-count test so it stays that way.
Last updated 23 Oct 2026.