Linear Data Structures
Level 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Instructions: Write pseudocode or Python. Complexity claims must be justified. "Explain out loud" prompts require prose reasoning, not just code.
Question 1 — Dynamic Array Amortized Analysis (10 marks)
A dynamic array doubles its capacity when full, copying all elements to the new buffer.
(a) From scratch, derive the amortized cost of append for a sequence of appends starting from capacity 1. Show the total copy cost as a summation and evaluate it. (5)
(b) Explain out loud why doubling gives amortized but growing by a fixed constant each time gives amortized . Show the summation for the fixed-growth case. (3)
(c) State one concrete way array cache locality outperforms a linked list for sequential traversal. (2)
Question 2 — Singly Linked List, Code from Memory (12 marks)
Define a singly linked list node, then write functions (any clear pseudocode/Python):
(a) insert_at_head(head, value) returning the new head. (3)
(b) delete_value(head, target) — delete the first node whose data equals target, returning the new head. Handle: empty list, target at head, target absent. (5)
(c) reverse(head) iteratively, returning the new head. State its time and space complexity. (4)
Question 3 — Balanced Parentheses & Stack (10 marks)
(a) Write an algorithm using a stack that returns True iff a string of () [] {} is balanced. (5)
(b) Trace your algorithm on the input ([)]. Show the stack state after each character and give the verdict. (3)
(c) Explain out loud why this problem cannot be solved correctly using only a single integer counter when multiple bracket types are present. (2)
Question 4 — Infix to Postfix (10 marks)
Convert the infix expression to postfix using the shunting-yard method.
(a) Show the operator stack and output queue after each token is read. (6)
(b) Give the final postfix string. (2)
(c) State the precedence and associativity rules you used for + - * /. (2)
Question 5 — Circular Queue on a Fixed Array (12 marks)
Implement a queue on a fixed array of capacity using two indices front, rear and a size counter.
(a) Write enqueue(x) and dequeue(), each detecting overflow/underflow. (6)
(b) Explain out loud why using size (or a spare slot) is needed to distinguish the full state from the empty state. (3)
(c) A circular queue has capacity 5. Starting empty, perform: enqueue 10, 20, 30; dequeue; dequeue; enqueue 40, 50, 60. Give the final front index, rear index, and array contents (index 0..4). Use the convention: rear points to the last inserted slot, initialised front=0, rear=-1. (3)
Question 6 — Deque & Priority Queue Concepts (6 marks)
(a) Name the four core deque operations and give one real use case where a deque is strictly better than a plain queue. (3) (b) Explain out loud how a priority queue differs from a FIFO queue in dequeue order, and give one application. (3)
Answer keyMark scheme & solutions
Question 1 (10)
(a) With capacity doubling from 1, resizes happen at sizes where . Copy cost at each resize equals current element count: Adding for the actual writes: total . Amortized per append . (3 for summation + evaluation, 2 for concluding amortized O(1))
(b) Fixed growth by : resizes at sizes , copy cost Total ⇒ amortized per append. Doubling makes resize count logarithmic and geometric copy cost sums to ; fixed growth makes it linear count with arithmetic cost sum . (2 summation, 1 conclusion)
(c) Array elements are contiguous in memory, so sequential access loads whole cache lines (spatial locality) with few cache misses; linked-list nodes are scattered, causing a cache miss / pointer chase per node. (2)
Question 2 (12)
(a)
def insert_at_head(head, value):
node = Node(value)
node.next = head
return node(1 create, 1 link, 1 return new head)
(b)
def delete_value(head, target):
if head is None: # empty
return None
if head.data == target: # target at head
return head.next
prev, cur = head, head.next
while cur is not None:
if cur.data == target:
prev.next = cur.next # unlink
return head
prev, cur = cur, prev.next
return head # target absent(1 empty, 1 head case, 2 traversal+unlink, 1 absent case)
(c)
def reverse(head):
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prevTime (one pass), space (three pointers). (3 code, 1 complexity)
Question 3 (10)
(a)
def balanced(s):
stack = []
pairs = {')':'(', ']':'[', '}':'{'}
for ch in s:
if ch in '([{':
stack.append(ch)
elif ch in ')]}':
if not stack or stack.pop() != pairs[ch]:
return False
return len(stack) == 0(2 push opens, 2 match on close, 1 final empty check)
(b) Input ([)]:
| char | action | stack |
|---|---|---|
( |
push | ( |
[ |
push | ( [ |
) |
pop [, expected ( → mismatch |
— |
Returns False (verdict: not balanced). (3)
(c) A single counter tracks only how many brackets are open, not which type. ([)] has counter returning to 0 yet is invalid because types interleave incorrectly. A stack preserves the nesting order/type needed to verify correct matching. (2)
Question 4 (10)
(a) Precedence: * / > + -; all left-associative. Tokens of :
| token | action | output | stack |
|---|---|---|---|
| A | operand → out | A | |
| + | push | A | + |
| B | out | A B | + |
| * | prec(*)>prec(+), push | A B | + * |
| C | out | A B C | + * |
| - | pop *, pop + (≥ prec, left-assoc), push - | A B C * + | - |
| D | out | A B C * + D | - |
| / | prec(/)>prec(-), push | A B C * + D | - / |
| E | out | A B C * + D E | - / |
| end | pop all | A B C * + D E / - |
(6 — deduct per wrong step)
(b) Final postfix: A B C * + D E / - (2)
(c) * / have higher precedence than + -; all are left-associative, so on a tie an incoming operator does not exceed the stack top and the stack operator is popped first. (2)
Question 5 (12)
(a)
def enqueue(x):
if size == capacity:
raise Overflow
rear = (rear + 1) % capacity
arr[rear] = x
size += 1
def dequeue():
if size == 0:
raise Underflow
x = arr[front]
front = (front + 1) % capacity
size -= 1
return x(3 enqueue incl. overflow+wrap, 3 dequeue incl. underflow+wrap)
(b) With only front and rear, the condition front == rear (or rear+1 == front) occurs both when the buffer is completely empty and completely full — indistinguishable. A size counter (or leaving one slot unused) removes the ambiguity. (3)
(c) Trace (cap 5, front=0, rear=-1):
- enq 10,20,30 → arr
[10,20,30,_,_], front=0, rear=2, size=3 - dequeue → removes 10, front=1, size=2
- dequeue → removes 20, front=2, size=1
- enq 40 → rear=3, arr
[10,20,30,40,_] - enq 50 → rear=4, arr
[10,20,30,40,50] - enq 60 → rear=(4+1)%5=0, arr
[60,20,30,40,50], size=4
Final: front = 2, rear = 0, array = [60, 20, 30, 40, 50]. (Indices 0,1 hold stale/overwritten values; only indices 2,3,4,0 are live: 30,40,50,60.) (3)
Question 6 (6)
(a) Operations: push_front, push_back, pop_front, pop_back. Use case: sliding-window maximum (monotonic deque), or an undo/redo browser history where both ends are accessed. (2 ops, 1 use case)
(b) FIFO queue dequeues strictly in insertion order (oldest first). A priority queue dequeues the element with the highest priority regardless of insertion time. Application: task/job scheduling, Dijkstra's shortest path. (2 difference, 1 application)
[
{"claim":"Doubling total copies from cap 1 to n=1024 is less than 2n and equals 2047","code":"n=1024; k=n.bit_length()-1; total=sum(2**i for i in range(k+1)); result = (total==2047 and total<2*n)"},
{"claim":"Fixed growth by c=1 gives sum of copies = n(n-1)/2 for n=1000, which is Theta(n^2)","code":"n=1000; c=1; total=sum(c*i for i in range(1,n//c)); result = (total == (n-1)*(n)//2 - 0 and total==sum(range(1,n)))"},
{"claim":"Postfix of A+B*C-D/E has operators in order * + / -","code":"post=['A','B','C','*','+','D','E','/','-']; ops=[t for t in post if t in '+-*/']; result = ops==['*','+','/','-']"},
{"claim":"Circular queue final rear index is 0 after the given operation sequence, cap 5","code":"cap=5; rear=-1; front=0; size=0\ndef enq():\n global rear,size\n rear=(rear+1)%cap; size+=1\ndef deq():\n global front,size\n front=(front+1)%cap; size-=1\nenq();enq();enq();deq();deq();enq();enq();enq()\nresult = (front==2 and rear==0)"}
]