Concepts / Tree Traversal

Tree Traversal

A search compares the item with the root, then discards either the left or right subtree, and repeats on the remaining subtree.

  • Programming
Interactive lab

Try it: The Call Stack

How the call stack keeps track of function calls: each call pushes a frame with its own arguments and local variables, and each return pops it.

How it works

  1. Calling a function pushes a new frame on top of the stack; the caller pauses.
  2. The frame holds that call's parameters and local variables.
  3. When the function returns, its frame is popped and the return value goes back to the paused caller.
  4. A recursive function pushes one frame per call until the base case, then the frames unwind in reverse order.

Default run (17 steps): Program starts: main is about to call factorial(4). … The stack is empty again. Result: 24. Deepest point: 4 frames, 4 calls in total.

Educational simulation

Loading the simulation…

Why Tree Operations Are Not Always Fast

A binary sort tree keeps smaller items on the left and larger items on the right of every node. Because of that rule, it is tempting to assume that searching for an item and inserting an item will always be fast. The placement rule is sound, but the speed of both operations depends on something the placement rule alone does not guarantee: the overall shape of the tree. This article starts from that assumption, shows exactly where it breaks, and then builds the search, insertion, and counting procedures so you can see which step depends on shape.

Searching by Discarding Subtrees

A search begins by comparing the item you want with the root item. There are three possible outcomes. If the two are equal, the search is finished and the item has been found. If the item is less than the root, only the left subtree can contain it, so the right subtree can be thrown away. If the item is greater than the root, only the right subtree needs to be examined, and the left subtree can be thrown away. The same procedure is then applied to whichever subtree remains.

Notice what a single comparison buys. One comparison at a node does not merely test one item. It removes an entire subtree from further consideration. When the tree is balanced, each comparison removes about half of the remaining items, which is the same pattern used by binary search on a sorted list. That is why a balanced tree gives a similarly efficient search, and why an unbalanced tree does not.

50compared first30discarded subtree20discarded70compared second40discarded60found, search stops80never examinedleftrightleftrightleftright
Which nodes does a search for 60 visit, which subtree is discarded at each comparison, and where does the search stop?

What do you think happens?

In the tree above, where does a search for 25 stop?

  • At 20, after falling off its empty right side

  • At 30, because 25 is smaller than 30

  • At 40, because 40 is the last node compared

Reveal answer
Answer:

At 20, after falling off its empty right side

25 is less than the root 50, so the right subtree (70 and its descendants) is discarded and the search moves left to 30. 25 is less than 30, so the subtree containing 40 is discarded and the search moves left to 20. 25 is greater than 20, and the right side of 20 is empty, so the search falls off the tree there without finding 25.

Worked example

Tracing a successful search for 60

Search for 60 in the tree with root 50, whose left child is 30 and right child is 70. Node 30 has children 20 and 40, and node 70 has children 60 and 80.

  1. 1
    Compare with the root 50

    60 is greater than 50, so only the right subtree can contain it. The left subtree rooted at 30, which holds 20 and 40, is discarded.

  2. 2
    Compare with 70

    60 is less than 70, so only the left subtree of 70 can contain it. The right subtree holding 80 is discarded.

  3. 3
    Compare with 60

    The two values are equal, so the search is finished and 60 has been found.

Result

60 is found after three comparisons. The nodes 30, 20, 40, and 80 were never compared, because two comparisons each discarded a whole subtree.

Insertion Follows the Search Path

Insertion is similar to search. The procedure first searches the tree for the position where the new item belongs. The path it follows is exactly the path a search for that item would follow. When the search falls off the tree at an empty spot, a new node is created and attached at that place. In other words, the new node takes the position of the empty link where the search would have ended.

  1. 1
    If the tree is empty, create the new node and make it the root. The empty tree is its own case and must be handled before any comparison.
  2. 2
    Otherwise, compare the new item with the current node.
  3. 3
    If the new item is smaller, move to the left child; if it is larger, move to the right child.
  4. 4
    Repeat the comparison at each node until the path reaches an empty position.
  5. 5
    Create a new node with the item and attach it at that empty position.

