The Cost of the Iterator

The work in every iterator operation is either a stack check or a call to pushLeftSpine, which follows a path down the tree. A path down the tree is at most edges long, where is the height of the tree, as we measured it in the tree chapter. Let be the number of values in the tree.

  • Creating the iterator runs pushLeftSpine(root), which follows the left edges down from the root: .
  • hasNext is a single isEmpty check: .
  • next pops one node, then runs pushLeftSpine on its right child, another path down the tree: in the worst case. In the trace, the call that produced 50 pushed 70 and 60.

The worst case does not happen on every call. Over the whole traversal, every node is pushed once and popped once, so visiting all values is , which is per call to next. That is an amortized , the same kind of bound as the dynamic array’s add. There, an occasional add takes time to grow the array, but because the array doubles each time, those copies are rare enough that add is averaged over all the calls.

The auxiliary space is the stack. The nodes on the stack all lie on one path down from the root, so the stack holds at most nodes, which is .

The iterators in the iterator chapter were per call and used auxiliary space. This one matches the time bound only when we amortize, and it does not meet the space bound. The space is still reasonable. Our nodes have no link to their parent. So when the iterator stops partway through the traversal, it has to remember the ancestors it has not visited yet. It never keeps a copy of all values, the way the anti-pattern from the iterator chapter does when it gathers every value into a list first.