Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Linked Lists, Stacks & Queues  ›  Lesson

Queues, Deques & Up-Next

Queues 22 minFirst in, first out, with O(1) work at both ends
You're building a piece ofTunebox — the player
This piece — up_next(): The first-in, first-out queue of tracks waiting to play.
Scenario You queue Chorus behind Intro and Verse, then tap "play next" on a friend's track. Tunebox's up-next queue serves tracks in arrival order, but lets "play next" jump to the front.
Your task
Build up_next(queue, ops). Start from queue (front first) and replay each op: ["add", title] enqueues title at the back, ["next", title] puts title at the front ("play next"), and ["play"] dequeues the front track (doing nothing if the queue is empty). Return what's still waiting, front first. Example: up_next(["A", "B"], [["add", "C"], ["play"]]) → ["B", "C"].

Queues, Deques & Up-Next

Queue three tracks in Tunebox and they play in the order you queued them: first in, first out. That structure is a queue. The obvious Python version, a list you pop(0) from, gets slower with every track waiting in line, so this lesson replaces it with collections.deque, which adds and removes at either end in O(1).

First in, first out

A queue has two ends. Enqueue adds at the back and dequeue removes from the front, so whatever has waited longest is served next. That's FIFO, the opposite of a stack.

python

The answer is right. The problem is what pop(0) has to do to produce it.

Why list.pop(0) is O(n)

A list's items sit in consecutive slots, and slot 0 must always hold the first item. Remove it and every remaining item moves one slot to the left:

What does this print?
queue = ["A", "B", "C"]
first = queue.pop(0)
print(queue.index("C"))

With 100,000 tracks waiting, one dequeue moves 99,999 items. Serving the whole queue that way moves about n²/2 items, around five billion here.

Coming from Java/JS: JS array.shift() has the same O(n) cost, and so does Java's ArrayList.remove(0). Java's fix is ArrayDeque with offer and poll. Python's fix comes next.

collections.deque: O(1) at both ends

A deque (said "deck", short for double-ended queue) is built for exactly this. It stores items in a chain of fixed-size blocks, a linked list of small arrays, so neither end ever has to shift anything:

python

The four method names pair up. append and pop work on the right end, and appendleft and popleft work on the left:

What does this print?
from collections import deque
q = deque(["B"])
q.append("C")
q.appendleft("A")
q.pop()
print(list(q))

Mix the two ends up and you've built a stack by accident:

broken — fix it

Tracks should play in the order they were queued, printing ['A', 'B', 'C']. It prints them backwards.

❓ Cross-question — "So why not use a deque for everything?" Fast ends come at the cost of a slow middle. q[i] is O(n) near the middle of a deque, where a list is O(1), and a deque can't be sliced. Use a list when you index into it and a deque when you work at the ends.

An empty deque raises IndexError on popleft(), just as an empty list does on pop(), which is why queue loops are written while q:.

The up-next loop

Queues are most useful when serving one item can add more items. Here, playing Intro adds a Chorus behind whatever is already waiting. Step through it and watch the order:

step through it
1from collections import deque
2up_next = deque(["Intro"])
3up_next.append("Verse")
4played = []
5while up_next:
6 track = up_next.popleft()
7 played.append(track)
8 if track == "Intro":
9 up_next.append("Chorus")
10print(played)

Chorus was added while Verse was already waiting, so Verse plays first. Nothing jumps the line in a queue.

Ring buffers

Tunebox shows your last three plays. A ring buffer stores them in a fixed-size array. A start index marks the oldest item, a new item overwrites the oldest once the array is full, and every index wraps around with % capacity. Nothing ever shifts or grows:

python

In everyday Python you won't write that class, because deque(maxlen=n) behaves the same way:

What does this print?
from collections import deque
recent = deque(maxlen=3)
for track in ["A", "B", "C", "D", "E"]:
    recent.append(track)
print(list(recent))

What it costs

operation list deque
add at the back: append O(1) amortized O(1)
remove from the front: pop(0) / popleft() O(n) O(1)
add at the front: insert(0, x) / appendleft O(n) O(1)
remove from the back: pop() O(1) O(1)
read q[0] or q[-1] O(1) O(1)
read q[i] in the middle O(1) O(n)
length: len(q) O(1) O(1)

