Quiz: Stacks, Queues, and Beyond

Test your understanding before moving on. Target: 70% or higher to proceed confidently.


Section 1: Multiple Choice (1 point each)

1. Which principle describes a stack's access pattern?

  • A) FIFO — First In, First Out
  • B) LIFO — Last In, First Out
  • C) Random access
  • D) Priority-based access
Answer **B)** LIFO — Last In, First Out. *Why B:* A stack always removes the most recently added item first, like a stack of plates. *Why not A:* FIFO describes a queue, not a stack. *Reference:* Section 20.2

2. What is the output of this code?

s = Stack()
s.push(1)
s.push(2)
s.push(3)
s.pop()
s.push(4)
print(s.peek())
  • A) 1
  • B) 2
  • C) 3
  • D) 4
Answer **D)** 4 *Why D:* After pushing 1, 2, 3, we pop (removes 3). Then push 4. The top is now 4. *Why not C:* 3 was removed by `pop()`. *Reference:* Section 20.2

3. Which operation is NOT part of the standard stack ADT?

  • A) push(item)
  • B) pop()
  • C) insert(index, item)
  • D) peek()
Answer **C)** `insert(index, item)` *Why C:* Stacks only allow adding/removing at the top. Inserting at an arbitrary position violates the LIFO constraint. *Why not A/B/D:* These are the three core stack operations. *Reference:* Section 20.2, 20.6

4. Why does our Queue implementation use collections.deque instead of a plain list?

  • A) deque uses less memory than a list
  • B) deque.popleft() is O(1), while list.pop(0) is O(n)
  • C) Lists cannot store mixed types
  • D) deque automatically sorts elements
Answer **B)** `deque.popleft()` is O(1), while `list.pop(0)` is O(n). *Why B:* When you remove the first element of a list, Python shifts every remaining element forward. A deque uses a doubly-linked block structure that avoids this shift. *Why not A:* Deque uses slightly more memory due to its internal structure. *Reference:* Section 20.4

5. In the bracket-matching algorithm, what data structure is used and why?

  • A) Queue — brackets must be checked in order of appearance
  • B) Stack — the most recently opened bracket must be the first one closed
  • C) Dictionary — to map opening brackets to closing brackets
  • D) Set — to check if a character is a bracket in O(1)
Answer **B)** Stack — the most recently opened bracket must be the first one closed. *Why B:* Brackets nest, so the last opening bracket encountered must match the next closing bracket. This is LIFO behavior. *Why not A:* While we scan left to right (order matters), the matching logic is LIFO. *Reference:* Section 20.3

6. What is the output of this code?

q = Queue()
q.enqueue("X")
q.enqueue("Y")
q.enqueue("Z")
print(q.dequeue())
print(q.dequeue())
  • A) Z, Y
  • B) X, Y
  • C) Y, X
  • D) Z, X
Answer **B)** X, Y *Why B:* Queues are FIFO. X was enqueued first, so it's dequeued first. Then Y. *Why not A:* That would be LIFO (stack) behavior. *Reference:* Section 20.4

7. What does ADT stand for, and what does it describe?

  • A) Advanced Data Technology — the fastest implementation of a data structure
  • B) Abstract Data Type — the operations and behavior of a data structure, independent of implementation
  • C) Automated Data Testing — a framework for testing data structures
  • D) Applied Data Transformation — converting between data formats
Answer **B)** Abstract Data Type — the operations and behavior of a data structure, independent of implementation. *Why B:* An ADT specifies *what* operations are available and their behavior, but not *how* they're implemented internally. *Reference:* Section 20.6

8. In a linked list, each node contains:

  • A) Only data
  • B) Data and an index number
  • C) Data and a reference to the next node
  • D) Data and a reference to all other nodes
Answer **C)** Data and a reference to the next node. *Why C:* A singly-linked list node stores its value and a pointer/reference to the next node. The last node's reference is `None`. *Why not D:* Each node only knows about the *next* node, not all nodes. *Reference:* Section 20.7

9. What is the time complexity of accessing the 100th element in a linked list?

  • A) O(1)
  • B) O(log n)
  • C) O(n)
  • D) O(n^2)
Answer **C)** O(n) *Why C:* Linked lists don't support random access. You must start at the head and follow references one by one until you reach the 100th node. *Why not A:* O(1) random access is a property of arrays/Python lists, not linked lists. *Reference:* Section 20.7

