Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Trees, Heaps & Tries  ›  Lesson

Heaps & Live Charts

Heaps 25 minA tree in a list that always knows its smallest item
You're building a piece ofTunebox — discovery
This piece — top_charts(): Keeps the most-played tracks on top as plays roll in.
Scenario Plays stream into Tunebox all day, and the live chart must always show the most-played tracks. Each refresh counts the plays and pulls the top k off a heap.
Your task
Build top_charts(plays, k). plays is a stream of play events, one title per play. Count plays per title, then use a heap to return the k most-played titles, most plays first, with ties broken alphabetically. Example: top_charts(["Tides", "Neon", "Tides", "Glass", "Neon", "Tides"], 2) → ["Tides", "Neon"].

Heaps & Live Charts

Tunebox's live chart shows the most-played tracks right now, and plays arrive by the thousand every minute. Re-sorting after every play costs O(n log n) each time. A heap keeps the top item one step away and absorbs each change in O(log n) — it's the structure behind every "what matters most next?" question.

A complete binary tree, stored in a list

A heap is a binary tree with two rules. Shape: it's complete — every level is full except perhaps the last, which fills from the left. Order: in a min-heap, every parent is ≤ its children, so the smallest item is always at the root.

Because the shape has no gaps, the tree needs no nodes or pointers at all. Number the slots level by level and keep them in a plain list; parent and child links become arithmetic:

             3            index:  0   1   2   3    4   5
          /     \         heap:  [3,  8,  5,  12,  9,  7]
         8       5
        / \     /         parent(i) = (i - 1) // 2
      12   9   7          left(i)   = 2 * i + 1
                          right(i)  = 2 * i + 2
What does this print?
heap = [3, 8, 5, 12, 9, 7]
i = 2
print(heap[(i - 1) // 2], heap[2 * i + 1])

❓ Cross-question — "So a heap is just a sorted list?" No, and that's the point. [3, 8, 5, 12, 9, 7] isn't sorted — 8 comes before 5. A heap only promises that each parent beats its own children. That weaker promise is much cheaper to keep, and it still puts the winner at index 0.

Sift-up: adding an item

Append the new item in the next free slot, which keeps the tree complete. Then swap it with its parent for as long as it's smaller. Each swap climbs one level, so a push is O(log n). Step through pushing a 1:

step through it
1heap = [2, 5, 3, 9, 7]
2heap.append(1)
3i = len(heap) - 1
4while i > 0:
5 parent = (i - 1) // 2
6 if heap[parent] <= heap[i]:
7 break
8 heap[parent], heap[i] = heap[i], heap[parent]
9 i = parent
10print(heap)

Sift-down: removing the top

To pop the minimum, move the last item into the root and shrink the list. Then push that item down, swapping it with its smaller child while that child is smaller than it. Pick the wrong child and a bigger value ends up on top:

broken — fix it

Popping every item should give them back in sorted order, but this prints [1, 3, 5, 6, 2, 9], because sift_down only ever looks at the left child.

Popping everything off a heap is a sort — heapsort, O(n log n).

heapq: Python's min-heap

Python gives you sift-up and sift-down ready-made. heapq works on an ordinary list:

python

heapq is min-heap only. A chart wants the most plays on top, so push negated counts — the smallest -plays is the biggest plays:

What does this print?
import heapq

charts = []
for plays in [40, 95, 12, 60]:
    heapq.heappush(charts, -plays)
heapq.heappop(charts)
print(-charts[0])

Heap items can be tuples, which compare element by element. (-plays, title) ranks by plays and breaks ties alphabetically, without any extra code.

❓ Cross-question — "Why is heapify O(n), when n pushes cost O(n log n)?" It sifts down, starting from the last parent. Half the nodes are leaves and never move, a quarter move at most one level, an eighth at most two… That sum stays below 2n swaps.

Coming from Java/JS: heapq is Java's PriorityQueue without the object — also a min-heap, but where Java takes a Comparator, you negate or use tuples. JavaScript has no built-in heap.

Top-k with a size-k heap

"The 3 most-played" doesn't need every item in order. Keep a min-heap of size k holding the best so far; its root is the weakest of them, and a new item only gets in by beating it:

python

That's O(n log k) time and O(k) memory. The heap never grows past k, even while scanning a million play counts.

Operation Time
Peek at the minimum, heap[0] O(1)
Push (sift-up) O(log n)
Pop (sift-down) O(log n)
heapify a list O(n)
Top-k with a size-k heap O(n log k)
Merge k sorted lists, n items in total O(n log k)

Idioms & real-world patterns

nlargest, nsmallest and k-way merge

For a one-off top-k, heapq has it built in, and takes a key just like sorted:

python

The first line gives the tie to whichever title came first. The second uses (-plays, title) to settle ties alphabetically.

A k-way merge combines k sorted lists. Put each list's first item in a heap, pop the smallest, then push the next item from the list it came from:

python

The heap never holds more than k entries, so each step is O(log k). The same loop drives job schedulers, event simulations, and Dijkstra's shortest paths in the graphs section.

⚡ Advanced — payloads that can't be compared

When two priorities tie, Python compares the next tuple element, and a dict there raises TypeError. Put a counter in between so a comparison never reaches the payload:

python

⚡ Advanced — changing a priority

A heap can't find an item quickly, so you can't edit a play count in place. The standard trick is lazy deletion: push the updated entry, and when a stale one reaches the top, recognise it and throw it away.


🎯 Your turn

Write top_charts(plays, k). plays is the stream of play events, one title per play. Count them, then use a heap to return the k most-played titles, most plays first, ties broken alphabetically:

  • top_charts(["Tides", "Neon", "Tides", "Glass", "Neon", "Tides"], 2)["Tides", "Neon"]
  • top_charts([], 3)[]

Hint — count with a dict, heapify a list of (-count, title) pairs, and pop at most k times.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upIs this list a valid heap?

Write is_min_heap(values) returning True if the list is a valid min-heap: for every index i, the children at 2 * i + 1 and 2 * i + 2 (where they exist) are not smaller than values[i]. Empty and one-item lists are valid.

heapindex
DrillSift a new play count up

Write heap_push(heap, value) that adds value to the min-heap heap by hand — no heapq — and returns the resulting list. Append it at the end, then swap it with its parent at (i - 1) // 2 while the parent is strictly bigger.

heap
BuildThe shortest tracks across playlists

Write shortest_across(playlists, n). Each playlist is a list of [seconds, title] pairs already sorted by seconds, then title. Return the titles of the n shortest tracks across all playlists, shortest first, ties broken alphabetically by title. Use a heap holding one entry per playlist, and stop as soon as you have n.

k-way-mergeheapq
BossPeak simultaneous streams

Write peak_streams(sessions). Each session is [start, end] in seconds; a stream is playing from start up to, but not including, end. Return the largest number of streams playing at the same moment, so Tunebox knows how much capacity to provision. No sessions → 0.

priority-queuesorted
CapstoneThe median play count, live

Write running_medians(stream) returning, after each new play count arrives, the median of all counts so far. With an odd number of counts the median is the middle one; with an even number it is the average of the two middle ones. Aim for O(log n) per count — re-sorting each time is too slow at Tunebox's scale.

heapqpriority-queue
top_charts.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
top_charts(plays, k) → listThe k most-played titles, most plays first, ties alphabetical.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.