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

Stacks & Play History

Stacks 22 minLast in, first out: only the top is reachable
You're building a piece ofTunebox — the player
This piece — undo_history(): The back-button stack of what you played.
Scenario You play Intro, then Verse, then hit back. Tunebox's back button pops Verse off the history stack and leaves Intro on top.
Your task
Build undo_history(actions). Replay a list of actions on a stack: ["play", title] pushes the title, and ["back"] pops the most recent track (doing nothing if the history is empty). Return the history that's left, oldest first. Example: undo_history([["play", "A"], ["play", "B"], ["back"]]) → ["A"].

Stacks & Play History

Every track you play in Tunebox lands on top of the one before it. Press back and the app lifts the top one off, which is always the most recent track and never the first. A structure you may only touch at the top is a stack. That one restriction is what makes undo, bracket matching and even function calls work.

Last in, first out

A stack has three operations. Push puts an item on top, pop takes the top item off, and peek looks at the top without removing it. The rule behind all three is LIFO: last in, first out.

A Python list is already a stack, as long as you only ever use its end:

python
What does this print?
stack = []
for track in ["A", "B", "C"]:
    stack.append(track)
stack.pop()
stack.append("D")
print(stack[-1], len(stack))

❓ Cross-question — "Why the end of the list and not the front?" Because the end is free. append and pop() only touch the last slot, so they're O(1). insert(0, x) and pop(0) shift every other item, which is the O(n) cost from the linked-list lesson. You'd get the same LIFO behaviour at a very different speed.