10. Which data structure would be BEST for modeling a social network (who follows whom)?

  • A) Stack
  • B) Queue
  • C) Tree
  • D) Graph
Answer **D)** Graph *Why D:* A graph represents arbitrary relationships between entities. Social network connections (follows, friendships) can be bidirectional or unidirectional, with cycles — exactly what graphs model. *Why not C:* Trees are hierarchical (parent-child) and don't allow cycles. Social networks have complex, non-hierarchical connections. *Reference:* Section 20.8

Section 2: Short Answer (2 points each)

11. Explain the difference between push and enqueue. Under what circumstances would you use each?

Answer **`push`** adds an item to the top of a **stack** (LIFO). Use it when you need to reverse order or access the most recent item first (undo systems, backtracking, bracket matching). **`enqueue`** adds an item to the back of a **queue** (FIFO). Use it when you need to process items in the order they arrived (task scheduling, print queues, BFS). Both add items, but the removal order differs: stacks remove from the same end (top), queues remove from the opposite end (front). *Reference:* Sections 20.2, 20.4

12. A student writes this code:

my_queue = []
my_queue.append("task1")
my_queue.append("task2")
my_queue.append("task3")
first = my_queue.pop(0)

Identify the problem and suggest a fix.

Answer The code works correctly but has a **performance problem**. `list.pop(0)` is O(n) because Python must shift all remaining elements forward by one position. For a queue with many items, this becomes very slow. **Fix:** Use `collections.deque`:
from collections import deque
my_queue = deque()
my_queue.append("task1")
my_queue.append("task2")
my_queue.append("task3")
first = my_queue.popleft()  # O(1) instead of O(n)
*Reference:* Section 20.4

13. What happens when you try to pop() from an empty stack in our implementation? Why is this behavior (raising an exception) preferable to returning None?

Answer Our implementation raises an `IndexError` with the message "pop from empty stack." Raising an exception is preferable to returning `None` because: 1. **Fail-fast design:** An empty pop almost always indicates a logic error. An exception makes the bug immediately visible rather than letting `None` propagate silently through the program. 2. **Ambiguity:** If `None` is a valid item in the stack, returning `None` from an empty pop is indistinguishable from popping an actual `None` value. 3. **Consistency:** Python's own `list.pop()` raises `IndexError` on an empty list. Our stack follows the same convention. *Reference:* Section 20.2, 20.9

14. Explain the threshold concept from this chapter in your own words: "A stack is a CONCEPT (LIFO behavior) that can be IMPLEMENTED in many ways." Give an example.

Answer An abstract data type (ADT) defines *what* a data structure does — its operations and their behavior — without specifying *how* it's built internally. The stack ADT says "support push, pop, and peek with LIFO ordering." That's the concept. The implementation is a separate decision. We implemented a stack using a Python list (`append` and `pop`), but we could also implement it using a `collections.deque`, a linked list, or even a dictionary with an integer counter as the key. All of these would behave identically from the outside. This separation matters because it lets you swap implementations (e.g., for better performance) without changing any code that uses the stack. *Reference:* Section 20.6

Section 3: Code Analysis (3 points each)

15. What does this function do? Trace through it with the input "hello".

def mystery(text: str) -> bool:
    s = Stack()
    for ch in text:
        s.push(ch)
    reversed_text = ""
    while not s.is_empty():
        reversed_text += s.pop()
    return text == reversed_text
Answer This function checks whether `text` is a **palindrome** (reads the same forwards and backwards). **Trace with "hello":** 1. Push: h, e, l, l, o → stack top is 'o' 2. Pop all: o, l, l, e, h → `reversed_text = "olleh"` 3. `"hello" == "olleh"` → `False` The function returns `False` because "hello" is not a palindrome. For "racecar": reversed would be "racecar" → returns `True`. *Reference:* Sections 20.2, 20.3

16. This code has a bug. Find it and explain how to fix it.

def process_tasks(task_list: list[str]) -> None:
    q = Queue()
    for task in task_list:
        q.enqueue(task)

    for i in range(len(task_list) + 1):  # Bug is here
        task = q.dequeue()
        print(f"Processing: {task}")
Answer **Bug:** The `range(len(task_list) + 1)` iterates one too many times. If `task_list` has 3 items, the loop runs 4 times. The fourth `dequeue()` call operates on an empty queue, raising `IndexError`. **Fix — Option 1:** Use `range(len(task_list))` (no `+ 1`). **Fix — Option 2 (preferred):** Use `while not q.is_empty()`:
while not q.is_empty():
    task = q.dequeue()
    print(f"Processing: {task}")