Idioms & real-world patterns

  • q = deque(items) and while q: item = q.popleft() is the queue loop you'll write most often.
  • deque(maxlen=n) keeps the last n of anything: recent plays, log lines, a moving window.
  • q.rotate(k) moves k items from the back to the front, and a negative k goes the other way. It's a one-line way to take turns.
  • For threads, use queue.Queue, which adds locking around the same idea. Async code uses asyncio.Queue.
  • Where queues live: print spoolers, web-server request backlogs, job queues such as SQS and Celery, network packet buffers, and breadth-first search.

⚡ Advanced — the monotonic deque: sliding-window maximum

Tunebox's volume normaliser needs the loudest level in every window of k consecutive tracks. Rescanning each window costs O(n·k). Instead, keep a deque of indices whose levels decrease from front to back:

  • a new level removes every smaller-or-equal level from the back, because those can never be the loudest while the new one is still in the window;
  • an index that has slid out of the window is dropped from the front;
  • after both steps, the front is always the loudest level in the current window.
python

Each index enters the deque once and leaves at most once, so this is O(n) whatever k is. It's the queue version of the monotonic stack from the last lesson, and the extra front end is what lets old items expire.

⚡ Advanced — a preview: queues drive breadth-first search

"Fans of this track also like..." is a graph. Finding everything within a few hops of a track means exploring the nearest tracks first, and a queue guarantees that: everything one hop away is served before anything two hops away. Section 5 builds this properly. Here's the basic loop:

python

Swap popleft() for pop() and the same loop explores depth-first, the stack version.


🎯 Your turn

Build up_next(queue, ops), Tunebox's up-next queue. Start from queue (front first) and replay each op:

  • ["add", title] enqueues title at the back.
  • ["next", title] is "play next": it puts title at the front.
  • ["play"] dequeues the front track, and does nothing if the queue is empty.

Return what's still waiting, front first:

  • up_next(["A", "B"], [["add", "C"], ["play"]])["B", "C"]
  • up_next(["A", "B"], [["next", "X"], ["add", "Z"]])["X", "A", "B", "Z"]

Hint: start with q = deque(queue), use append, appendleft and popleft, and finish with return list(q).

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of the app 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-upThe last k plays

Write recent_plays(plays, k) returning the last k tracks played, oldest first, using a deque with maxlen=k. If fewer than k tracks were played, return all of them. k is never negative. recent_plays(["A", "B", "C", "D"], 2)["C", "D"].

deque
DrillStreams in the last window

Tunebox's live counter shows how many streams started in the last window seconds. times lists stream start times in increasing order. For each time t, return how many start times fall within [t - window, t], inclusive. Keep a queue of recent times and drop stale ones from the front. streams_in_window([1, 100, 3001, 3002], 3000)[1, 2, 3, 3].

queuesliding-window
BuildParty mode turns

In party mode every guest has their own queue, and guests take turns: one track from each guest who still has tracks, in guest order, round after round. Write round_robin(queues) returning the combined play order. round_robin([["a1", "a2", "a3"], ["b1"], ["c1", "c2"]])["a1", "b1", "c1", "a2", "c2", "a3"].

queuedeque
BossThe deep-cut badge

Tunebox's deep-cut badge highlights the earliest-played track in a session that has been played exactly once so far. Write first_unique(plays) returning, after each play, the track holding the badge, or None if no track has been played exactly once. Aim for O(n) overall. first_unique(["A", "B", "A", "C", "B"])["A", "A", "B", "B", "C"].

queuehash-map
CapstoneThe longest steady stretch

A steady stretch of a playlist is a run of consecutive tracks whose loudest and quietest levels differ by at most limit. Write longest_steady_run(levels, limit) returning the length of the longest steady stretch (0 for an empty playlist), in O(n). longest_steady_run([10, 1, 2, 4, 7, 2], 5)4, the stretch [2, 4, 7, 2].

monotonic-dequesliding-window
up_next.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
up_next(queue, ops) → listThe up-next queue after every op, front first.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.