munotes®

The Array That Holds a Heap

Get access to whole semester resourcesSemester Pass

Chapter Eighty-Two

Syllabus topic Module 2, "Priority Queues & Heaps: Heaps"

Pages 258 to 261 of 411

In one line

Because a heap is always complete there are no gaps, so it fits an array exactly, with children at 2i + 1 and 2i + 2 and no pointers at all.

The representation

Read the heap level by level, left to right, and write the values into an array in that order. Then:

root = index 0

left child of i = 2i + 1

right child of i = 2i + 2

parent of i = (i - 1) / 2, integer division

last node = index n - 1

Chapter 58 gave that arithmetic. What makes it usable here, and unusable for a general binary tree, is that a complete tree has no gaps, so every index from 0 to n - 1 holds a real node.

import heapq

heap = [3, 5, 17, 9, 11, 25, 30, 12, 15]

print("the array:", heap)
print()
print("%6s %7s %10s %10s %9s" % ("index", "value", "left", "right", "parent"))
for i, value in enumerate(heap):
    left, right = 2 * i + 1, 2 * i + 2
    left_value = heap[left] if left < len(heap) else "-"
    right_value = heap[right] if right < len(heap) else "-"
    parent_value = heap[(i - 1) // 2] if i > 0 else "-"
    print("%6d %7d %10s %10s %9s" % (i, value, left_value, right_value, parent_value))

print()
print("no index is empty, because the tree is complete:",
      all(v is not None for v in heap))
print()
print("drawn as a tree, level by level:")
level, index = 0, 0
while index < len(heap):
    count = 2 ** level
    row = heap[index:index + count]
    print("   %s%s" % (" " * (16 - 2 * level), "   ".join(str(v) for v in row)))
    index += count
    level += 1
the array: [3, 5, 17, 9, 11, 25, 30, 12, 15]

 index   value       left      right    parent
     0       3          5         17         -
     1       5          9         11         3
     2      17         25         30         3
     3       9         12         15         5
     4      11          -          -         5
     5      25          -          -        17
     6      30          -          -        17
     7      12          -          -         9
     8      15          -          -         9

no index is empty, because the tree is complete: True

drawn as a tree, level by level:
                   3
                 5   17
               9   11   25   30
             12   15

Every parent and child relation is computed, not stored. The tree exists entirely in the arithmetic.

What this buys

No pointers. A linked binary tree of n nodes carries 2n pointers, mostly null (chapter 70 counted n + 1 of them). A heap carries none. For a million integers that is 16 MB of pointers not spent.

The parent for free. Chapter 58 noted this and here it is used: sift up in chapter 84 walks from a node to the root, and does it with (i - 1) // 2 and no stored parent pointers, which a linked tree cannot do without an extra field.

munotes.in258

The Array That Holds a Heap

Perfect cache behaviour. The nodes are contiguous, so walking a path reads neighbouring memory. This is the structure chapter 22's warning does not apply to.

The shape maintains itself. Adding an item means appending to the array, which is exactly adding the next node of a complete tree. Removing the last node means shortening the array. The shape property can never be broken, because an array has no gaps. Only the order property needs repairing, and that is what chapter 84 does.

That last point is the elegant part: half the heap's invariant is enforced by the representation rather than by code.

The two operations, in outline

Both are the same two steps in opposite orders, and chapter 84 implements them.

Insert. Append the new value at the end of the array, which keeps the shape. Then move it up until the order property holds. That is at most the height, O(log n).

Remove the best. Take index 0, which is the answer. Move the last element into index 0, which keeps the shape, and shorten the array. Then move it down until the order property holds. Again O(log n).

In both, the shape is fixed first and cheaply, and then the order is repaired.

The off-by-one that ruins it

The 1-based convention of chapter 58 is common in textbooks:

Conventionleftrightparent
root at 02i + 12i + 2(i - 1) / 2
root at 12i2i + 1i / 2

Mixing them is the standard error and it does not fail loudly: the structure continues to work for small indices and goes wrong deeper in. State the convention and use it everywhere, including in the termination conditions of the loops.

def children_zero(i):
    return 2 * i + 1, 2 * i + 2


def parent_zero(i):
    return (i - 1) // 2


def children_one(i):
    return 2 * i, 2 * i + 1


def parent_one(i):
    return i // 2


n = 20
zero_ok = all(parent_zero(c) == i for i in range(n) for c in children_zero(i))
one_ok = all(parent_one(c) == i for i in range(1, n) for c in children_one(i))
print("0-based: parent(child(i)) == i for every i:", zero_ok)
print("1-based: parent(child(i)) == i for every i:", one_ok)
print()
print("mixing them, 0-based children with 1-based parent:")
for i in (0, 1, 2, 3, 7):
    left, _ = children_zero(i)
    print("   node %2d, left child %2d, 1-based parent of that child says %2d  %s"
          % (i, left, parent_one(left),
             "correct" if parent_one(left) == i else "WRONG"))
munotes.in259

The Array That Holds a Heap

0-based: parent(child(i)) == i for every i: True
1-based: parent(child(i)) == i for every i: True

mixing them, 0-based children with 1-based parent:
   node  0, left child  1, 1-based parent of that child says  0  correct
   node  1, left child  3, 1-based parent of that child says  1  correct
   node  2, left child  5, 1-based parent of that child says  2  correct
   node  3, left child  7, 1-based parent of that child says  3  correct
   node  7, left child 15, 1-based parent of that child says  7  correct

Interesting, and worth reading carefully: for a left child the mixed arithmetic happens to agree, because (2i + 1) // 2 equals i. The error only shows on right children, which is precisely why it survives casual testing and then fails on real data.

Quick revision

  • A heap is stored as an array read level by level, left to right.
  • Root at 0; children of i at 2i + 1 and 2i + 2; parent of i at (i - 1) / 2.
  • It works because a complete tree has no gaps, so every index below n holds a node.
  • No pointers at all, the parent is free, and the memory is contiguous so the cache works.
  • The shape property is enforced by the representation: appending adds the next node of a complete tree

and an array cannot have a gap. Only the order needs repairing.

  • Insert: append, then sift up. Remove: take index 0, move the last element there, shorten, then sift

down. Both O(log n).

  • Mixing the 0-based and 1-based conventions agrees by accident on left children and fails on right ones.

Test yourself

1. Give the index arithmetic for a heap with the root at 0. Children of i at 2i + 1 and 2i + 2; parent of i at (i - 1) divided by 2 with integer division.

2. Why does this representation work for a heap when chapter 58 showed it wasteful for a general tree? Because a heap is always complete, so there are no gaps and every index from 0 to n - 1 holds a real node. A general tree may be degenerate and would need an exponentially large array.

3. Name three things the array representation buys. No pointers at all; the parent of a node for free from arithmetic; and contiguous memory, so the cache works well.

4. Which of the heap's two properties is enforced by the representation, and how? The shape property. Appending to the array adds exactly the next node of a complete tree, and an array cannot contain a gap, so the shape can never be broken. Only the order property needs repairing.

5. Outline insertion and removal. Insert: append at the end, which keeps the shape, then sift the value up until the order holds. Remove: take index 0 as the answer, move the last element to index 0, shorten the array, then sift it down.

munotes.in260

The Array That Holds a Heap

6. Why does mixing the 0-based and 1-based conventions survive casual testing? Because for a left child the mixed arithmetic happens to agree: (2i + 1) divided by 2 is i. The error appears only on right children.

munotes.in261

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.

Issue
Done!