Inserting 25 into the generated tree from the search example follows the same path as the failed search. 25 is less than 50, so it moves left to 30. It is less than 30, so it moves left to 20. It is greater than 20, and the right side of 20 is empty, so a new node holding 25 is attached there. The new node is a leaf, and every other node keeps its position.

How Insertion Order Shapes the Tree

A binary tree is balanced when, for each node, the left subtree has approximately the same number of nodes as the right subtree. In a perfectly balanced tree those two counts differ by at most one. Not every binary tree is balanced. If items are inserted in a random order, there is a high probability that the tree comes out approximately balanced. If the order of insertion is not random, the tree can become very unbalanced.

Insertion orderRootResulting shapeDepth of the deepest node
2, then 1, then 32Root with 1 on the left and 3 on the right1 edge below the root
1, then 2, then 31A chain where every node has an empty left child2 edges below the root

The same three values produce a shallow, balanced tree in one order and a lopsided chain in the other.

The chain is the extreme case, and it shows why the balance matters. Every node has an empty left subtree, so each comparison removes only one item from consideration instead of about half of the remaining items. With three values the difference looks small, but with a long list of insertions in sorted order, the search path grows with the number of items. The efficient behavior described for binary search trees depends on balance, and balance depends on the insertion order.

Splitting a Tree into Three Pieces

Definition

A subtree is a node together with all of its descendants, meaning its children, its children's children, and so on. A subtree is itself a binary tree, and every node has its own subtree, so subtrees appear at every level and are nested inside one another.

Any non-empty binary tree can be viewed as three pieces: a root node, a left subtree, and a right subtree. Either subtree, or both, may be empty. In the six-node tree used throughout this article, the root has node 2 as the top of its left subtree, and node 2 together with its descendants 4 and 5 forms that subtree. Node 3 and its descendant 6 form the right subtree of the root. The root itself is not part of either subtree.

rootnot part of either subtreenode 2top of left subtreenode 4descendant of 2node 5descendant of 2node 3top of right subtreenode 6descendant of 3leftrightchildchildchild
Which nodes belong to the root, the left subtree, and the right subtree of the root, and how does each subtree sit inside the larger tree?

This description is recursive, because it refers to smaller binary trees inside its own definition. A tree is defined in terms of trees. The shape matches the recursive definition of the TreeNode class used in code, where each node refers onward to its left and right children. Since the data and the definition have the same shape, a subroutine that handles a tree can naturally call itself on the smaller pieces, which is why recursive subroutines are so often used to process trees.

Counting Nodes Recursively

Counting the nodes of a binary tree looks simple, but a single straight-line pass is awkward. Each node can lead to two branches, and once you move down one branch you must still remember that the other branch exists. Recursion handles this bookkeeping automatically. The recursive rule is almost trivial: the count for an empty tree is zero, which is the base case that stops the recursion. For a tree with a root, the count is the number of nodes in the left subtree, plus the number in the right subtree, plus one for the root itself.

Key point

Count(empty tree) = 0. Count(non-empty tree) = Count(left subtree) + Count(right subtree) + 1. The base case stops the recursion, and the results are combined by addition as they return.

A recursive count does not finish in one visible step. Each call starts another call on a smaller subtree, and it cannot produce its own answer until the calls beneath it have returned. Empty subtrees are left out of the diagram below, because each of them returns zero immediately. Read the diagram from the top: the call for the root begins first, and the results travel back up as each call finishes.

rootreturns 6node 2returns 3node 4returns 1node 3returns 2node 5returns 1node 6returns 1calls leftcalls rightcallscallscalls
How do the recursive calls stack up for the six-node tree, where does the base case stop them, and how do the returned counts add up to the total of 6?
Worked example

Counting the six-node tree

