Data Structures & Algorithms: Industry Edition
Unit 3: Stacks, Queues & Recursion
Polish notation, expression evaluation, Tower of Hanoi, Merge Sort & Quick Sort โ with real examples from Swiggy, Paytm, and Amazon.
๐ข Real Projects | ๐ป 4 Lab Programs (Python + C) | ๐ 25 MCQs | ๐ฏ 3 Interview Questions
Industry Hook โ The Real-World Problem First
๐ The Swiggy Problem: 50,000 Orders Per Hour, Zero Dropped
It's 8 PM on a Friday in Bangalore. Swiggy is processing 50,000+ orders per hour โ over 14 orders every second. Behind the scenes, two invisible data structures keep the entire system running:
- Order Queue (FIFO): Every new order enters the back of the queue. The kitchen sees orders in the exact sequence customers placed them. No order is skipped, no order cuts in line. This is a queue โ First In, First Out.
- Payment Transaction Stack (LIFO): When a customer pays โน500, Paytm's payment gateway records: "charge โน500" โ "verify OTP" โ "deduct from wallet." If the OTP fails, the system must undo operations in reverse order โ redo wallet credit, cancel verification, reverse charge. This is a stack โ Last In, First Out.
- Recursive Route Optimization: Swiggy's delivery algorithm breaks the city into zones, then sub-zones, then individual streets โ recursively dividing the problem until each piece is solvable. This is divide-and-conquer recursion โ the same principle behind Merge Sort and Quick Sort.
If Swiggy used an array instead of a queue, removing the first order would shift 50,000 elements โ the system would freeze. If Paytm used forward processing instead of a stack for rollbacks, failed transactions would corrupt account balances for millions of users.
This is exactly the problem stacks, queues, and recursion solve. Let's understand how.
Concept Explanation โ Theory, Earned
2.1 Stacks โ Last In, First Out (LIFO)
Layer 1 โ Intuition
A stack is a pile of plates in a hostel mess. You can only add a plate on top (push) and remove the plate on top (pop). You can't pull a plate from the middle without toppling the pile. The last plate placed is the first one taken โ LIFO.
Layer 2 โ Visual: Array vs Linked List Representation
Array-based Stack
top = 3
โ
โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโ
โ 5 โ 12 โ 8 โ 3 โ โ โ capacity = 6
โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
[0] [1] [2] [3] [4] [5]
push(7): arr[4] = 7, top = 4 โ O(1)
pop(): return arr[3] = 3, top = 2 โ O(1)
Linked-List-based Stack
top
โ
โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ
โ 3 โโโโถโ 8 โโโโถโ 12 โโโโถโ 5 โโโโถ NULL
โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ
push(7): Create node [7], [7].next = top, top = [7] โ O(1)
pop(): return top.data, top = top.next โ O(1)
(Push/pop always at the HEAD โ that's why it's O(1))
Layer 3 โ Complexity Table
| Operation | Array Stack | LL Stack | Notes |
|---|---|---|---|
| Push | O(1)* | O(1) | *Amortized for dynamic array |
| Pop | O(1) | O(1) | Both just move the top pointer |
| Peek/Top | O(1) | O(1) | Read without removing |
| isEmpty | O(1) | O(1) | Check if top == -1 or top == NULL |
| Space | O(n) fixed | O(n) dynamic | LL uses extra pointer per node |
2.2 Arithmetic Expressions & Polish Notation
Why does this matter?
When you type 3 + 5 * 2 in a calculator, how does it know to multiply first? Humans use parentheses and BODMAS rules, but computers need a stack-based algorithm to parse and evaluate expressions. This is how every compiler, interpreter, and calculator app works.
Three Expression Formats
| Format | Example | Operator Position | Used By |
|---|---|---|---|
| Infix | A + B * C | Between operands | Humans |
| Prefix (Polish) | + A * B C | Before operands | Lisp, some calculators |
| Postfix (Reverse Polish) | A B C * + | After operands | Stack machines, HP calculators, Java bytecode |
Java's JVM and Python's bytecode compiler both convert your infix code to postfix internally. When you write x = a + b * c, the compiler generates: LOAD a, LOAD b, LOAD c, MULTIPLY, ADD, STORE x โ that's postfix! Every expression you've ever written gets converted using the stack algorithm below.
Infix โ Postfix Conversion (Shunting-Yard Algorithm)
Convert: A + B * C - D
Token Action Stack Output
โโโโโ โโโโโโ โโโโโ โโโโโโ
A Operand โ output (empty) A
+ Push (stack empty) + A
B Operand โ output + A B
* * > + precedence โ push + * A B
C Operand โ output + * A B C
- - โค * โ pop * to output + A B C *
- โค + โ pop + to output (empty) A B C * +
push - - A B C * +
D Operand โ output - A B C * + D
END Pop remaining (empty) A B C * + D -
Result: A B C * + D - โ
Postfix Evaluation using Stack
Evaluate: 3 5 2 * + (which is 3 + 5 * 2 = 13)
Token Action Stack โโโโโ โโโโโโ โโโโโ 3 Push [3] 5 Push [3, 5] 2 Push [3, 5, 2] * Pop 2,5 โ 5*2=10 [3, 10] + Pop 10,3 โ 3+10=13 [13] Result: 13 โ
2.3 Queues โ First In, First Out (FIFO)
Layer 1 โ Intuition
A queue is the line at a Swiggy delivery counter. The first order placed is the first one prepared and delivered. New orders join at the rear; completed orders leave from the front. No cutting in line!
Layer 2 โ Visual
Array-based Queue (Circular)
front=1 rear=4
โ โ
โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโ
โ โ 20 โ 30 โ 40 โ 50 โ โ
โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
[0] [1] [2] [3] [4] [5]
Enqueue(60): rear = (4+1) % 6 = 5, arr[5] = 60
Dequeue(): return arr[1]=20, front = (1+1) % 6 = 2
Circular trick: rear = (rear + 1) % capacity
This reuses space when front advances, avoiding the "false full" problem.
Priority Queue & Deque
| Variant | Rule | Real Example |
|---|---|---|
| Queue (FIFO) | First in, first out | Swiggy order processing |
| Priority Queue | Highest priority dequeued first | Ola: nearest driver gets the ride |
| Deque (Double-ended) | Insert/delete at both ends | Browser history โ add at front, remove old from back |
2.4 Recursion: Divide, Conquer, Combine
Layer 1 โ Intuition
Recursion is like Russian nesting dolls (Matryoshka). Open the big doll โ inside is a smaller doll. Open that โ even smaller. Keep opening until you find the tiny solid doll (base case). Then you "close" them back up in reverse order (returning from recursive calls). Each doll is the same shape, just smaller โ that's the recursive structure.
The Three Laws of Recursion
- Base Case: A condition where the function stops calling itself (the tiny doll)
- Recursive Case: The function calls itself with a SMALLER problem
- Progress: Each call must move TOWARD the base case
Missing base case = infinite recursion = stack overflow. Every recursive call adds a frame to the call stack. Without a base case, the stack grows until memory runs out โ Python hits RecursionError at depth ~1000, C just crashes with a segfault. Always write your base case FIRST.
Merge Sort โ O(n log n) guaranteed
Visual
[38, 27, 43, 3, 9, 82, 10]
/ \
[38, 27, 43, 3] [9, 82, 10] โ DIVIDE
/ \ / \
[38, 27] [43, 3] [9, 82] [10] โ DIVIDE
/ \ / \ / \ |
[38] [27] [43] [3] [9] [82] [10] โ BASE CASE (size 1)
\ / \ / \ / |
[27, 38] [3, 43] [9, 82] [10] โ MERGE
\ / \ /
[3, 27, 38, 43] [9, 10, 82] โ MERGE
\ /
[3, 9, 10, 27, 38, 43, 82] โ MERGE (final)
Quick Sort โ average O(n log n), worst O(nยฒ)
Visual
Pivot = last element. Partition: elements โค pivot go left, > pivot go right.
[10, 80, 30, 90, 40, 50, 70] pivot = 70
โ โ โ โ โ โ โ
[10, 30, 40, 50] [70] [80, 90] โ After partition
Then recursively sort left [10,30,40,50] and right [80,90].
| Algorithm | Best | Average | Worst | Space | Stable? |
|---|---|---|---|---|---|
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quick Sort | O(n log n) | O(n log n) | O(nยฒ) | O(log n) | No |
Quick Sort has O(nยฒ) worst case, yet it's used more often in practice than Merge Sort. Why? (Hint: Quick Sort is in-place with O(log n) space, while Merge Sort needs O(n) extra memory. For 1 billion elements, that's 4 GB of extra RAM. Also, Quick Sort has better cache locality.)
Lab Program Implementations
LAB PROGRAM 1: Stack โ Push and Pop (Array + Linked List)
Python โ Array-based Stack
Python
class ArrayStack:
"""Stack using a fixed-size array.
Industry: Paytm's payment gateway uses a stack to undo
transaction steps if OTP verification fails."""
def __init__(self, capacity):
self.stack = [None] * capacity
self.top = -1 # -1 means empty
self.capacity = capacity
def is_empty(self):
return self.top == -1
def is_full(self):
return self.top == self.capacity - 1
def push(self, value):
"""Add element to top. O(1)."""
if self.is_full():
print("Stack Overflow!")
return
self.top += 1
self.stack[self.top] = value
def pop(self):
"""Remove and return top element. O(1)."""
if self.is_empty():
print("Stack Underflow!")
return None
value = self.stack[self.top]
self.top -= 1
return value
def peek(self):
if self.is_empty(): return None
return self.stack[self.top]
def display(self):
print("Stack (topโbottom):", self.stack[self.top::-1] if self.top >= 0 else "(empty)")
# === Driver ===
s = ArrayStack(5)
for val in [10, 20, 30]:
s.push(val)
s.display()
print(f"Pop: {s.pop()}")
print(f"Peek: {s.peek()}")
s.display()
Python โ Linked-List-based Stack
Python
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedStack:
"""Stack using linked list โ no size limit (dynamic)."""
def __init__(self):
self.top = None
self.size = 0
def push(self, data):
new_node = Node(data)
new_node.next = self.top # New node points to old top
self.top = new_node # Update top
self.size += 1
def pop(self):
if not self.top:
print("Stack Underflow!"); return None
data = self.top.data
self.top = self.top.next # Move top to next
self.size -= 1
return data
def display(self):
curr, parts = self.top, []
while curr:
parts.append(str(curr.data)); curr = curr.next
print("Top โ " + " โ ".join(parts) + " โ NULL")
ls = LinkedStack()
for v in [100, 200, 300]: ls.push(v)
ls.display()
print(f"Pop: {ls.pop()}")
ls.display()
C โ Array-based Stack
C
#include <stdio.h>
#define MAX 100
int stack[MAX], top = -1;
void push(int val) {
if (top == MAX-1) { printf("Overflow!\n"); return; }
stack[++top] = val;
}
int pop() {
if (top == -1) { printf("Underflow!\n"); return -1; }
return stack[top--];
}
int main() {
push(10); push(20); push(30);
printf("Pop: %d\n", pop()); // 30
printf("Pop: %d\n", pop()); // 20
return 0;
}
LAB PROGRAM 2: Queue โ Enqueue and Dequeue (Array + Linked List)
Python โ Circular Array Queue
Python
class CircularQueue:
"""Queue using circular array โ prevents the 'false full' problem.
Industry: Swiggy's order pipeline uses this exact pattern."""
def __init__(self, capacity):
self.queue = [None] * capacity
self.front = -1
self.rear = -1
self.capacity = capacity
self.size = 0
def is_empty(self): return self.size == 0
def is_full(self): return self.size == self.capacity
def enqueue(self, value):
"""Add element at rear. O(1)."""
if self.is_full():
print("Queue is full!"); return
if self.is_empty():
self.front = 0
self.rear = (self.rear + 1) % self.capacity # Circular wrap
self.queue[self.rear] = value
self.size += 1
def dequeue(self):
"""Remove element from front. O(1)."""
if self.is_empty():
print("Queue is empty!"); return None
value = self.queue[self.front]
self.front = (self.front + 1) % self.capacity # Circular wrap
self.size -= 1
if self.is_empty(): # Reset if empty
self.front = self.rear = -1
return value
def display(self):
if self.is_empty(): print("Queue: (empty)"); return
parts = []
for i in range(self.size):
idx = (self.front + i) % self.capacity
parts.append(str(self.queue[idx]))
print("Front โ " + " โ ".join(parts) + " โ Rear")
# === Driver: Swiggy order queue ===
orders = CircularQueue(5)
orders.enqueue("Order#101-Biryani")
orders.enqueue("Order#102-Pizza")
orders.enqueue("Order#103-Dosa")
orders.display()
print(f"Processing: {orders.dequeue()}")
orders.enqueue("Order#104-Momos")
orders.display()
C โ Array Queue + Linked List Queue
C
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
/* Array-based circular queue */
int q[MAX], front = -1, rear = -1, qsize = 0;
void enqueue(int val) {
if (qsize == MAX) { printf("Full!\n"); return; }
if (front == -1) front = 0;
rear = (rear + 1) % MAX;
q[rear] = val; qsize++;
}
int dequeue() {
if (qsize == 0) { printf("Empty!\n"); return -1; }
int val = q[front];
front = (front + 1) % MAX;
qsize--;
return val;
}
/* Linked-list queue */
struct QNode { int data; struct QNode* next; };
struct QNode *qfront = NULL, *qrear = NULL;
void llEnqueue(int val) {
struct QNode* n = (struct QNode*)malloc(sizeof(struct QNode));
n->data = val; n->next = NULL;
if(!qrear) { qfront = qrear = n; return; }
qrear->next = n; qrear = n;
}
int llDequeue() {
if(!qfront) { printf("Empty!\n"); return -1; }
int val = qfront->data;
struct QNode* t = qfront;
qfront = qfront->next;
if(!qfront) qrear = NULL;
free(t);
return val;
}
int main() {
enqueue(10); enqueue(20); enqueue(30);
printf("Dequeue: %d\n", dequeue()); // 10
llEnqueue(100); llEnqueue(200);
printf("LL Dequeue: %d\n", llDequeue()); // 100
return 0;
}
LAB PROGRAM 3: Tower of Hanoi using Recursion
Python Implementation
Python
def tower_of_hanoi(n, source, auxiliary, destination):
"""Move n disks from source to destination using auxiliary.
The key insight:
1. Move n-1 disks from source to auxiliary (recursive)
2. Move the largest disk from source to destination (base move)
3. Move n-1 disks from auxiliary to destination (recursive)
Total moves = 2^n - 1. For n=64 (legend): 18 quintillion moves!
"""
if n == 1:
# Base case: only one disk โ move it directly
print(f"Move disk 1 from {source} to {destination}")
return
# Step 1: Move top n-1 disks out of the way
tower_of_hanoi(n - 1, source, destination, auxiliary)
# Step 2: Move the largest disk
print(f"Move disk {n} from {source} to {destination}")
# Step 3: Move the n-1 disks from auxiliary to destination
tower_of_hanoi(n - 1, auxiliary, source, destination)
print("=== Tower of Hanoi (3 disks) ===")
tower_of_hanoi(3, "A", "B", "C")
print(f"\nTotal moves for 3 disks: {2**3 - 1} = 7")
C Implementation
C
#include <stdio.h>
void hanoi(int n, char src, char aux, char dst) {
if (n == 1) {
printf("Move disk 1: %c -> %c\n", src, dst);
return;
}
hanoi(n-1, src, dst, aux);
printf("Move disk %d: %c -> %c\n", n, src, dst);
hanoi(n-1, aux, src, dst);
}
int main() {
hanoi(3, 'A', 'B', 'C');
return 0;
}
The Tower of Hanoi requires 2โฟ - 1 moves. For the legendary 64-disk version, that's 18,446,744,073,709,551,615 moves. At 1 move per second, it would take 585 billion years โ about 42 times the age of the universe. This is the power (and danger) of exponential growth.
LAB PROGRAM 4: Merge Sort and Quick Sort (Recursive)
Merge Sort โ Python
Python
def merge_sort(arr):
"""Divide array in half, sort each half, merge sorted halves.
Time: O(n log n) always. Space: O(n). Stable.
Used by: Python's Timsort, Java's Arrays.sort() for objects."""
if len(arr) <= 1: # Base case: single element is sorted
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # Sort left half
right = merge_sort(arr[mid:]) # Sort right half
return merge(left, right)
def merge(left, right):
"""Merge two sorted arrays into one sorted array. O(n)."""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= for stability
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:]) # Append remaining
result.extend(right[j:])
return result
data = [38, 27, 43, 3, 9, 82, 10]
print("Merge Sort:", merge_sort(data))
Quick Sort โ Python
Python
def quick_sort(arr, low, high):
"""In-place Quick Sort using Lomuto partition.
Average: O(n log n). Worst: O(nยฒ) on already-sorted input.
Used by: C's qsort(), Go's sort, Rust's sort_unstable."""
if low < high:
pivot_index = partition(arr, low, high)
quick_sort(arr, low, pivot_index - 1) # Sort left
quick_sort(arr, pivot_index + 1, high) # Sort right
def partition(arr, low, high):
"""Lomuto partition: last element as pivot."""
pivot = arr[high] # Choose last element as pivot
i = low - 1 # Boundary of "โค pivot" section
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # Swap to left side
arr[i + 1], arr[high] = arr[high], arr[i + 1] # Place pivot correctly
return i + 1
data = [10, 80, 30, 90, 40, 50, 70]
quick_sort(data, 0, len(data) - 1)
print("Quick Sort:", data)
C โ Merge Sort & Quick Sort
C
#include <stdio.h>
/* โโโ Merge Sort โโโ */
void merge(int a[], int l, int m, int r) {
int n1=m-l+1, n2=r-m, L[n1], R[n2];
for(int i=0;i<n1;i++) L[i]=a[l+i];
for(int j=0;j<n2;j++) R[j]=a[m+1+j];
int i=0,j=0,k=l;
while(i<n1 && j<n2) a[k++]=(L[i]<=R[j])?L[i++]:R[j++];
while(i<n1) a[k++]=L[i++];
while(j<n2) a[k++]=R[j++];
}
void mergeSort(int a[], int l, int r) {
if(l<r) { int m=l+(r-l)/2; mergeSort(a,l,m); mergeSort(a,m+1,r); merge(a,l,m,r); }
}
/* โโโ Quick Sort โโโ */
int partition(int a[], int lo, int hi) {
int pivot=a[hi], i=lo-1;
for(int j=lo;j<hi;j++)
if(a[j]<=pivot){i++; int t=a[i];a[i]=a[j];a[j]=t;}
int t=a[i+1]; a[i+1]=a[hi]; a[hi]=t;
return i+1;
}
void quickSort(int a[], int lo, int hi) {
if(lo<hi){int p=partition(a,lo,hi);quickSort(a,lo,p-1);quickSort(a,p+1,hi);}
}
int main() {
int a[]={38,27,43,3,9}, b[]={10,80,30,90,40};
mergeSort(a,0,4);
quickSort(b,0,4);
printf("Merge: "); for(int i=0;i<5;i++) printf("%d ",a[i]);
printf("\nQuick: "); for(int i=0;i<5;i++) printf("%d ",b[i]);
return 0;
}
Industry Case Studies
Case Study 1: Paytm โ Transaction Rollback Stack
Case Study 2: Amazon โ Order Processing Queue (AWS SQS)
Case Study 3: GCC/Clang โ Expression Evaluation Stack Machine
a + b * c) with operator precedence and parentheses. CPUs only understand sequential instructions. The compiler must convert infix to machine-executable form.Chapter Project โ "Build It Yourself"
๐ข Expression Evaluator & Bracket Validator
Pitch: Build a mini calculator that parses mathematical expressions with proper operator precedence, validates bracket matching, and converts between infix/postfix/prefix โ like building the core of a Python interpreter.
Problem Statement
You are a compiler engineering intern at a startup building a calculator app. Your task is to build a module that: (1) validates bracket matching, (2) converts infix to postfix, (3) evaluates postfix expressions, and (4) handles invalid input gracefully.
Step-by-Step Build Guide
- Step 1 โ Bracket Validator (Stack): Push opening brackets. On closing bracket, pop and check match. Uses Lab Program 1's stack.
- Step 2 โ Infix to Postfix Converter: Implement Shunting-yard (Section 2.2). Handle
+, -, *, /, ^with precedence. Uses Lab Program 1's stack for operators. - Step 3 โ Postfix Evaluator: Walk postfix tokens. Push numbers, pop-compute-push on operators. Uses Lab Program 1's stack.
- Step 4 โ Integration: Input:
"(3 + 5) * 2 - 4 / 2"โ Validate brackets โ Convert to postfix โ Evaluate โ Output:14.
Extension Challenges
- โญ Easy: Support variables โ
"x + y * 2"with a symbol table - โญโญ Medium: Add support for functions like
sin(),sqrt(),log() - โญโญโญ Hard: Build a full REPL (Read-Eval-Print Loop) with history (queue of past expressions)
Interview Corner โ "Crack It at Google/Amazon"
Q1: Valid Parentheses โญ [Product Company Interview]
Problem: Given a string of brackets ()[]{}โ, determine if every opening bracket has a matching closing bracket in the correct order.
Python
def is_valid(s):
stack = []
bracket_map = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in bracket_map: # Closing bracket
top = stack.pop() if stack else '#'
if bracket_map[char] != top:
return False # Mismatch!
else:
stack.append(char) # Opening bracket โ push
return len(stack) == 0 # Empty stack = all matched
print(is_valid("({[]})")) # True
print(is_valid("([)]")) # False โ interleaved
Time: O(n), Space: O(n). Classic stack problem โ asked at Amazon, Google, Microsoft.
Q2: Implement Queue Using Two Stacks โญโญ [GATE + Amazon]
Problem: Implement a FIFO queue using only LIFO stacks.
Python
class QueueFromStacks:
"""Key insight: reversing a stack gives you a queue!
Push to stack1. When dequeue, pour stack1 into stack2
(reversing order) and pop from stack2."""
def __init__(self):
self.stack_push = [] # For enqueue
self.stack_pop = [] # For dequeue
def enqueue(self, val):
self.stack_push.append(val)
def dequeue(self):
if not self.stack_pop: # Only transfer when pop stack empty
while self.stack_push:
self.stack_pop.append(self.stack_push.pop())
return self.stack_pop.pop() if self.stack_pop else None
q = QueueFromStacks()
q.enqueue(1); q.enqueue(2); q.enqueue(3)
print(q.dequeue()) # 1 (FIFO!)
print(q.dequeue()) # 2
Amortized O(1) per operation. Each element is moved at most twice (push once, pour once).
Q3: Sort a Stack Using Recursion โญโญโญ [Hard โ Google]
Problem: Sort a stack using only push, pop, peek, isEmpty โ no other data structure.
Python
def sort_stack(stack):
if not stack: return
top = stack.pop() # Remove top
sort_stack(stack) # Sort remaining (recursion!)
sorted_insert(stack, top) # Insert top in sorted position
def sorted_insert(stack, val):
# Base: empty or val >= top โ push
if not stack or val >= stack[-1]:
stack.append(val); return
top = stack.pop() # Hold elements greater than val
sorted_insert(stack, val)
stack.append(top) # Put them back on top
s = [34, 3, 31, 98, 92, 23]
sort_stack(s)
print(s) # [3, 23, 31, 34, 92, 98]
Time: O(nยฒ), Space: O(n) call stack. Uses the call stack as implicit temporary storage โ elegant recursion!
MCQ Assessment Bank โ 25 Questions
Level 1 โ Recall โญ (5 Questions)
Stack follows which discipline?
- FIFO
- LIFO
- Random access
- Priority-based
๐ Last In First Out โ the most recently pushed element is popped first.
โ (a) FIFO is a queue. (c) That's arrays. (d) That's priority queue.
The postfix form of A + B is:
+ A BA B +A + BAB+
A B +๐ Postfix = operator after operands. (With spaces for clarity; (d) is also correct but (b) is standard.)
โ (a) That's prefix. (c) That's infix.
Queue follows which discipline?
- LIFO
- FIFO
- LILO
- Random
๐ First In First Out โ like a line at a counter.
โ (a) That's a stack. (c) LILO isn't a standard term. (d) No.
Tower of Hanoi for n disks requires how many moves?
- nยฒ
- 2n
- 2โฟ - 1
- n!
๐ T(n) = 2ยทT(n-1) + 1, solving gives 2โฟ - 1. For n=3: 7 moves. For n=64: ~18 quintillion.
โ Others are wrong formulas.
Merge sort's time complexity is:
- O(nยฒ) worst case
- O(n log n) in all cases
- O(n) best case
- O(n log n) average, O(nยฒ) worst
๐ Merge sort always divides in half and merges โ no dependency on input order. Guaranteed O(n log n).
โ (d) That's quick sort, not merge sort.
Level 2 โ Conceptual โญโญ (6 Questions)
Function calls in any programming language use which data structure internally?
- Queue
- Stack (call stack)
- Array
- Linked list
๐ Each function call pushes a frame (local variables, return address) onto the call stack. Returning pops the frame. This is why infinite recursion causes "stack overflow."
โ (a) Functions return in LIFO, not FIFO order.
Why is a circular array queue better than a linear array queue?
- It's faster to enqueue
- It reuses space freed by dequeue, preventing the "false full" problem
- It uses less memory
- It supports priority operations
๐ In a linear queue, after dequeuing all elements, front=rear=end-of-array. The queue appears "full" even though all slots are empty. Circular wrapping (
rear = (rear+1) % size) reuses freed front slots.โ (a) Same speed. (c) Same memory. (d) No priority support.
Quick sort's worst case is O(nยฒ), which occurs when the input is already sorted. Therefore, quick sort should never be used on sorted data.
- True โ merge sort is always better
- False โ randomized pivot selection reduces worst case to expected O(n log n)
- True โ bubble sort is better for sorted data
- False โ O(nยฒ) is theoretical and never happens
๐ Production implementations (C's qsort, Java's DualPivotQuicksort) use randomized or median-of-three pivot selection, making the worst case extremely unlikely. Quick sort's O(log n) space advantage over merge sort's O(n) makes it preferred in practice.
โ (a) Too absolute. (d) It CAN happen with naive pivot, just rarely with random pivot.
A deque (double-ended queue) supports:
- Insert at front only
- Insert and delete at both front and rear
- Priority-based removal
- Random access like arrays
๐ Deque = "Double-Ended Queue." Supports insertFront, insertRear, deleteFront, deleteRear. It generalizes both stacks and queues.
โ Others are too restrictive or wrong.
Merge sort is stable but quick sort is not. "Stable" means:
- The algorithm doesn't crash on edge cases
- Equal elements maintain their original relative order after sorting
- The time complexity doesn't vary with input
- The algorithm is deterministic
๐ If two students have the same score, a stable sort keeps them in the same relative order as the input. Merge sort's
<= comparison ensures this. Quick sort's partitioning can swap equal elements past each other.โ (a)(c)(d) Not what "stable" means in sorting.
Every recursive function can be converted to an iterative one by using:
- A queue
- An explicit stack
- Additional arrays
- It can't be converted
๐ Recursion uses the call stack implicitly. You can always simulate this with an explicit stack data structure โ push parameters, pop when returning. DFS, tree traversals, and expression evaluation all have iterative stack-based versions.
โ (d) Every recursion CAN be converted.
Level 3 โ Application โญโญ (8 Questions)
Swiggy uses a queue for order processing. If 100 orders are enqueued per minute and kitchen processes 80 per minute, after 10 minutes the queue has:
- 100 orders
- 200 orders
- 180 orders
- 1000 orders
๐ Net growth: 100-80 = 20 orders/minute backlog. After 10 minutes: 20 ร 10 = 200 pending orders. This is why queue monitoring is critical โ unbounded growth crashes systems.
โ Others miscalculate.
Evaluate postfix: 6 2 3 + - 3 8 2 / + * 2 ^ 3 +
- 52
- 46
- 51
- 49
๐ 6 2 3+โ5 6-5โ1 3 8 2/โ4 3+4โ7 1*7โ7 7^2โ49 49+3โ Wait, let me retrace: 6,(2,3,+โ5),(6-5=1),(3,(8,2,/โ4),+โ7),(1*7=7),(7ยฒ=49),(49+3=52). Actually (a) 52. Trace carefully: Push 6,2,3. +: 2+3=5. -: 6-5=1. Push 3,8,2. /: 8/2=4. +: 3+4=7. *: 1*7=7. ^: 7ยฒ=49. +: 49+3=52.
โ Recalculated: answer is (a) 52.
What happens with: push(5), push(3), pop(), push(7), pop(), pop()?
- Returns 3, 7, 5
- Returns 5, 3, 7
- Returns 7, 3, 5
- Stack underflow
๐ push(5)โ[5], push(3)โ[5,3], pop()โ3 [5], push(7)โ[5,7], pop()โ7 [5], pop()โ5 []. Returns: 3, 7, 5.
โ (b) That's FIFO. (c) Wrong order. (d) No underflow โ 3 pops for 3 pushes.
Quick sort on an already-sorted array of 1 million elements with first element as pivot takes O(n log n).
- True โ quick sort is always O(n log n)
- False โ with first/last element pivot on sorted data, it degrades to O(nยฒ)
- True โ sorting an already sorted array is trivial
- False โ quick sort can't handle sorted arrays
๐ Trap! With first element as pivot on sorted data, every partition produces one empty subarray and one of size n-1. This gives n + (n-1) + (n-2) + ... = O(nยฒ). For 1M elements: ~500 billion operations instead of 20 million. Fix: use random pivot.
โ (a)(c) O(n log n) is NOT guaranteed for quick sort.
Ola needs to always assign rides to the nearest available driver. Which variant of queue is most appropriate?
- Simple FIFO queue
- Circular queue
- Priority queue (min-heap by distance)
- Deque
๐ "Nearest" = lowest distance = highest priority. A min-heap gives the nearest driver in O(log n). FIFO would assign by registration order, not proximity โ riders would get distant drivers.
โ (a)(b)(d) Don't consider priority.
The infix expression (A + B) * (C - D) in postfix is:
A B + C D - ** + A B - C DA B C D + - *A + B * C - D
A B + C D - *๐ (A+B) โ AB+, (C-D) โ CD-, then multiply: AB+CD-*.
โ (b) That's prefix. (c)(d) Incorrect order.
Merge sort for a linked list vs array:
- Linked list version is slower
- Linked list version uses O(1) extra space (vs O(n) for array)
- Same space complexity for both
- Linked list can't be merge sorted
๐ Array merge sort needs O(n) temporary array for merging. LL merge sort can merge by relinking pointers โ no extra array needed. This is why merge sort is the preferred sort for linked lists.
โ (a) Same time. (c)(d) Wrong.
Python has a default recursion limit of ~1000. What happens if you call a recursive function with depth 5000?
- It runs normally
- RecursionError (stack overflow)
- The program terminates silently
- It uses heap memory instead
๐ Python limits recursion depth to prevent C stack overflow.
sys.setrecursionlimit() can increase it, but deep recursion should be converted to iteration. C has no such protection โ it just segfaults.โ (a) Hits limit. (d) Python doesn't auto-convert.
Level 4 โ Analysis โญโญโญ (6 Questions)
Quick sort uses O(log n) space on average (for recursive stack), while merge sort uses O(n) extra space. For sorting 1 billion 32-bit integers, the difference is:
- Negligible
- ~4 GB (merge sort needs a temporary copy of the entire array)
- ~30 stack frames vs 4GB โ a factor of 100,000,000x
- Both (b) and (c)
๐ Merge sort: O(n) = 1 billion ร 4 bytes = ~4 GB extra. Quick sort: O(log n) = ~30 stack frames ร ~50 bytes = ~1.5 KB. The difference is literally 4GB vs 1.5KB. This is why quick sort is preferred for large in-memory sorts.
โ (a) Absolutely not negligible at this scale.
Implementing a stack using an array has a fundamental limitation that linked list stacks don't:
- Slower push/pop
- Fixed capacity โ push fails when full
- Can't store strings
- Requires more memory
๐ Array stacks must declare a size upfront. If exceeded, either overflow error or expensive reallocation. LL stacks grow dynamically โ push always succeeds (until system memory runs out). Trade-off: LL uses extra memory per node for the pointer.
โ (a) Same O(1). (c) Both can. (d) LL uses more per element.
Merge sort is always better than quick sort because it guarantees O(n log n).
- True โ guaranteed performance is always preferred
- False โ quick sort has better cache locality, less space, and is faster in practice despite worst case
- True โ O(nยฒ) worst case makes quick sort unusable
- False โ but only because quick sort is simpler to code
๐ In practice, quick sort's sequential array access gives excellent CPU cache performance. Its O(log n) space vs merge sort's O(n) matters enormously for large data. With randomized pivots, O(nยฒ) is vanishingly rare. Most language standard libraries use quick sort (or Introsort) โ not merge sort โ for arrays.
โ (a)(c) Too absolute. Production quick sort โ naive quick sort.
The recurrence relation for merge sort is T(n) = 2T(n/2) + O(n). Using the Master Theorem, this gives:
- O(n)
- O(nยฒ)
- O(n log n)
- O(log n)
๐ Master Theorem Case 2: a=2, b=2, f(n)=n. n^(log_b(a)) = n^1 = n = f(n). Result: O(n log n). This is the mathematical proof behind merge sort's complexity.
โ Others don't satisfy the recurrence.
You need to find the k-th smallest element in an unsorted array of n elements. Best approach?
- Sort with merge sort O(n log n), then access index k-1
- Use Quick Select (partition-based) โ average O(n)
- Use a min-heap โ O(n + k log n)
- All work, but (b) is optimal
๐ Quick Select uses quick sort's partition but only recurses into ONE half โ the half containing index k. Average O(n), worst O(nยฒ). With randomized pivot: expected O(n). This is faster than sorting the entire array.
โ All approaches work but have different complexities.
A priority queue is most efficiently implemented using:
- Sorted array โ O(n) insert, O(1) extract min
- Unsorted array โ O(1) insert, O(n) extract min
- Binary heap โ O(log n) insert AND extract min
- Linked list โ O(n) for both
๐ Heap gives O(log n) for BOTH operations. Sorted array has O(n) insert (shifting). Unsorted array has O(n) extract (scanning). Heap is the best balance โ this is why Python's
heapq and Java's PriorityQueue use binary heaps.โ Others have at least one O(n) operation.
Chapter Summary
5 Things Every Engineer Must Remember
- Stacks power undo, rollback, and expression evaluation โ Paytm's payment rollback, browser back button, and every compiler uses LIFO. If you need to "reverse the last action," you need a stack.
- Queues decouple fast producers from slow consumers โ Swiggy's order pipeline, Amazon SQS, print spoolers, and CPU schedulers all use FIFO to buffer work without losing requests.
- Recursion = trust the base case โ Write the base case first, then assume the recursive call works on smaller input. Tower of Hanoi looks impossible until you trust that
hanoi(n-1)just works. - Merge sort guarantees O(n log n) but needs O(n) space โ For 1 billion elements, that's 4 GB extra RAM. Quick sort uses O(log n) โ 1.5 KB. Space matters in production.
- Quick sort wins in practice despite O(nยฒ) worst case โ Randomized pivot makes worst case vanishingly rare. Cache locality and in-place operation make it 2-3x faster than merge sort on arrays. Every major language uses it.
Master Comparison Table
| Concept | Best | Average | Worst | Space | Use When | Avoid When |
|---|---|---|---|---|---|---|
| Stack (push/pop) | O(1) | O(1) | O(1) | O(n) | Undo, parsing, DFS | Need FIFO order |
| Queue (enqueue/dequeue) | O(1) | O(1) | O(1) | O(n) | BFS, task scheduling | Need LIFO order |
| Priority Queue | O(log n) | O(log n) | O(log n) | O(n) | Shortest path, scheduling | Simple FIFO suffices |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Linked lists, need stability | Memory-constrained |
| Quick Sort | O(n log n) | O(n log n) | O(nยฒ) | O(log n) | Arrays, in-memory data | Must guarantee worst case |
Coming Up Next: Unit 4 โ Trees
You've now mastered linear data structures โ arrays, linked lists, stacks, and queues. But what if your data is inherently hierarchical? Think about Nykaa's product categories: Beauty โ Skincare โ Moisturizers โ Under โน500. Or your computer's file system: C:\ โ Users โ Documents โ Projects. These aren't lists โ they're trees.
In Unit 4, we'll build binary trees and binary search trees โ structures where every search eliminates half the data (like binary search, but dynamic). We'll traverse trees in pre-order, in-order, and post-order, find the lowest common ancestor of two nodes, and implement the file system hierarchy that every OS uses. Trees are where DSA gets truly powerful โ and truly beautiful.