# Lecture 15: Stacks, Queues, and Abstract Data Types ## Choose a structure from the operations the program needs, and what they cost **Instructor:** [Dr. Debasish Pattanayak](https://drdebmath.github.io)
## Where Lecture 14 ended · Insertion at the head is a stack ~~~cpp Node* head = nullptr; for (int reading : {31, 29, 33, 30}) head = new Node{reading, head}; // insert at the head // list: 30 33 29 31 -- the last reading inserted is the first in the list ~~~
head
address
30
33
29
31
nullptr
push = insert at the head
pop = remove the head
top = head->value
the head of the list is the top of the stack
stack, top = 30
30
33
29
31
- Insert at the head and remove from the head: the last item in is the first item out. That is a **stack**, and the list already implements it. - The program using it needs only three operations, not the nodes: `push`, `pop`, `top`. - **Today's question:** which operations does the program need, and what does each cost? Plan: a stack, a queue, the interface that hides the representation, then a cost table.
## Bauer and Samelson · The cellar principle, 1955
- At the Technical University of Munich in 1955, Friedrich L. Bauer (1924–2015) and Klaus Samelson proposed the *Kellerprinzip*, the "cellar principle", for translating formulas. - Reading a formula left to right, an operator that cannot be applied yet waits in the cellar. The last one put in is the first taken out. - They filed a German patent on 30 March 1957 and published "Sequential Formula Translation" in *Communications of the ACM* in 1960.
Friedrich L. Bauer, 1995. Photo: Rolf Kickuth, Wikimedia Commons,
CC BY-SA 4.0
.
## One idea, found more than once · Last in, first out - Charles Hamblin in Australia reached the same idea independently around the same time. - In 1988 Bauer received the IEEE Computer Pioneer Award, with the citation "For computer stacks". - Today's bracket-matching example is the cellar principle: an opening bracket waits on the stack until its closing bracket arrives. **Lesson:** choose the structure from the operation you need. Here it is "take back the most recent one".
## Check brackets with a stack · The last one opened closes first ~~~cpp #include
#include
#include
bool balanced(const std::string& text) { std::stack
open; for (char c : text) { if (c == '(' || c == '[' || c == '{') { open.push(c); } else if (c == ')' || c == ']' || c == '}') { if (open.empty()) return false; // nothing to close char o = open.top(); open.pop(); bool ok = (o == '(' && c == ')') || (o == '[' && c == ']') || (o == '{' && c == '}'); if (!ok) return false; // wrong kind of closer } } return open.empty(); // anything left is unclosed } int main() { for (std::string s : {"a[i] = (b + c) * {d}", "(]", "((x)", "x)"}) std::cout << s << " -> " << (balanced(s) ? "balanced" : "not balanced") << '\n'; } ~~~ ~~~text a[i] = (b + c) * {d} -> balanced (] -> not balanced ((x) -> not balanced x) -> not balanced ~~~ An opener waits on the stack. The next closer must match the **most recent** opener, which is exactly what `top()` returns. The invariant: the stack holds the openers not yet closed, innermost on top.
## Stack and queue: the general forms ~~~cpp #include
#include
std::stack
s; // LIFO: last in, first out s.push(x); s.top(); s.pop(); s.empty(); s.size(); std::queue
q; // FIFO: first in, first out q.push(x); q.front(); q.back(); q.pop(); q.empty(); q.size(); ~~~ | Operation | Meaning | | --- | --- | | `push(x)` | Add `x`: on top of the stack, or at the back of the queue. | | `top()` | The most recently pushed item of a stack, left in place. | | `front()` | The earliest pushed item of a queue, left in place. | | `pop()` | Remove that item. It returns nothing: read it first with `top()` or `front()`. | | `empty()` | `true` when there is nothing to read or remove. | | `size()` | The number of items stored. | `pop()` removes and `top()`/`front()` reads: two separate calls. Both require a non-empty container.
## An abstract data type · It specifies operations, not representation - The **interface** is the set of operations and what each promises: `push`, `pop`, `top`, `empty`. - The **implementation** is how the data is stored: nodes and `head` (Lecture 14), a `std::vector`, or a `std::deque`. - The **invariant** is the rule every operation keeps true. For the bracket checker: the stack holds the openers not yet closed. - Code that uses only the interface keeps working when the implementation changes. The abstract data type (ADT) says what a stack does; a linked list, a vector, and a deque are three ways to do it.
## Stack · Last in, first out (LIFO)
push(1)
push(2)
push(3)
pop()
top() reads 3
1
1
2
1
2
3
1
2
3
3 removed
top
top
top
top
LIFO: the last item pushed is the first item popped
- `push` puts an item on top; `top` reads the item on top; `pop` removes it. Only the top is reachable. - Function calls behave this way: the function called last returns first (Lecture 16 shows the call stack). - An undo history is a stack: each edit is pushed, and Undo pops the most recent one. - With a linked list, `push` is insertion at the head and `pop` is removal of the head, so each takes the same few steps for any length: O(1).
## Queue · First in, first out (FIFO)
dequeue
pop()
report.pdf
notes.txt
lab3.cpp
enqueue
push(job)
front
front() reads it
back
back() reads it
FIFO: first in, first out
- `push` adds at the back; `front` reads the earliest item; `pop` removes it from the front. - Waiting lines are queues: print jobs, network requests, tasks served in arrival order. - A linked queue keeps pointers to the first and last nodes; a circular array keeps `front`, `rear`, and `count`. Either gives O(1) `push` and `pop`. - Textbooks name the operations enqueue and dequeue; `std::queue` calls them `push` and `pop`.
## A print queue · Jobs served in arrival order ~~~cpp #include
#include
#include
int main() { std::queue
jobs; jobs.push("report.pdf"); jobs.push("notes.txt"); jobs.push("lab3.cpp"); std::cout << "waiting: " << jobs.size() << ", next: " << jobs.front() << '\n'; while (!jobs.empty()) { std::cout << "printing " << jobs.front() << '\n'; jobs.pop(); // remove the job just served } std::cout << "waiting: " << jobs.size() << '\n'; } ~~~ ~~~text waiting: 3, next: report.pdf printing report.pdf printing notes.txt printing lab3.cpp waiting: 0 ~~~ **Takeaways** - The loop condition `!jobs.empty()` guards both `front()` and `pop()`, so neither runs on an empty queue. - Read with `front()` before `pop()`: `pop()` returns nothing. - Fairness is the invariant: a job never overtakes one that arrived earlier.
## One interface, three implementations · The container is a template argument ~~~cpp std::stack
a; // default container: std::deque
std::stack
> b; // same interface, vector inside std::stack
> c; // same interface, list inside for (int x : {1, 2, 3}) { a.push(x); b.push(x); c.push(x); } std::cout << a.top() << ' ' << b.top() << ' ' << c.top() << '\n'; a.pop(); b.pop(); c.pop(); std::cout << a.top() << ' ' << b.top() << ' ' << c.top() << ' ' << a.size() << '\n'; ~~~ ~~~text 3 3 3 2 2 2 2 ~~~ `std::stack` and `std::queue` are container adaptors: they restrict an underlying container to the stack or queue operations. The program cannot index into a stack or insert in its middle, so the invariant cannot be broken from outside.
## Cost decides the representation · A queue on a vector ~~~cpp std::vector
v(100000, 1); std::deque
d(100000, 1); while (!v.empty()) v.erase(v.begin()); // dequeue from the front: shifts the rest while (!d.empty()) d.pop_front(); // dequeue from the front ~~~ ~~~text vector erase(begin): 563 ms deque pop_front: 1 ms ~~~ Timings are implementation- and machine-specific; the gap is the point. Removing the first element of a `std::vector` moves all the others down one place: O(n) per removal, O(n²) for the whole loop. A `std::deque` removes at either end in O(1). That is why `std::queue` uses a `std::deque` by default.
## Compare structures by operation cost | Operation | `std::vector` | linked list | stack | queue | |---|---|---|---|---| | add at the end | O(1) amortized | O(n) with `head` only | `push`: O(1) | `push`: O(1) | | add at the front | O(n) | O(1) | n/a | n/a | | remove at the front | O(n) | O(1) | `pop`: O(1) | `pop`: O(1) | | read the n-th item | O(1) | O(n) | not allowed | not allowed | | search an unsorted collection | O(n) | O(n) | not allowed | not allowed | - Lecture 10's linear search inspects every element in the worst case: O(n). Bubble sort (Lecture 10) does O(n²) work; merge sort (Lecture 11) does O(n log n). - A stack and a queue give up indexing and searching on purpose, so every operation they allow is O(1). - Choose the structure whose cheap operations are the ones the program performs most.
## Poll 1: What is printed?
~~~cpp std::stack
s; s.push(1); s.push(2); s.push(3); s.pop(); s.push(4); while (!s.empty()) { std::cout << s.top(); s.pop(); } ~~~ - **A)** 421 - **B)** 124 - **C)** 431 - **D)** Undefined behavior:
top
is called on an empty stack
📊 sli.do · #2393872
## Common errors · Reading or removing from an empty container ~~~cpp std::stack
s; s.push(7); s.pop(); if (!s.empty()) std::cout << s.top() << '\n'; else std::cout << "empty: no top\n"; assert(!s.empty() && "pop requires a non-empty stack"); s.pop(); // pre-condition violated ~~~ ~~~text empty: no top Assertion failed: (!s.empty() && "pop requires a non-empty stack"), function main, file a5.cpp, line 10. ~~~ The assertion text is implementation-specific (here: Apple clang), and it appears on standard error. | Error | Example | Result | |---|---|---| | `top()` on an empty stack | `std::stack
s; s.top();` | undefined behavior | | `pop()` on an empty stack or queue | `q.pop();` with nothing queued | undefined behavior | | `front()` or `back()` on an empty queue | `q.front();` | undefined behavior | | Expecting `pop()` to return the item | `int x = s.pop();` | does not compile: `pop` returns `void` | Non-empty is the **pre-condition** of `top`, `front`, and `pop` (Lecture 12). Check `empty()` first, or `assert` it where the caller guarantees it.
## Poll 2: What are front() and size()?
~~~cpp std::queue
q; q.push(1); q.push(2); q.push(3); q.pop(); q.push(4); std::cout << q.front() << ' ' << q.size(); ~~~ - **A)** 1 and 3 - **B)** 2 and 3 - **C)** 2 and 4 - **D)** 4 and 3
📊 sli.do · #2393872
## Questions about stacks and queues | Question | Answer | |---|---| | Why does `pop()` not return the item? | Returning by value could throw while copying, after the item is already removed, and the item would be lost. So you read with `top()`, then remove with `pop()`. | | Can I loop over a stack or queue? | Not directly: there are no iterators. Drain it with `while (!s.empty())`, or keep a `std::vector` if you need to traverse. | | Is `std::stack` a container? | It is a container adaptor: it wraps a container (`std::deque` by default) and exposes only the stack operations. | | Stack or queue: how do I choose? | Ask which item must come out next. The newest (undo, matching brackets): stack. The oldest (print jobs, requests): queue. | | Is O(1) always faster than O(n)? | Not for tiny inputs. It says how the cost grows with n, so it matters as n gets large. | | Where does a stack appear besides `std::stack`? | Function calls form a call stack, which Lecture 16 uses to explain recursion. |
## Summary: stacks, queues, and abstract data types - An abstract data type is defined by its operations and invariant; the interface hides the implementation. - A stack is LIFO (`push`, `top`, `pop`); a queue is FIFO (`push`, `front`, `pop`). A linked list with head insertion is a stack. - `std::stack` and `std::queue` adapt `std::deque` (or `std::vector`, `std::list`); the interface stays the same. - Removing the front of a `std::vector` is O(n); `std::deque` and a linked list do it in O(1). - `top`, `front`, and `pop` on an empty container are undefined behavior: check `empty()`. **Reading:** Stroustrup, *Programming: Principles and Practice Using C++* (2nd ed.), chapter “Containers and Iterators”; Savitch, *Problem Solving with C++*, “Stacks and Queues”.
## Lab practice: stacks and queues **IC151 lab practice · 5 problems** 1. **Reverse.** Read integers until `-1`, push each on a `std::stack
`, then pop and print them. What order appears, and why? 1. **Brackets.** Extend the bracket checker to ignore brackets inside double-quoted strings. Test on `"(" + (a)`. 1. **Print queue.** Read job names until `done`, queue them, then serve and print each one. Print the number waiting after each job. 1. **Two stacks.** Make `std::stack
>` and a `std::stack
>`. Push 1 to 5 on both and show the pops are identical. 1. **Guard.** Write `int top_or(const std::stack
& s, int fallback)` that returns `fallback` when empty. Then call `s.top()` on an empty stack under `-fsanitize=address` and read the report. Reaching this set marks the lecture as studied in this browser. [Open the IC151 lab setup and batch schedule](ic151.html).