Count the nodes in the six-node tree: a root whose left subtree is node 2 with children 4 and 5, and whose right subtree is node 3 with child 6.

  1. 1
    Finish the leaves first

    Node 4 has two empty subtrees, so its count is 0 + 0 + 1 = 1. Node 5 likewise returns 1. Node 6 returns 1.

  2. 2
    Combine at node 2

    Node 2 adds the count from its left subtree, 1, and from its right subtree, 1, plus one for itself: 1 + 1 + 1 = 3.

  3. 3
    Combine at node 3

    Node 3 has an empty left subtree, which returns 0, and a right subtree whose count is 1. Its count is 0 + 1 + 1 = 2.

  4. 4
    Combine at the root

    The root adds 3 from the left, 2 from the right, and 1 for itself: 3 + 2 + 1 = 6.

Result

The recursive calls return 1, 1, and 1 for the leaves, 3 and 2 for their parents, and 6 for the root, so the tree has six nodes.

No loop has to keep a list of pending branches. The call stack that the language already maintains holds that bookkeeping. Each call waits for the calls beneath it, and each return value is added to a running total at the level above.

Mistakes Learners Make with Tree Recursion

  • Common mistake

    Assuming that the placement rule alone guarantees fast searches

    What it looks like

    Inserting 1, 2, 3, 4 in that order produces a chain in which every node has an empty left subtree.

    Why it fails

    The speed of search and insertion depends on the overall shape, and the placement rule does not keep the shape balanced.

    Do this instead

    Check whether the insertion order is random or lopsided, and remember that speed depends on balance.

  • Common mistake

    Forgetting the empty-tree case in insertion

    Why it fails

    An empty tree has no node to compare with, so the search path has no first step.

    Do this instead

    Test whether the tree is empty first, create the new node as the root in that case, and only then compare.

  • Common mistake

    Leaving out the plus one for the root when counting

    What it looks like

    Counting node 3 as 0 + 1 = 1 instead of 0 + 1 + 1 = 2.

    Why it fails

    The rule counts the root of each subtree as well as its children's subtrees, so omitting the root undercounts.

    Do this instead

    Always add one for the current node after adding the left and right counts.

  • Common mistake

    Leaving the top node out of its own subtree

    Why it fails

    A subtree is a node together with all of its descendants, so its top node is part of it.

    Do this instead

    When naming the subtree rooted at node 2, include node 2 along with nodes 4 and 5.

  • Common mistake

    Expecting a recursive insertion routine to change the caller's root

    Why it fails

    A subroutine that receives the root as a parameter cannot change the caller's variable.

    Do this instead

    Use a non-recursive insertion routine that works directly with the static member variable root.

Practice: Trace a Search and a Count

EASY

Using the search tree with root 50, search for 40. List each comparison in order, state which subtree is discarded after each one, and say where the search stops.

Hints
  • Start by comparing 40 with 50 and decide which side can contain 40.
  • After moving left to 30, compare again and decide whether 40 belongs in the left or right subtree of 30.
  • The answer should use three comparisons and end at 40.
MEDIUM

Using the six-node tree, count the nodes in the subtree rooted at node 2 by applying the recursive rule. Then state the count for the whole tree.

Hints
  • The subtree rooted at node 2 contains node 2 and its descendants 4 and 5.
  • Apply Count(empty) = 0 to the empty subtrees under 4 and 5 before adding one for each node.
  • The subtree rooted at node 2 has 3 nodes; the whole tree has 6.

Key Takeaways

  • A search compares the item with the root and then discards either the left or right subtree, repeating on what remains; one comparison removes a whole subtree.
  • Insertion follows the same path a search would take, creates a new node at the first empty position, and needs its own case for the empty tree.
  • Searching and inserting are efficient only when the tree is close to balanced, and the insertion order decides whether that holds.
  • A non-empty binary tree can be split into a root, a left subtree, and a right subtree, where either subtree or both may be empty.
  • The recursive count is zero for an empty tree and otherwise the left count plus the right count plus one, with results returning upward through the call stack.