Linear Data Structures
Level: 4 (Application — novel problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Justify your reasoning. Assume 0-indexed arrays unless stated otherwise. Pseudocode or any mainstream language is acceptable for code.
Question 1 — Dynamic array growth & amortization (12 marks)
A dynamic array starts with capacity 1. Whenever an append finds the array full, it allocates a new buffer of capacity ⌈1.5 × current_capacity⌉, copies all elements, then appends.
(a) Starting from empty, list the capacities the array passes through as it grows to hold the first 20 elements. (3)
(b) Compute the total number of element copies (moves during resizes) performed while appending those 20 elements. (4)
(c) A colleague proposes a growth factor of exactly 1.0 (i.e. grow by +1 slot each time it fills). State the amortized cost per append under this policy for appends, with a one-line justification. (3)
(d) Explain in one or two sentences why cache locality makes an array traversal faster than an equivalent linked-list traversal, even when both are . (2)
Question 2 — Infix to postfix by hand + stack trace (14 marks)
Consider the infix expression:
Standard precedence: *,/ higher than +,-; all left-associative; ( ) for grouping.
(a) Convert the expression to postfix (RPN) using the shunting-yard algorithm. (6)
(b) Show the operator-stack contents at the moment just after the second + (the one inside the parentheses) is processed. (3)
(c) Evaluate your postfix result for , showing the value stack after each of the first five postfix tokens are consumed. (5)
Question 3 — Design with a deque (12 marks)
You must design a data structure MedianWindow that supports a sliding window of the last inserted integers and answers max() on the current window in amortized .
(a) Which linear structure(s) from this chapter would you combine, and why? Name the auxiliary structure used to track the maximum. (3)
(b) Give pseudocode for insert(x) (which also evicts the oldest element if the window already holds items) and max(). (6)
(c) Give the amortized time complexity of insert and justify why it is despite an inner loop. (3)
Question 4 — Linked-list surgery (12 marks)
You are given the head of a singly linked list of nodes. Write an algorithm that rearranges it in place so nodes are reordered as:
(a) Describe your algorithm in clear steps (you may use the standard three-phase technique). (5)
(b) Write the pseudocode/code. (5)
(c) State the time and space complexity, and one reason this is harder on a singly linked list than on an array. (2)
Question 5 — Circular queue reasoning (10 marks)
A circular array queue is implemented over an array of fixed size using indices front, rear and a count variable.
(a) After the operation sequence below, give the final values of front, rear, and count. Start with front=0, rear=0, count=0. rear points to the next free slot; enqueue writes at rear; dequeue reads at front. (5)
enqueue×6, dequeue×4, enqueue×5, dequeue×2
(b) Explain why the count variable (or an equivalent reserved-slot scheme) is necessary to distinguish a full queue from an empty one when using only front and rear. (3)
(c) State one real-world application where a circular buffer is preferred over a plain dynamic array. (2)
Answer keyMark scheme & solutions
Question 1 (12)
(a) Growth by ⌈1.5×cap⌉ from cap 1: 1 → 2 → 3 → 5 → 8 → 12 → 18 → 27.
Sequence of capacities passed through until ≥20: 1, 2, 3, 5, 8, 12, 18, 27. (3: correct rounding & stopping at 27)
(b) Copies happen at each resize = old size copied. Resizes occur when going from cap 1→2 (copy 1), 2→3 (copy 2), 3→5 (copy 3), 5→8 (copy 5), 8→12 (copy 8), 12→18 (copy 12), 18→27 (copy 18). We need capacity ≥20, so we grow up to 27. Total copies = 1+2+3+5+8+12+18 = 49. (4: correct set of resizes 2, correct sum 2)
(c) Growing by +1 each fill: appending elements triggers a resize almost every append, total copies , so amortized cost per append is (not constant). (3)
(d) Array elements are contiguous in memory, so sequential access loads whole cache lines and prefetching works; linked-list nodes are scattered, causing cache misses per hop. (2)
Question 2 (14)
(a) Shunting-yard on A + B * C - ( D / E + F ) * G:
- A → output:
A +→ stack:+- B →
A B *(higher than +) → stack:+ *- C →
A B C -: pop*,+(both ≥-) → outputA B C * +; push-. stack:-(→ stack:- (- D →
A B C * + D /→ stack:- ( /- E →
A B C * + D E +inside: pop/(higher) → output... D E /; push+. stack:- ( +- F →
A B C * + D E / F ): pop until(→ pop+→ output... F +; discard(. stack:-*→ stack:- *- G → append G
- end: pop
*,-
Postfix: A B C * + D E / F + G * - (6)
(b) Just after the inner + is processed, stack (bottom→top): - ( +. (3)
(c) Values A=6,B=2,C=3,D=8,E=4,F=1,G=5. Tokens: A B C * + D E / F + G * -.
Value stack after first five tokens:
| token | stack (bottom→top) |
|---|---|
| A(6) | 6 |
| B(2) | 6, 2 |
| C(3) | 6, 2, 3 |
* |
6, 6 |
+ |
12 |
(5: 1 each row) Full evaluation continues: D E / = 8/4=2, F=1, + =3, ×G(5)=15, then 12−15 = −3.
Question 3 (12)
(a) Use a queue (or ring buffer) to hold the window elements in insertion order, plus a monotonic decreasing deque of candidate maxima. The deque's front is always the current max. (3)
(b)
insert(x):
if count == k: # evict oldest
old = queue.dequeue()
if maxdq.front == old:
maxdq.pop_front()
count -= 1
while not maxdq.empty and maxdq.back < x:
maxdq.pop_back()
maxdq.push_back(x)
queue.enqueue(x)
count += 1
max():
return maxdq.front
(6: correct eviction 2, correct monotonic maintenance 2, max()=front 2)
(c) Each element is pushed to and popped from the max-deque at most once, so over inserts total deque work is → amortized per insert, max is . (3)
Question 4 (12)
(a) Three-phase technique:
- Find middle with slow/fast pointers, split into two halves.
- Reverse the second half in place.
- Merge the two halves alternately. (5)
(b)
reorder(head):
if head==null or head.next==null: return
# 1. find middle
slow=head; fast=head
while fast.next and fast.next.next:
slow=slow.next; fast=fast.next.next
second = slow.next; slow.next = null
# 2. reverse second half
prev=null
while second:
nxt=second.next; second.next=prev; prev=second; second=nxt
# 3. merge
first=head; second=prev
while second:
f2=first.next; s2=second.next
first.next=second; second.next=f2
first=f2; second=s2
(5)
(c) Time , space . Harder than an array because a singly linked list has no random access / backward links, so index can't be reached in — hence the reverse-and-merge trick. (2)
Question 5 (10)
(a) rear = next free slot, arithmetic mod 8.
- enqueue×6: rear=6, count=6, front=0
- dequeue×4: front=4, count=2, rear=6
- enqueue×5: rear=(6+5)mod8=3, count=7, front=4
- dequeue×2: front=6, count=5, rear=3
Final: front=6, rear=3, count=5. (5: front 2, rear 2, count 1)
(b) With only front and rear, the condition front==rear occurs both when the queue is empty and when it is completely full, so the two states are indistinguishable. A count variable (or leaving one slot unused) resolves the ambiguity. (3)
(c) Fixed-size streaming/producer-consumer buffers: e.g. audio/network packet buffers, keyboard input buffer, or a bounded log — reuses memory with no reallocation. (2)
[
{"claim":"Q1b total copies for growth to hold 20 elements = 49","code":"cap=1; total=0\nwhile cap<20:\n total+=cap\n cap=-(-3*cap//2) # ceil(1.5*cap)\nresult = (total==49)"},
{"claim":"Q1a capacity sequence is 1,2,3,5,8,12,18,27","code":"seq=[1]; cap=1\nwhile cap<20:\n cap=-(-3*cap//2)\n seq.append(cap)\nresult = (seq==[1,2,3,5,8,12,18,27])"},
{"claim":"Q2c postfix evaluation of A B C * + D E / F + G * - equals -3","code":"vals={'A':6,'B':2,'C':3,'D':8,'E':4,'F':1,'G':5}\ntokens='A B C * + D E / F + G * -'.split()\nst=[]\nimport operator\nops={'+':operator.add,'-':operator.sub,'*':operator.mul,'/':lambda a,b:a//b}\nfor t in tokens:\n if t in ops:\n b=st.pop(); a=st.pop(); st.append(ops[t](a,b))\n else:\n st.append(vals[t])\nresult = (st[-1]==-3)"},
{"claim":"Q5a final front,rear,count = 6,3,5","code":"N=8; front=0; rear=0; count=0\ndef enq():\n global rear,count; rear=(rear+1)%N; count+=1\ndef deq():\n global front,count; front=(front+1)%N; count-=1\nfor _ in range(6): enq()\nfor _ in range(4): deq()\nfor _ in range(5): enq()\nfor _ in range(2): deq()\nresult = (front==6 and rear==3 and count==5)"}
]