Solution: Checking Completeness

Example 3 showed that completeness cannot be checked one node at a time. Here is that tree again:

            ╭───╮
            │ 1 │
            ╰─┬─╯
      ┌───────┴───────┐
    ╭─┴─╮           ╭─┴─╮
    │ 3 │           │ 2 │
    ╰─┬─╯           ╰─┬─╯
  ┌───┘           ┌───┘
╭─┴─╮           ╭─┴─╮
│ 5 │           │ 8 │
╰───╯           ╰───╯

3 is missing its right child, and 2 has a child further along the same level. To see that, the check has to read the tree across each level.

Reading the tree in level order

The tree chapter has a traversal that reads a tree one level at a time: level order. It visits the nodes top to bottom, and left to right within each level, and it keeps the nodes waiting to be visited in a queue.

Now list the child slots in the same order: the root’s left and right, then each child’s left and right, and so on. In a complete tree, every filled slot comes before every empty slot.

So the check is: traverse the tree in level order, and remember whether we have passed an empty child slot. If we find a node after that, the tree is not complete.

private static <T> boolean isComplete(Node<T> root) {
  if (root == null) {
    return true;
  }
  Queue<Node<T>> queue = new LinkedQueue<>();
  queue.enqueue(root);
  boolean gap = false;  // have we passed a missing child yet?
  while (!queue.isEmpty()) {
    Node<T> node = queue.front();
    queue.dequeue();
    if (node.left == null) {
      gap = true;
    } else if (gap) {
      return false;  // a node after a missing child
    } else {
      queue.enqueue(node.left);
    }
    if (node.right == null) {
      gap = true;
    } else if (gap) {
      return false;  // a node after a missing child
    } else {
      queue.enqueue(node.right);
    }
  }
  return true;
}

The loop is the level-order traversal from the tree chapter, with the gap check added at each child slot. We never enqueue null, because our enqueue rejects it.

Tracing it on Example 3

Start with 1 in the queue, and gap set to false:

            ╭───╮
            │ 1 │
            ╰─┬─╯
      ┌───────┴───────┐
    ╭─┴─╮           ╭─┴─╮
    │ 3 │           │ 2 │
    ╰─┬─╯           ╰─┬─╯
  ┌───┘           ┌───┘
╭─┴─╮           ╭─┴─╮
│ 5 │           │ 8 │
╰───╯           ╰───╯

queue:

┌───┐
│ 1 │
└───┘
  ▲
front, back

gap = false

Take 1 from the front. Its left child 3 and right child 2 both exist, and there is no gap yet, so both go in the queue:

            ╭───╮
            │█1█│
            ╰─┬─╯
      ┌───────┴───────┐
    ╭─┴─╮           ╭─┴─╮
    │ 3 │           │ 2 │
    ╰─┬─╯           ╰─┬─╯
  ┌───┘           ┌───┘
╭─┴─╮           ╭─┴─╮
│ 5 │           │ 8 │
╰───╯           ╰───╯

queue:

┌───┬───┐
│ 3 │ 2 │
└───┴───┘
  ▲   ▲
front back

gap = false

Take 3 from the front. Its left child 5 goes in the queue. Its right child is missing, so gap becomes true:

            ╭───╮
            │ 1 │
            ╰─┬─╯
      ┌───────┴───────┐
    ╭─┴─╮           ╭─┴─╮
    │█3█│           │ 2 │
    ╰─┬─╯           ╰─┬─╯
  ┌───┘           ┌───┘
╭─┴─╮           ╭─┴─╮
│ 5 │           │ 8 │
╰───╯           ╰───╯

queue:

┌───┬───┐
│ 2 │ 5 │
└───┴───┘
  ▲   ▲
front back

gap = true

Take 2 from the front. Its left child 8 exists, but gap is already true, so isComplete returns false:

            ╭───╮
            │ 1 │
            ╰─┬─╯
      ┌───────┴───────┐
    ╭─┴─╮           ╭─┴─╮
    │ 3 │           │█2█│
    ╰─┬─╯           ╰─┬─╯
  ┌───┘           ┌───┘
╭─┴─╮           ╭─┴─╮
│ 5 │           │ 8 │
╰───╯           ╰───╯

queue:

┌───┐
│ 5 │
└───┘
  ▲
front, back

gap = true

On Example 1, the first missing slot is the right child of 2. Every node left in the queue after that is a leaf, so no node comes after the gap, and isComplete returns true.

The cost

The level-order traversal visits each node at most once and does a constant amount of work per visit, so isComplete is . isOrdered is too, so isMinHeap is .

The queue holds the nodes waiting to be visited. At one point it holds the whole bottom level, and the bottom level of a complete tree can hold about half of its nodes. So the extra space is . The recursion in isOrdered also uses space, one call frame per level, and on a tree where every node has at most one child that is levels.