Coding Interview Patterns Module 8 – Stacks and monotonic stacks
Stack patterns: nesting and undo
Spot the problems a stack solves, from nesting to undo, choose the stack type of five languages, and simplify a file path from brute force to tested code.
What you will learn
- Recognise problems in which the most recent unfinished item decides what happens next
- Use a stack for nesting, for undo and for items that wait until a later item settles them
- Pick the stack type of Python, JavaScript, Java, C++ and Go, and avoid the trap of each
- Simplify a file path with a stack, argue why it is correct and test it against a brute force
Before you start
What you’ll learn · Before you start
What you will learn
- Recognise problems in which the most recent unfinished item decides what happens next
- Use a stack for nesting, for undo and for items that wait until a later item settles them
- Pick the stack type of Python, JavaScript, Java, C++ and Go, and avoid the trap of each
- Simplify a file path with a stack, argue why it is correct and test it against a brute force
Before you start
On this page
A stack hands items back in the reverse of the order they arrived: the last one pushed is the first one popped. That
one rule fits every problem in which the most recent unfinished item decides what happens next. A closing bracket
must match the newest bracket that is still open. A .. in a file path leaves the folder you entered last. An undo
reverses the latest change that is still in effect. In each case the newest item waits on top, older items wait
below it, and each new item either joins the pile or settles the top. This lesson shows how to recognise these
problems and which stack type to use in Python, JavaScript, Java, C++ and Go. Then it solves one problem in full,
simplifying a path typed into a terminal, from a brute force to tested code in four languages, and ends with an undo
stack for a small editor.
When a problem wants a stack
Look for these cues in a problem statement. Each one says that an item waits until a later item settles it, and that the newest waiting item is always settled first:
| Cue in the problem | What waits on the stack | What settles the top |
|---|---|---|
Brackets, tags, begin and end |
each opener not closed yet | its closer, which must match it |
| Nesting: folders, calls, nested data | each level you are inside | leaving that level: .., a return, a ] |
“Cancel the previous one”: backspace, undo, .. |
each item or change still in effect | the cancel, which removes it |
| “The nearest earlier item that is larger” (or smaller) | items whose answer is not known yet | the first later item that answers them |
- Brackets are the clearest case. An opener stays on the stack until a closer arrives, and a closer that does not match the top means the text is unbalanced.
- Nesting is the same rule under another name. You leave levels in the reverse order you entered them, so the level you are in is always the top.
- Cancelling removes the most recent item that is still standing, never an older one.
- “The nearest earlier item that is larger” is the subtlest cue. Items wait on the stack until a later item answers a question about them, such as “on which later day is it warmer?”. Such a stack stays sorted from bottom to top, which gives it its name, a monotonic stack, and the later lessons of this module are built on it.
A shop’s billing keypad shows the third cue in a few lines: its < key deletes the digit typed just before it.
# A shop's billing keypad: "<" deletes the digit typed just before it, if there is one.
def entered(keys):
digits = [] # a stack: the newest digit is at the end
for key in keys:
if key == "<":
if digits:
digits.pop() # cancel the most recent digit that is still standing
else:
digits.append(key)
return "".join(digits)
print(entered("98<76<<5")) Output
95
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keypad.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each < removed the newest digit that was still there, and a < with nothing left to delete would be ignored, which
is why the code checks if digits before it pops.
A stack is the wrong tool when the oldest item must go first (a queue), when the largest or most urgent item must go first (a heap), or when items are looked up by a key (a hash map). The self-check at the end of this lesson mixes all four.
Stacks in five languages
Every language in this track has a ready-made stack. Here is the type to use in each, its three operations (push, look at the top, pop), and what happens when you pop an empty stack, which is where the five differ most:
- Python: a
list. Push withs.append(x), look at the top withs[-1], pop withs.pop(). Popping an empty list raisesIndexError. - JavaScript: an
Array. Push withs.push(x), look withs.at(-1), pop withs.pop(). Popping an empty array returnsundefined, with no error. - Java: an
ArrayDeque, used through theDequeinterface. Push withs.addLast(x), look withs.peekLast(), pop withs.pollLast(). On an empty dequepollLast()returnsnull, whilepop()throwsNoSuchElementException. - C++: a
std::vector. Push withs.push_back(x), look withs.back(), pop withs.pop_back(). Popping an empty vector is undefined behaviour, so checks.empty()first. - Go: a slice,
[]T. Push withs = append(s, x), look withs[len(s)-1], pop withs = s[:len(s)-1]. On an empty slice, looking and popping both cause a run-time panic.
Python, Java and C++ document pushing and popping at the end as O(1) amortised: now and then the array behind the
stack moves to a bigger block of memory, and that cost is spread over all the pushes. The JavaScript and Go
specifications promise no cost at all. Python’s table of costs also says that removing the first element, pop(0),
is O(n), because every other element moves, so a stack works at the end, never at the front.
Here are Python and JavaScript side by side, with one pop too many:
Python · stacks/stack_ops.py
# Push, look at the top, pop, and pop once too often: a Python list used as a stack.
stack = []
stack.append("home") # push
stack.append("asha")
print(stack[-1]) # the top, without removing it: asha
print(stack.pop()) # remove the top and return it: asha
print(stack, len(stack))
stack.pop()
try:
stack.pop() # the stack is empty now
except IndexError as error:
print("IndexError:", error) Output
asha asha ['home'] 1 IndexError: pop from empty list
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stack_ops.py
JavaScript · stacks/stack_ops.mjs
// Push, look at the top, pop, and pop once too often: a JavaScript array used as a stack.
const stack = [];
stack.push('home'); // push
stack.push('asha');
console.log(stack.at(-1)); // the top, without removing it: asha
console.log(stack.pop()); // remove the top and return it: asha
console.log(JSON.stringify(stack), stack.length);
stack.pop();
console.log(stack.pop()); // the stack is empty now: no error, just undefined Output
asha asha ["home"] 1 undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node stack_ops.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Only Python stopped at the extra pop. JavaScript returned undefined, which is harmless here and a bug as soon as the
value is used: undefined + 1 is NaN, with no error. Three more traps cost points in coding rounds:
- Java: which end you push to.
push()adds at the front of a deque, and its iterator, whichString.joinand printing use, reads from front to back, so names pushed withpush()come out in reverse. The Java run below shows it. When the stack must be read in order, add withaddLast()and remove withpollLast(). The oldjava.util.Stackclass still exists, but it extendsVector, a synchronized list, and its documentation recommends aDequein its place.ArrayDequealso refusesnullelements. - C++:
std::stackhides everything below the top. Its operations reach only the top (push,pop,top), apart fromemptyandsize, and it has no way to walk its elements. When the answer needs the whole stack in order, as a path does, use astd::vector. - Go: keep what
appendreturns. When the slice is full,appendmoves the elements to a new, larger array and returns a slice of that array, sos = append(s, x)is the only safe way to push.
A worked problem: simplifying a terminal path
The problem
A file manager shows where a typed path leads. Given an absolute path, one that starts with /, return its simplest
form: folder names separated by single slashes, no . (this folder) or .. (the parent folder) left, and no slash
at the end. The path has at most 100,000 characters.
Before planning, settle the questions whose answers change the code. Here are the answers this problem gives:
- What does
..at the root do? It stays at the root. POSIX, the standard that Unix-like systems such as Linux and macOS follow, allows the root’s parent to be the root itself. - Is
...special? No. Only.and..are;...,.envanda..bare ordinary folder names. - What about repeated slashes?
//counts as one/, at the start too. POSIX lets a system give a path that begins with exactly two slashes a meaning of its own; this problem does not. - Should symbolic links be followed? No: simplify the text alone. A shell’s
cddoes the same by default, which POSIX calls handling dot-dot logically;cd -Presolves the links first. - What form does the answer take? It starts with
/and never ends with one, except the root itself,/.
Work four examples by hand, and keep them as tests: /home/asha/../ravi/./notes/ leads to /home/ravi/notes;
/../ to /; /srv//logs/.../ to /srv/logs/...; and /a/b/../../.. to /.
The brute force
The rule for one .. is easy to state: it deletes itself and the folder name just before it. So repeat that until no
.. is left. Split the path at its slashes, drop the empty parts and the .s, find the first .., delete it with
the name before it (or alone, at the root), and start again:
"""The brute force: delete the first ".." together with the folder name just before it, and repeat."""
def simplify_brute(path, cost=None):
names = [name for name in path.split("/") if name not in ("", ".")]
while ".." in names:
i = names.index("..")
start = i - 1 if i > 0 else 0 # a ".." at the front stands at the root: there is no name before it
names = names[:start] + names[i + 1 :]
if cost is not None:
cost["rounds"] += 1
cost["copied"] += len(names) # each round builds a new list out of the names that stay
return "/" + "/".join(names)
if __name__ == "__main__":
for path in ["/home/asha/../ravi/./notes/", "/../", "/srv//logs/.../", "/a/b/../../.."]:
print(f'"{path}" -> "{simplify_brute(path)}"')
print()
print("characters rounds names copied")
for depth in [200, 400, 800, 1600]:
path = "/d" * depth + "/.." * depth # down `depth` folders, then all the way back up
cost = {"rounds": 0, "copied": 0}
simplify_brute(path, cost)
print(f"{len(path):>10,} {cost['rounds']:>6,} {cost['copied']:>12,}") Output
"/home/asha/../ravi/./notes/" -> "/home/ravi/notes"
"/../" -> "/"
"/srv//logs/.../" -> "/srv/logs/..."
"/a/b/../../.." -> "/"
characters rounds names copied
1,000 200 39,800
2,000 400 159,600
4,000 800 639,200
8,000 1,600 2,558,400
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
It gets every example right. The table under the answers shows its cost on a path that hurts it, one that goes k
folders down and then climbs straight back up. Every round deletes one pair and copies the rest of the list, so the
names copied add up to k² − k, and doubling the path’s length roughly multiplies them by four. At the limit of 100,000
characters (k = 20,000) that comes to about 400 million copies, with about as many comparisons to find each ..:
ten thousand times the work of the one-pass method below.
What to notice
Watch which name each .. deletes. It is not simply the name before it in the original path: in /a/b/../../.. the
second .. deletes a, because b is already gone. A .. always cancels the most recent name that is still
standing. That is the stack cue from the first section, so keep the names that are still standing on a stack, and
let each .. pop the top.
One pass with a stack
Read the parts between the slashes from left to right. An empty part (from //, or before the first slash or after
the last) and . change nothing; .. pops the top, if there is one; every other part is pushed. At the end the
answer is a slash followed by the names on the stack, joined with slashes.
The stack is the folder you are in
Text description of the diagram
The diagram shows the stack after each part of the path /home/asha/../ravi/./notes/, from top to bottom. The newest name on the stack, its top, is drawn with a thick border.
- At the start the stack is empty: the folder is the root, /.
- The part home is pushed: the stack holds home, the folder /home.
- The part asha is pushed: the stack holds home and asha, the folder /home/asha.
- The part .. pops asha: the stack holds home again, the folder /home.
- The part ravi is pushed: the stack holds home and ravi, the folder /home/ravi.
- The part . changes nothing, and notes is pushed: the stack holds home, ravi and notes, the folder /home/ravi/notes.
The empty parts before the first slash and after the last one change nothing and are not drawn. At every step, a slash followed by the names on the stack joined with slashes is the folder the parts so far lead to, so the last box is the answer.
The proof rests on one statement that stays true after every part: the stack holds the folders of the path read
so far, from the root down. It is true at the start, when the stack is empty and the path so far leads to
the root. A name moves one folder down, which is one more name on the stack. A .. moves one folder up, which drops
the last name, or stays at the root when there is no name to drop. A . and an empty part stay where they are. So
after the last part, the stack spells the answer.
Each part costs at most one push or one pop, so the pass takes O(n) time for a path of n characters, and the stack and the answer take O(n) memory. On the deep path from the brute force’s table, that means 400 pushes and pops at 1,000 characters, against 39,800 names copied, and 40,000 at the full 100,000 characters.
| Operation | Time | Extra space |
|---|---|---|
| Brute force: delete the first .. and repeat | O(n²) | O(n) |
| One pass with a stack | O(n) | O(n) |
The same pass in four languages
Python · path/simplify.py
"""Simplify an absolute path with a stack of folder names."""
def simplify_path(path):
stack = [] # the folders of the path read so far, from the root down
for name in path.split("/"):
if name in ("", "."):
continue # a repeated or trailing "/", or "this folder": nothing changes
if name == "..":
if stack:
stack.pop() # back to the parent: drop the most recent folder
else:
stack.append(name) # into a folder; "..." and ".env" are names too
return "/" + "/".join(stack)
if __name__ == "__main__":
for path in ["/home/asha/../ravi/./notes/", "/../", "/srv//logs/.../", "/a/b/../../..", "/.config/../.local/share/"]:
print(f'"{path}" -> "{simplify_path(path)}"') Output
"/home/asha/../ravi/./notes/" -> "/home/ravi/notes" "/../" -> "/" "/srv//logs/.../" -> "/srv/logs/..." "/a/b/../../.." -> "/" "/.config/../.local/share/" -> "/.local/share"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 simplify.py
JavaScript · path/simplify.mjs
// Simplify an absolute path with a stack of folder names.
function simplifyPath(path) {
const stack = []; // the folders of the path read so far, from the root down
for (const name of path.split('/')) {
if (name === '' || name === '.') continue; // a repeated or trailing "/", or "this folder"
if (name === '..') {
if (stack.length > 0) stack.pop(); // back to the parent: drop the most recent folder
} else {
stack.push(name); // into a folder; "..." and ".env" are names too
}
}
return '/' + stack.join('/');
}
for (const path of ['/home/asha/../ravi/./notes/', '/../', '/srv//logs/.../', '/a/b/../../..', '/.config/../.local/share/']) {
console.log(`"${path}" -> "${simplifyPath(path)}"`);
} Output
"/home/asha/../ravi/./notes/" -> "/home/ravi/notes" "/../" -> "/" "/srv//logs/.../" -> "/srv/logs/..." "/a/b/../../.." -> "/" "/.config/../.local/share/" -> "/.local/share"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node simplify.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Java · compiled/PathSimplifier.java
import java.util.ArrayDeque;
import java.util.Deque;
// Simplify an absolute path with a stack of folder names.
public class PathSimplifier {
static String simplify(String path) {
Deque<String> stack = new ArrayDeque<>(); // the folders read so far: first is nearest the root, last is the top
for (String name : path.split("/")) {
if (name.isEmpty() || name.equals(".")) continue; // a repeated or trailing "/", or "this folder"
if (name.equals("..")) {
stack.pollLast(); // back to the parent; on an empty deque pollLast() just returns null
} else {
stack.addLast(name); // into a folder; "..." and ".env" are names too
}
}
return "/" + String.join("/", stack); // a deque is read from first to last: the path's own order
}
public static void main(String[] args) {
String[] paths = {"/home/asha/../ravi/./notes/", "/../", "/srv//logs/.../", "/a/b/../../..", "/.config/../.local/share/"};
for (String path : paths) {
System.out.println("\"" + path + "\" -> \"" + simplify(path) + "\"");
}
// The trap: push() adds at the front, and reading starts at the front, so the folders come out reversed.
Deque<String> pushed = new ArrayDeque<>();
for (String name : new String[] {"home", "ravi", "notes"}) pushed.push(name);
System.out.println("joined after push(): /" + String.join("/", pushed));
}
} C++ · compiled/simplify.cpp
// Simplify an absolute path with a stack of folder names (C++23).
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
std::string simplify(const std::string& path) {
std::vector<std::string> stack; // the folders read so far; back() is the top
std::istringstream parts(path);
std::string name;
while (std::getline(parts, name, '/')) {
if (name.empty() || name == ".") continue; // a repeated or trailing "/", or "this folder"
if (name == "..") {
if (!stack.empty()) stack.pop_back(); // pop_back() on an empty vector is undefined behaviour
} else {
stack.push_back(name); // into a folder; "..." and ".env" are names too
}
}
std::string out;
for (const std::string& folder : stack) out += "/" + folder;
return out.empty() ? "/" : out;
}
int main() {
for (const std::string path : {"/home/asha/../ravi/./notes/", "/../", "/srv//logs/.../", "/a/b/../../..", "/.config/../.local/share/"}) {
std::cout << '"' << path << "\" -> \"" << simplify(path) << "\"\n";
}
} Build and run both · compiled/build_and_run.py
"""Compiles and runs the Java and the C++ version, showing each command and what it printed."""
from terminal import session
session("""
java --version | head -n 1 | cut -d ' ' -f 1,2
javac --release 25 PathSimplifier.java
java PathSimplifier
clang++ --version | head -n 1
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined simplify.cpp -o simplify
./simplify
""") Output
$ java --version | head -n 1 | cut -d ' ' -f 1,2 openjdk 27 $ javac --release 25 PathSimplifier.java $ java PathSimplifier "/home/asha/../ravi/./notes/" -> "/home/ravi/notes" "/../" -> "/" "/srv//logs/.../" -> "/srv/logs/..." "/a/b/../../.." -> "/" "/.config/../.local/share/" -> "/.local/share" joined after push(): /notes/ravi/home $ clang++ --version | head -n 1 Apple clang version 21.0.0 (clang-2100.3.34.2) $ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined simplify.cpp -o simplify $ ./simplify "/home/asha/../ravi/./notes/" -> "/home/ravi/notes" "/../" -> "/" "/srv//logs/.../" -> "/srv/logs/..." "/a/b/../../.." -> "/" "/.config/../.local/share/" -> "/.local/share"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_and_run.py
All four print the same answers for the same five paths. The Java version is compiled with javac --release 25,
which checks it against the Java 25 platform, although the JDK that ran it is a newer one, version 27. The last line
of its run is the trap from the language section: the same three names, pushed with push(), come out as
/notes/ravi/home. The C++ version is built with AddressSanitizer and UndefinedBehaviorSanitizer, which report bad
memory accesses and undefined behaviour while the program runs; both stayed silent. In Go the same pass keeps a
[]string, appends each name and drops the last one with a slice expression, as shown for Go above.
Checking it against the brute force
The brute force is slow but easy to trust, which makes it the right judge for the fast version:
"""Checks the stack version against the brute force on random paths (a fixed seed: every run tests the same paths)."""
import random
from brute import simplify_brute
from simplify import simplify_path
PARTS = ["", ".", "..", "...", "a", "b", ".env"] # every kind of part, the awkward ones included
rng = random.Random(35)
paths = ["/" + "/".join(rng.choice(PARTS) for _ in range(rng.randint(0, 10))) for _ in range(3000)]
for path in paths[:4]:
print(f'"{path}" -> "{simplify_path(path)}"')
disagreements = [p for p in paths if simplify_path(p) != simplify_brute(p)]
print(f"{len(paths):,} random paths, {len(disagreements)} disagreements")
for path in disagreements[:3]: # the first few, if there are any, are a ready-made bug report
print(f' "{path}": stack "{simplify_path(path)}", brute force "{simplify_brute(path)}"') Output
"/../.env/./b/../.env/./.." -> "/.env" "/../a//b/a/.env" -> "/a/b/a/.env" "/b/../.env/b" -> "/.env/b" "/.env//..//a///b/..." -> "/a/b/..." 3,000 random paths, 0 disagreements
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stress.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The random paths mix every awkward part: //, ., .. at the root, ... and .env. The seed is fixed, so every
run tests the same 3,000 paths, and the first four are printed so that you can check them by hand. Not one
disagreement turned up. Had there been any, the program would have printed the first three, each a ready-made bug
report: the path, the stack’s answer and the brute force’s.
Why not call a library?
Python and Node.js both come with a path normaliser, and in real code you might call one. In a coding round, ask first: the interviewer usually wants to see the stack, and the libraries follow rules of their own.
Python · libraries/normpath.py
"""What Python's own normaliser makes of four paths (posixpath: the POSIX rules, on any operating system)."""
import posixpath
THE_RULES = { # path -> the answer under this lesson's rules
"/home/asha/../ravi/./notes/": "/home/ravi/notes",
"/srv//logs/.../": "/srv/logs/...",
"//srv/logs": "/srv/logs",
"/../": "/",
}
for path, rule in THE_RULES.items():
got = posixpath.normpath(path)
note = "same" if got == rule else f'differs: the rules give "{rule}"'
print(f'"{path}" -> "{got}" ({note})') Output
"/home/asha/../ravi/./notes/" -> "/home/ravi/notes" (same) "/srv//logs/.../" -> "/srv/logs/..." (same) "//srv/logs" -> "//srv/logs" (differs: the rules give "/srv/logs") "/../" -> "/" (same)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 normpath.py
JavaScript · libraries/normalize.mjs
// What Node.js's own normaliser makes of four paths (path.posix: the POSIX rules, on any operating system).
import { posix } from 'node:path';
const theRules = new Map([ // path -> the answer under this lesson's rules
['/home/asha/../ravi/./notes/', '/home/ravi/notes'],
['/srv//logs/.../', '/srv/logs/...'],
['//srv/logs', '/srv/logs'],
['/../', '/'],
]);
for (const [path, rule] of theRules) {
const got = posix.normalize(path);
const note = got === rule ? 'same' : `differs: the rules give "${rule}"`;
console.log(`"${path}" -> "${got}" (${note})`);
} Output
"/home/asha/../ravi/./notes/" -> "/home/ravi/notes/" (differs: the rules give "/home/ravi/notes") "/srv//logs/.../" -> "/srv/logs/.../" (differs: the rules give "/srv/logs/...") "//srv/logs" -> "/srv/logs" (same) "/../" -> "/" (same)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node normalize.mjs
Each one differs from this problem’s rules on an edge case of its own. Python keeps exactly two leading slashes, because POSIX lets a system give such a path a meaning of its own. Node.js keeps a trailing slash, and its documentation says so. Both, like the stack, work on the text alone, and Python’s documentation warns that this can change the meaning of a path that contains symbolic links.
Undo: a stack of changes
Undo is “cancel the previous one” applied to changes instead of characters. Each change pushes what it would take to
reverse it, and an undo pops the newest entry and applies it. This editor knows three commands: type <text>,
delete <count> (characters from the end) and undo.
"""A tiny editor with type, delete and undo: every change pushes what it takes to reverse it."""
def run(commands):
text = [] # the document, one character per item
undo = [] # one entry per change still in effect, the most recent last
for command in commands:
action, _, argument = command.partition(" ")
if action == "type":
text.extend(argument)
undo.append(("typed", len(argument))) # to reverse it: delete that many characters
elif action == "delete":
count = min(int(argument), len(text)) # there may be fewer characters than asked for
removed = "".join(text[len(text) - count :])
del text[len(text) - count :]
undo.append(("deleted", removed)) # to reverse it: type the same characters again
elif action == "undo" and undo: # with nothing left to undo, undo does nothing
kind, value = undo.pop()
if kind == "typed":
del text[len(text) - value :]
else:
text.extend(value)
print(f"{command:<17} {''.join(text)!r}")
run(["type masala chai", "delete 4", "type coffee", "undo", "undo", "delete 50", "undo", "undo", "undo"]) Output
type masala chai 'masala chai' delete 4 'masala ' type coffee 'masala coffee' undo 'masala ' undo 'masala chai' delete 50 '' undo 'masala chai' undo '' undo ''
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 undo.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Three details keep it correct:
- An entry holds only what reversing needs: how many characters were typed, or exactly which characters were deleted. The document itself is never copied, so an undo costs time in proportion to the change, not to the document.
- An entry records what really happened.
delete 50on an 11-character text deletes 11, and the entry stores those 11, so undoing it restores exactlymasala chai. - An undo with nothing to undo does nothing, as the last command shows. Without the check,
pop()on the empty list would stop the program with anIndexError.
The same shape, push what reverses a step and pop it to go back, is how backtracking takes back a choice before it tries the next one, in a later module of this track.
Python Online Compiler Paste the editor and try commands of your own, such as an undo right after deleting more characters than the text has. JavaScript & TypeScript Online Compiler Run the JavaScript path simplifier on paths of your own, with repeated slashes, dots and a few more .. than folders.Interview questions
Warm-up (fresher to mid-level): How do you recognise that a coding problem wants a stack?
Look for a rule in which the newest unfinished item decides what happens next. Brackets and tags are the clearest
case: a closer must match the most recent opener that is still open. Nested structures behave the same way, whether
folders, nested calls or nested data, because you leave levels in the reverse order you entered them. “Cancel the
previous one” is another sign: a backspace, an undo or a .. removes the most recent item still standing. And “the
nearest earlier item that is larger” calls for a stack of items still waiting for their answer, a monotonic stack. If
instead the oldest item must go first, use a queue; if the largest or most urgent, a heap.
Warm-up (fresher to mid-level): In Java, should a stack be a java.util.Stack or an ArrayDeque, and does it
matter which end you push to?
Use an ArrayDeque through the Deque interface. Stack is a legacy class that extends Vector, a synchronized
list, and its own documentation recommends a Deque instead; the ArrayDeque documentation adds that it is likely
to beat Stack at the job. The end matters when you read the stack. push() adds at the front, and a deque’s
iterator reads from front to back, so after pushing home, ravi and notes, String.join gives notes/ravi/home. To
read a stack in the order it was built, as when folder names become a path, add with addLast() and remove with
pollLast(). Remember too that ArrayDeque refuses null elements.
Key takeaways
- Reach for a stack when the most recent unfinished item decides what happens next: brackets and tags, nesting, “cancel the previous one” and “the nearest earlier item that is larger”.
- Use a
listin Python, anArrayin JavaScript, anArrayDequein Java (withaddLastandpollLastwhen order matters), astd::vectorin C++ and a slice in Go. Check for an empty stack before you pop: the five languages answer an empty pop with anything from an exception to undefined behaviour. - A path simplifies in one pass: skip empty parts and
., pop on.., push everything else. The stack is always the folder you are in. - The brute force, deleting the first
..and repeating, is O(n²). Keep it as the judge in a stress test of the O(n) stack. - An undo stack stores just enough to reverse each change, records what really happened, and ignores an undo when it is empty.
Exercise
Exercise · Easy · Python
Follow a cd command from the current folder
A terminal keeps its current folder as a simplified absolute path, such as /home/asha. Write change_dir(current, typed), which returns the folder the terminal is in after the command cd typed.
The rules are the ones from this lesson, with one addition:
- The current folder,
current, is already simplified: it starts with/and has no.,.., repeated/or trailing/. - What was typed,
typed, is never empty. If it starts with/, it is an absolute path and you start from the root. Otherwise it is relative, and you start fromcurrent. .is this folder and..its parent;..at the root stays at the root. Repeated/count as one, a trailing/changes nothing, and every other part is a folder name,...and.envincluded.- Return a simplified absolute path. Work on the text only: do not check whether the folders exist.
change_dir("/home/asha", "../ravi/notes/") # "/home/ravi/notes"
change_dir("/home/asha", "/../") # "/"
change_dir("/", "...//x") # "/.../x"
Work one more example by hand before you code: a relative path with more .. than current has folders.
Starter code · change_dir.py
def change_dir(current, typed):
"""Return the folder a terminal is in after `cd typed`, starting in the folder `current`."""
# Replace this line with your code.
return current The sample tests · test_change_dir.py
from change_dir import change_dir
def test_relative_path():
"""the first example of the prompt: up one folder, then down two"""
assert change_dir("/home/asha", "../ravi/notes/") == "/home/ravi/notes"
def test_absolute_path_starts_at_the_root():
"""an absolute path ignores the current folder"""
assert change_dir("/home/asha", "/srv/logs") == "/srv/logs"
def test_up_from_the_root():
"""'..' at the root stays at the root"""
assert change_dir("/home/asha", "/../") == "/"
def test_more_ups_than_folders():
"""a relative path cannot climb above the root"""
assert change_dir("/home", "../../..") == "/"
def test_dot_stays():
"""'.' is the current folder"""
assert change_dir("/home/asha", ".") == "/home/asha"
def test_repeated_and_trailing_slashes():
"""repeated slashes count as one, and a trailing slash changes nothing"""
assert change_dir("/srv", "logs///") == "/srv/logs"
def test_three_dots_is_a_name():
"""only '.' and '..' are special: '...' is a folder name"""
assert change_dir("/", "...//x") == "/.../x"
def test_hidden_folders():
"""names that start with a dot are ordinary folder names"""
assert change_dir("/home/asha", ".config/../.local") == "/home/asha/.local"
def test_from_the_root():
"""a relative path from the root"""
assert change_dir("/", "notes") == "/notes"
def test_up_then_down_to_the_same_name():
"""leaving a folder and entering it again"""
assert change_dir("/home/asha", "../asha/../asha") == "/home/asha" A hint
The stack is the folder you are in. For a relative path it starts with the folders of current, and for an absolute path it starts empty, at the root. After that, read typed exactly as the lesson reads a path. What should .. do when the stack is already empty?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Python Tutorial: Using Lists as Stacks (Python Software Foundation)
- Time complexity of operations on built-in types (Python Software Foundation)
- os.path: normpath() (Python Software Foundation)
- ECMAScript 2026 Language Specification: Array.prototype.pop (Ecma International)
- Array.prototype.pop() (MDN Web Docs (Mozilla))
- Array.prototype.at() (MDN Web Docs (Mozilla))
- Node.js 24 path module: path.normalize() (OpenJS Foundation)
- Java SE 25 API: ArrayDeque (Oracle)
- Java SE 25 API: Deque (Oracle)
- Java SE 25 API: Stack (Oracle)
- Java SE 25 API: Vector (Oracle)
- The javac command: --release (Oracle)
- C++ working draft N4950: sequence container requirements [sequence.reqmts] (ISO/IEC JTC1/SC22/WG21 (HTML rendering of the working draft))
- C++ working draft N4950: class template stack [stack.defn] (ISO/IEC JTC1/SC22/WG21 (HTML rendering of the working draft))
- AddressSanitizer (The Clang Team (LLVM Project))
- UndefinedBehaviorSanitizer (The Clang Team (LLVM Project))
- The Go Programming Language Specification: Appending to and copying slices (The Go Authors)
- The Go Programming Language Specification: Index expressions (The Go Authors)
- POSIX.1-2024, Shell and Utilities: cd (The Open Group and IEEE)
- POSIX.1-2024, Base Definitions: 4.16 Pathname Resolution (The Open Group and IEEE)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress