Heapify: Sifting Up and Sifting Down
Chapter Eighty-Four
Syllabus topic Module 2, "Priority Queues & Heaps: Heapifying the element"
Pages 265 to 268 of 411
In one line
Sifting up moves a value towards the root while it is better than its parent, and sifting down moves it away from the root while a child is better, and the two together are all a heap ever does.
The two directions
A heap is repaired in exactly two situations, and each has its own direction.
One value is too good for its position, and everything else is fine. That happens after an insertion at the end. The value may be smaller than its parent, so it moves up.
One value is too bad for its position, and everything else is fine. That happens after a removal, when the last element is moved to the root. It may be larger than its children, so it moves down.
In both cases exactly one value is out of place and the path it travels is at most the height.
Sift up
sift_up(i):
while i > 0 and heap[i] is better than heap[parent(i)]:
swap them
i = parent(i)
Sift down
sift_down(i):
loop:
best = i
if left child exists and is better than heap[best]: best = left
if right child exists and is better than heap[best]: best = right
if best == i: stop
swap heap[i] and heap[best]
i = best
The difference worth noticing: sift up compares against one node, the parent. Sift down compares against two, and must swap with the better of the two children, not merely with one that is better than the current value. Swapping with the wrong child breaks the heap at the other one.
Both, traced
def parent(i):
return (i - 1) // 2
def sift_up(heap, i, trace):
while i > 0 and heap[i] < heap[parent(i)]:
p = parent(i)
heap[i], heap[p] = heap[p], heap[i]
trace.append("swap index %d with parent %d -> %s" % (i, p, list(heap)))
i = p
return i
def sift_down(heap, i, trace):
n = len(heap)
while True:
left, right, best = 2 * i + 1, 2 * i + 2, i
if left < n and heap[left] < heap[best]:
best = left
if right < n and heap[right] < heap[best]:
best = right
if best == i:
return i
heap[i], heap[best] = heap[best], heap[i]
trace.append("swap index %d with child %d -> %s" % (i, best, list(heap)))
i = best
def holds(heap):
n = len(heap)
return all(heap[i] <= heap[c]
for i in range(n) for c in (2 * i + 1, 2 * i + 2) if c < n)
print("INSERT 2 into a min-heap")
heap = [3, 5, 17, 9, 11, 25, 30]
print(" before :", heap, "| heap:", holds(heap))
heap.append(2)
print(" appended :", heap, "| heap:", holds(heap), " (shape is fine, order is not)")
trace = []
sift_up(heap, len(heap) - 1, trace)
for line in trace:
print(" ", line)
print(" after :", heap, "| heap:", holds(heap))
print()
print("REMOVE the best from that heap")
best = heap[0]
last = heap.pop()
heap[0] = last
print(" took %d, moved %d to the root: %s | heap: %s"
% (best, last, heap, holds(heap)))
trace = []
sift_down(heap, 0, trace)
for line in trace:
print(" ", line)
print(" after :", heap, "| heap:", holds(heap))Heapify: Sifting Up and Sifting Down
INSERT 2 into a min-heap
before : [3, 5, 17, 9, 11, 25, 30] | heap: True
appended : [3, 5, 17, 9, 11, 25, 30, 2] | heap: False (shape is fine, order is not)
swap index 7 with parent 3 -> [3, 5, 17, 2, 11, 25, 30, 9]
swap index 3 with parent 1 -> [3, 2, 17, 5, 11, 25, 30, 9]
swap index 1 with parent 0 -> [2, 3, 17, 5, 11, 25, 30, 9]
after : [2, 3, 17, 5, 11, 25, 30, 9] | heap: True
REMOVE the best from that heap
took 2, moved 9 to the root: [9, 3, 17, 5, 11, 25, 30] | heap: False
swap index 0 with child 1 -> [3, 9, 17, 5, 11, 25, 30]
swap index 1 with child 3 -> [3, 5, 17, 9, 11, 25, 30]
after : [3, 5, 17, 9, 11, 25, 30] | heap: TrueFollow the insertion. 2 was appended at index 7, then rose through indices 3 and 1 to reach the root, three swaps for a tree of height 3. The order property was broken immediately after the append, exactly as chapter 82 said, and the shape never was.
Why sifting down must take the better child
def holds(heap):
n = len(heap)
return all(heap[i] <= heap[c]
for i in range(n) for c in (2 * i + 1, 2 * i + 2) if c < n)
def sift_down_correct(heap, i):
n = len(heap)
while True:
left, right, best = 2 * i + 1, 2 * i + 2, i
if left < n and heap[left] < heap[best]:
best = left
if right < n and heap[right] < heap[best]:
best = right
if best == i:
return
heap[i], heap[best] = heap[best], heap[i]
i = best
def sift_down_wrong(heap, i):
"""Swaps with the FIRST child that is better, not the BETTER child."""
n = len(heap)
while True:
left, right = 2 * i + 1, 2 * i + 2
if left < n and heap[left] < heap[i]:
heap[i], heap[left] = heap[left], heap[i]
i = left
elif right < n and heap[right] < heap[i]:
heap[i], heap[right] = heap[right], heap[i]
i = right
else:
return
start = [20, 5, 3, 8, 9, 7, 6]
for name, routine in (("correct", sift_down_correct), ("wrong ", sift_down_wrong)):
heap = list(start)
routine(heap, 0)
print("%s: %s | a valid heap: %s" % (name, heap, holds(heap)))
print()
print("the wrong version swapped 20 with its LEFT child 5, but 3 was smaller.")
print("5 then sat above 3, which breaks the order property at the root.")Heapify: Sifting Up and Sifting Down
correct: [3, 5, 6, 8, 9, 7, 20] | a valid heap: True
wrong : [5, 8, 3, 20, 9, 7, 6] | a valid heap: False
the wrong version swapped 20 with its LEFT child 5, but 3 was smaller.
5 then sat above 3, which breaks the order property at the root.The wrong version produced an array that is not a heap, and it did so without any error. 5 ended up at the root while 3 sits below it. That is why the rule is "swap with the better of the two children", not "swap with a child that is better".
The costs
| Cost | |
|---|---|
| sift up | at most the height, so O(log n) |
| sift down | at most the height, so O(log n) |
| insert | append O(1), then sift up: O(log n) |
| remove best | take the root, move the last up, shorten, then sift down: O(log n) |
| peek best | O(1) |
Sift down does about twice the comparisons of sift up per level, because it examines two children rather than one parent. Both are O(log n) and the constant difference is why building a heap is done with sift down in chapter 85 rather than with repeated insertion.
Quick revision
- Two repairs, two directions. Sift up after an insertion at the end; sift down after the last element is
moved to the root.
- Sift up: while better than the parent, swap and move up. One comparison per level.
- Sift down: find the better of the two children; if it is better than the current value, swap and move
down. Two comparisons per level.
- Swapping with the first better child rather than the better child produces an array that is not a heap,
silently.
- The shape is never broken by either operation; only the order is repaired.
- Both are O(log n), so insert and remove are O(log n) and peek is O(1).
Test yourself
1. When is each direction used? Sift up after inserting at the end of the array. Sift down after removing the root and moving the last element into its place.
2. Write sift up. While the index is not 0 and the value is better than its parent, swap the two and move to the parent's index.
3. Write sift down, and state the rule that is easy to get wrong. Find the better of the two children; if it is better than the current value, swap and continue from there. The rule is to swap with the BETTER child, not merely with a child that is better.
Heapify: Sifting Up and Sifting Down
4. What happens if you swap with the first better child instead? The result is not a heap, and no error is raised. In the run, 5 ended at the root while 3 was below it.
5. Which operation does more comparisons per level, and why? Sift down, because it must examine both children, where sift up examines only the parent.
6. Give the costs of insert, remove and peek, with their parts. Insert: append O(1) then sift up O(log n), so O(log n). Remove: take the root, move the last element there, shorten, then sift down O(log n), so O(log n). Peek: O(1).
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.