Coming from Java/JS: JS arrays work the same way with push and pop. In Java, skip the old Stack class (it's built on the synchronised Vector) and use ArrayDeque with push, pop and peek.

Peek, and the empty stack

Pressing back with no history is the classic stack bug:

What happens when this runs?
history = ["Intro"]
history.pop()
print(history.pop())

So guard every pop and peek on a stack that can run empty:

python

Matching brackets

People type Tunebox's smart-playlist rules by hand, for example (genre = jazz or [year < 1970]), so the app checks the brackets before running anything. Every closing bracket must match the most recent opening bracket that is still open, and "most recent" is exactly what a stack gives you. Push each opener. On a closer, pop and compare:

python

Step through the failing case and watch what's on the stack when ) arrives:

step through it
1pairs = {")": "(", "]": "["}
2stack = []
3ok = True
4for ch in "([)]":
5 if ch in "([":
6 stack.append(ch)
7 elif not stack or stack.pop() != pairs[ch]:
8 ok = False
9 break
10print(ok and not stack)

Evaluating an expression

Calculators and virtual machines evaluate postfix notation, also called Reverse Polish notation, where the operator comes after its operands: 3 4 + 2 * means (3 + 4) * 2. You don't need brackets or precedence rules, just a stack. Push each number. When an operator arrives, pop two numbers, apply the operator, and push the result:

python

Most bugs here come from popping the two operands in the wrong order:

broken — fix it

`10 4 -` means 10 - 4, so this should print 6. It prints -6.

Monotonic stacks: the next greater element

For each day's play count, Tunebox's trends page asks: which later count beat it first? The brute-force answer checks every later day for every day, which is O(n²). A monotonic stack does it in one pass. The stack holds days still waiting for a bigger number, and when a big day arrives it answers every smaller day on top of the stack:

python

It stores indices rather than counts, so the answer can be written back to the right position. Each index is pushed once and popped at most once, so the inner while does O(n) work in total and the whole function is O(n), not O(n²).

What does this print?
nums = [5, 3, 4, 6, 2]
stack = []
for i, x in enumerate(nums):
    while stack and nums[stack[-1]] < x:
        stack.pop()
    stack.append(i)
print([nums[i] for i in stack])

The call stack: why recursion is a stack

Python keeps a stack of its own. Calling a function pushes a frame holding its local variables and where to resume, and returning pops that frame. So the most recently called function always finishes first:

python

That means you can rewrite any recursion with a stack of your own. This matters when the input is deep enough to hit Python's limit of about 1000 frames, which raises RecursionError. Here the explicit version counts the tracks in playlists nested inside playlists:

python

❓ Cross-question — "Is the explicit version faster?" Not in any way that matters, since both are O(n). What it gives you is control: there's no depth limit, and you can pause, inspect or reorder the work still waiting. Depth-first search on a graph (Section 5) is this same loop.

What it costs

operation list used as a stack
push: append(x) O(1) amortized
pop: pop() O(1)
peek: stack[-1] O(1)
is it empty? if stack: O(1)
find an item O(n)
push or pop at the front: insert(0, x) / pop(0) O(n), so don't

Idioms & real-world patterns

  • while stack: empties a stack, and stack[-1] if stack else None peeks safely.
  • Push indices when the answer goes back into a position, as next_greater does.
  • Push tuples to carry extra state, such as stack.append((folder, depth)).
  • Undo and redo use two stacks. Undo pops from one and pushes onto the other, and any new action clears the redo stack.
  • Where stacks live: browser back buttons, editor undo, compilers and JSON parsers matching brackets, the CPython and JVM interpreters (both are stack machines), and depth-first search.

⚡ Advanced — why append is only amortized O(1)

A list keeps some spare slots. When they run out, Python allocates a bigger block and copies every item across, which is an O(n) step. The block grows by a fixed proportion each time, so those copies happen rarely enough that n appends cost O(n) in total, or O(1) each on average:

python

⚡ Advanced — a stack that knows its minimum

Say Tunebox wants the quietest track in the history at any moment, in O(1). Keep a second stack whose entries are the minimum so far, and push and pop both stacks together:

python

🎯 Your turn

Build undo_history(actions), Tunebox's play history. Replay each action on a stack:

  • ["play", title] pushes title.
  • ["back"] pops the most recent track, and does nothing if the history is empty.

Return the history that's left, oldest first:

  • undo_history([["play", "A"], ["play", "B"], ["back"]])["A"]
  • undo_history([["back"]])[]

Hint: a list is your stack. Call append on play, and on back call pop() only if stack:.

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-upPress back n times

history is Tunebox's play-history stack, oldest first with the top at the end. Write back_n(history, n): press back n times, where each press pops the top, and return the track that is then on top. Return None if the history runs out. Don't change the caller's list. back_n(["A", "B", "C"], 1)"B".

stacklifo
DrillBackspaces in the search box

Tunebox's search box records raw keystrokes, with # standing for backspace. Write apply_backspaces(typed) returning the text actually left in the box. A backspace on an empty box does nothing. apply_backspaces("jaa#zz")"jazz".

stackstrings
BuildResolve a folder path

Tunebox's file browser accepts typed paths. Write simplify_path(path) that resolves an absolute path: . means stay in the current folder, .. goes up one folder (never above /), and repeated slashes count as one. Return the canonical path, which starts with / and has no trailing slash. simplify_path("/music/jazz/../rock/./live/")"/music/rock/live".

stacksplitjoin
BossHot streaks

For each day, Tunebox's trends page shows a streak: how many consecutive days, ending on that day, had a play count less than or equal to that day's count. Write play_span(counts) returning the streak for every day, in O(n) overall. play_span([100, 80, 60, 70, 60, 75, 85])[1, 1, 1, 2, 1, 4, 6].

monotonic-stack
CapstoneExpand a mix script

Tunebox's mix scripts compress repeats: k[...] means play the bracketed part k times, and brackets can nest. Letters are tracks. Write decode_mix(s) returning the expanded string. k is a positive whole number and may have more than one digit. decode_mix("2[a3[b]]c")"abbbabbbc".

stackstrings
undo_history.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
undo_history(actions) → listThe play-history stack after every action, oldest first.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.