Level 3 — ProductionLinear Data Structures

Linear Data Structures

45 minutes60 marksprintable — key stays hidden on paper

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 nn 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 O(1)O(1) but growing by a fixed constant cc each time gives amortized O(n)O(n). 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+BCD/EA + B * C - D / E

(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 kk 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 1,2,4,8,,2k1,2,4,8,\dots,2^{k} where 2k<n2^{k}<n. Copy cost at each resize equals current element count: total copies=i=0log2n2i=2log2n+11<2n.\text{total copies} = \sum_{i=0}^{\lfloor\log_2 n\rfloor} 2^i = 2^{\lfloor\log_2 n\rfloor+1}-1 < 2n. Adding nn for the actual writes: total =n+(2n1)<3n=O(n)= n + (2n-1) < 3n = O(n). Amortized per append =O(n)/n=O(1)= O(n)/n = O(1). (3 for summation + evaluation, 2 for concluding amortized O(1))

(b) Fixed growth by cc: resizes at sizes c,2c,3c,,nc,2c,3c,\dots,n, copy cost i=1n/cci=c(n/c)(n/c+1)2=Θ(n2/c).\sum_{i=1}^{n/c} c\cdot i = c\cdot\frac{(n/c)(n/c+1)}{2} = \Theta(n^2/c). Total Θ(n2)\Theta(n^2) ⇒ amortized Θ(n)\Theta(n) per append. Doubling makes resize count logarithmic and geometric copy cost sums to Θ(n)\Theta(n); fixed growth makes it linear count with arithmetic cost sum Θ(n2)\Theta(n^2). (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 prev

Time O(n)O(n) (one pass), space O(1)O(1) (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 A+BCD/EA + B * C - D / E:

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)"}
]