This is more robust because it doesn't assume the queue size matches the original list length. *Reference:* Section 20.9 (debugging walkthrough)

17. Analyze this undo/redo sequence. What is the final value of editor.text?

editor = TextEditor()
editor.type_text("AB")       # text = "AB"
editor.type_text("CD")       # text = "ABCD"
editor.type_text("EF")       # text = "ABCDEF"
editor.undo()                # text = ?
editor.undo()                # text = ?
editor.redo()                # text = ?
editor.type_text("GH")       # text = ?
editor.redo()                # text = ? (does anything happen?)
Answer Step by step: 1. `type_text("AB")` → text = "AB", undo stack: [""], redo stack: [] 2. `type_text("CD")` → text = "ABCD", undo stack: ["", "AB"], redo stack: [] 3. `type_text("EF")` → text = "ABCDEF", undo stack: ["", "AB", "ABCD"], redo stack: [] 4. `undo()` → text = **"ABCD"**, undo stack: ["", "AB"], redo stack: ["ABCDEF"] 5. `undo()` → text = **"AB"**, undo stack: [""], redo stack: ["ABCDEF", "ABCD"] 6. `redo()` → text = **"ABCD"**, undo stack: ["", "AB"], redo stack: ["ABCDEF"] 7. `type_text("GH")` → text = **"ABCDGH"**, undo stack: ["", "AB", "ABCD"], redo stack: **[]** (cleared!) 8. `redo()` → Nothing happens. Redo stack is empty because the new action in step 7 cleared it. **Final text: "ABCDGH"** The key insight: typing "GH" after undoing clears the redo stack, because you've branched into a new timeline. *Reference:* Section 20.3

18. Compare these two implementations of the same operation. Which is better and why?

# Version A
def drain_queue_a(q: Queue) -> list:
    result = []
    while q.size() > 0:
        result.append(q.dequeue())
    return result

# Version B
def drain_queue_b(q: Queue) -> list:
    result = []
    try:
        while True:
            result.append(q.dequeue())
    except IndexError:
        pass
    return result
Answer **Version A is better.** It explicitly checks whether the queue has items before dequeuing. This follows the "Look Before You Leap" (LBYL) pattern and communicates the developer's intent clearly. **Version B works** but uses exceptions for normal control flow, which is generally discouraged. Exceptions should signal *exceptional* situations, not expected ones. An empty queue after draining is completely expected, not an error. Additionally, Version B is harder to read — a reader has to understand that the `IndexError` is intentionally swallowed. Note: Using `while not q.is_empty()` is slightly more Pythonic than `while q.size() > 0` but both are acceptable. *Reference:* Section 20.9, [Chapter 11](../../part-04-building-robust-programs/chapter-11-error-handling/index.md) (error handling philosophy)

Section 4: Conceptual Questions (2 points each)

19. A student says: "Linked lists are better than Python lists because insertion is O(1)." What important caveat are they missing?

Answer The student is missing a critical caveat: **linked list insertion is O(1) only when you already have a reference to the insertion point.** Finding that point (i.e., getting to the right node) is O(n) because you must traverse from the head. In contrast, a Python list provides O(1) *access* to any position via indexing, though the actual insertion at that position is O(n) due to shifting. The full trade-off: linked lists sacrifice O(1) random access for O(1) insertion/deletion *at known positions*. Python lists sacrifice O(1) insertion for O(1) random access. Neither is universally better — the right choice depends on the problem. *Reference:* Section 20.7

20. For each scenario, name the best data structure and justify your choice in one sentence:

a) Managing browser tabs that you open and close in order b) Processing customer service tickets in the order they were submitted c) Representing a family tree d) Checking if a word exists in a dictionary of 100,000 words e) Implementing Ctrl+Z in a word processor

Answer a) **Stack** — browser tabs follow LIFO: you typically close the most recently opened tab first, and "close" undoes the last "open." b) **Queue** — tickets should be processed in FIFO order to be fair; the first customer to submit should be served first. c) **Tree** — family relationships are hierarchical (parent-child), which is exactly what trees model. d) **Set** (or dict/hash table) — membership testing (`word in dictionary`) is O(1) average with a hash-based structure, versus O(n) for a list. e) **Stack** (two stacks for undo/redo) — undo reverses the most recent action (LIFO), and a second stack enables redo. *Reference:* Section 20.9