🔎 Analogy
A binary search tree is a guessing game played on a sorted line. Ask “is your number bigger or smaller than this one?” and with each answer you throw away half of what’s left — exactly how you find a word in a dictionary by opening near the middle and narrowing down, instead of reading every page.
The ordering invariant
A BST is a binary tree with one extra rule that holds at every node: all keys in the left subtree are smaller, and all keys in the right subtree are larger. That single invariant is what makes searching fast — at each node you compare once and discard an entire subtree.
graph TD A["8"] --> B["3"] A --> C["10"] B --> D["1"] B --> E["6"] C --> F["14"] E --> G["4"] E --> H["7"]
In the tree above, everything left of 8 is below 8 and everything right is above 8 — and the same holds recursively for 3, for 10, and so on.
Search and insert in O(h)
To search, start at the root and compare: equal means found, smaller means go left, larger means go right, nil means absent. Insert follows the identical path and hangs the new node where the search fell off the tree. Both do one comparison per level, so both cost O(h) where h is the height:
- Balanced tree:
h ≈ log₂ n, so search and insert areO(log n). - Degenerate tree (keys inserted already sorted):
h = n − 1, a one-sided chain, so both degrade toO(n)— no better than a linked list.
package main
import "fmt"
type Node struct {
Key int
Left, Right *Node
}
// insert returns the (possibly new) subtree root with key added.
func insert(n *Node, key int) *Node {
if n == nil {
return &Node{Key: key} // fell off the tree: hang the new node here
}
switch {
case key < n.Key:
n.Left = insert(n.Left, key)
case key > n.Key:
n.Right = insert(n.Right, key)
}
// equal key: ignore (no duplicates)
return n
}
// search walks down, discarding half the tree at each step. O(h).
func search(n *Node, key int) bool {
for n != nil {
switch {
case key == n.Key:
return true
case key < n.Key:
n = n.Left
default:
n = n.Right
}
}
return false
}
// inorder: left, node, right -> visits keys in ascending order.
func inorder(n *Node, out *[]int) {
if n == nil {
return
}
inorder(n.Left, out)
*out = append(*out, n.Key)
inorder(n.Right, out)
}
func main() {
var root *Node
for _, k := range []int{8, 3, 10, 1, 6, 14, 4, 7} {
root = insert(root, k)
}
var sorted []int
inorder(root, &sorted)
fmt.Println("inorder (sorted):", sorted)
fmt.Println("search 7:", search(root, 7)) // true
fmt.Println("search 9:", search(root, 9)) // false
}
The inorder walk prints the keys already sorted — that is the BST’s signature property. If you only ever need sorted output, a BST gives it to you in O(n) with no separate sort step.
Balance, and Go’s missing BST
Because performance hinges on height, real systems use self-balancing trees that rotate nodes to keep h at O(log n) no matter the insertion order. The two classics are the AVL tree (strictly balanced, faster lookups) and the red-black tree (looser balance, fewer rotations on insert/delete — the choice in many standard libraries, like C++‘s std::map).
Go’s standard library has no built-in BST. Reach for one of these instead:
- For ordered data, keep a sorted slice and binary-search it with
sort.Search—O(log n)lookups, with the trade-off that an insert isO(n)because elements shift. Great for read-heavy, write-light data. - For unordered key/value lookups, use the built-in
mapfor averageO(1)access — see hash tables. A map gives no ordering, which is exactly what a BST trades away speed for.
⚠️ A BST is not automatically balanced
Inserting keys in sorted order (1, 2, 3, 4, …) builds a tree that leans entirely to one side — height n − 1, so search becomes O(n), the very linear scan the tree was meant to avoid. A plain BST gives O(log n) only on reasonably random input; for worst-case guarantees you need a self-balancing variant (AVL or red-black) or a different structure altogether.
See also
- binary trees — the structure and traversals a BST builds on (inorder = sorted).
- heaps — a weaker ordering for O(1) min/max instead of full search.
- hash tables — O(1) average lookups when you don’t need order.
- binary search — the same halving logic on a sorted slice.
Next: a tree tuned for fast min/max instead of full ordering — heaps.
Related topics
Nodes with up to two children — structure, depth vs height, the three DFS traversals (inorder, preorder, postorder), and level-order BFS with a queue.
trees-graphsHeaps & Priority QueuesA binary heap packed in a slice — parent/child by index arithmetic, sift-up and sift-down in O(log n), and Go's container/heap to build a priority queue.
complexityBig-O & ComplexityMeasure cost before you optimize — Big-O for time and space, the common growth classes, and how to read the complexity of real Go code.
Check your understanding
Score: 0 / 51. What is the binary search tree ordering invariant?
The invariant holds recursively for the whole subtree, not just the immediate children. That global ordering is what lets search discard half the remaining tree at each step. (The last option describes a heap, not a BST.)
2. Why is BST search O(h) rather than O(log n) in general?
Each step descends one level, so the cost is the height h. Inserting already-sorted keys builds a one-sided chain of height n − 1, making search O(n). Self-balancing trees keep h at O(log n).
3. What order does an inorder traversal of a BST produce?
Inorder visits left subtree, node, right subtree. Because every left subtree holds smaller keys and every right subtree holds larger keys, this emits keys from smallest to largest — sorted, for free.
4. Deleting a BST node that has TWO children — what takes its place?
A leaf is removed directly; a one-child node is replaced by that child. With two children you can't just pick one, so you copy in the in-order successor (leftmost node of the right subtree) — the next-larger key — then delete that successor (which has at most one child). This preserves the ordering invariant.
5. What keeps a BST's height at O(log n) regardless of insertion order?
A plain BST can degrade to a chain (height n−1) on sorted input. AVL and red-black trees track balance and perform rotations to keep height O(log n), guaranteeing O(log n) search/insert/delete in the worst case — at the cost of a little bookkeeping per node.
Comments
Sign in with GitHub to join the discussion.