Unit III β Stacks & Queues
Syllabus Coverage: Stack and Queue as ADT. Operations on stack and queue. Implementations using arrays and dynamic memory allocation. Application of stack for expression conversion and expression evaluation, Recursion and stacks, Linear queue vs. Circular queue. (08 Hours)
Table of Contents
1 β Stack
- 1.1 Stack as an ADT
- 1.2 Stack Operations
- 1.3 Stack Implementation Using Array
- 1.4 Stack Implementation Using Linked List
- Practice Questions β Stack Basics
- 1.5 Two Stacks in One Array
- 1.6 Stack Permutations & Catalan Number
2 β Applications of Stack
- 2.1 Expression Types β Infix, Prefix, Postfix
- 2.2 Infix to Postfix Conversion
- 2.3 Infix to Prefix Conversion
- 2.4 Postfix Expression Evaluation
- 2.5 Prefix Expression Evaluation
- 2.6 Parenthesis Matching
- Practice Questions β Expression Conversion & Evaluation
3 β Recursion and Stacks
- 3.1 What is Recursion?
- 3.2 How Recursion Uses the Stack
- 3.3 Classic Recursive Problems
- 3.4 Nested Recursion & Tail Recursion
- 3.5 Recursion vs Iteration
- Practice Questions β Recursion
4 β Queue
- 4.1 Queue as an ADT
- 4.2 Queue Operations
- 4.3 Linear Queue Implementation Using Array
- 4.4 Problem with Linear Queue β Need for Circular Queue
- 4.5 Circular Queue Implementation
- 4.6 Queue Implementation Using Linked List
- 4.7 Linear Queue vs Circular Queue
- Practice Questions β Queue
- 4.8 Deque (Double-Ended Queue)
- 4.9 Priority Queue β Overview
- 4.10 Implementing Stack Using Two Queues
- 4.11 Implementing Queue Using Two Stacks
Glossary β Key Terms
| Term | Meaning |
|---|---|
| Stack | A linear data structure following LIFO (Last In, First Out) β the last element added is the first removed. |
| Queue | A linear data structure following FIFO (First In, First Out) β the first element added is the first removed. |
| LIFO | Last In, First Out β like a stack of plates. |
| FIFO | First In, First Out β like a line at a ticket counter. |
| Push | Add an element to the top of a stack. |
| Pop | Remove and return the top element of a stack. |
| Peek / Top | View the top element without removing it. |
| Overflow | Attempting to add to a full data structure. |
| Underflow | Attempting to remove from an empty data structure. |
| Enqueue | Add an element to the rear of a queue. |
| Dequeue | Remove an element from the front of a queue. |
| Infix | Operator between operands: A + B. |
| Prefix (Polish) | Operator before operands: + A B. |
| Postfix (Reverse Polish) | Operator after operands: A B +. |
| Circular Queue | A queue where the last position connects back to the first, forming a circle. |
| Recursion | A technique where a function calls itself to solve smaller sub-problems. |
| Base Case | The condition that stops recursion (prevents infinite calls). |
| Call Stack | The system stack that stores function activation records during recursion. |
1 β Stack
1.1 Stack as an ADT
Definition: A Stack is a linear data structure that follows the LIFO (Last In, First Out) principle. Elements can only be added or removed from one end, called the top.
Real-Life Analogies:
- Stack of plates in a cafeteria
- Pile of books on a desk
- Browser back button (most recent page first)
- Undo operation in text editors
ADT Stack:
Data:
An ordered collection of elements with LIFO access
A variable 'top' pointing to the topmost element
Operations:
push(element) β Insert element at top β O(1)
pop() β Remove and return top element β O(1)
peek() / top() β Return top element (no removal) β O(1)
isEmpty() β Return true if stack is empty β O(1)
isFull() β Return true if stack is full β O(1)
size() β Return number of elements β O(1)
Error Conditions:
pop() or peek() on empty stack β "Stack Underflow"
push() on full stack (array) β "Stack Overflow"
Visual:
βββββββ
top β β 30 β β last pushed, first popped
βββββββ€
β 20 β
βββββββ€
β 10 β β first pushed, last popped
βββββββ
1.2 Stack Operations
| Operation | Action | Time |
|---|---|---|
push(x) |
Add x on top; increment top |
$O(1)$ |
pop() |
Return top element; decrement top |
$O(1)$ |
peek() |
Return stack[top] without modifying |
$O(1)$ |
isEmpty() |
Check if top == -1 |
$O(1)$ |
isFull() |
Check if top == MAX - 1 (array only) |
$O(1)$ |
Trace: push(10), push(20), push(30), pop(), peek(), pop()
Operation Stack (bottomβtop) top Return
βββββββββ βββββββββββββββββ βββ ββββββ
push(10) [10] 0 β
push(20) [10, 20] 1 β
push(30) [10, 20, 30] 2 β
pop() [10, 20] 1 30
peek() [10, 20] 1 20
pop() [10] 0 20
1.3 Stack Implementation Using Array
#include <stdio.h>
#define MAX 100
typedef struct {
int data[MAX];
int top;
} Stack;
// Initialize stack
void init(Stack *s) {
s->top = -1;
}
// Check if stack is empty
int isEmpty(Stack *s) {
return s->top == -1;
}
// Check if stack is full
int isFull(Stack *s) {
return s->top == MAX - 1;
}
// Push operation
void push(Stack *s, int value) {
if (isFull(s)) {
printf("Stack Overflow!\n");
return;
}
s->data[++(s->top)] = value;
}
// Pop operation
int pop(Stack *s) {
if (isEmpty(s)) {
printf("Stack Underflow!\n");
return -1; // Error value
}
return s->data[(s->top)--];
}
// Peek operation
int peek(Stack *s) {
if (isEmpty(s)) {
printf("Stack is empty!\n");
return -1;
}
return s->data[s->top];
}
π Line-by-Line Logic β Array-Based Stack:
s->top = -1β Why -1? Because valid array indices start at 0. Using -1 as the initialtopmeans βno elements exist.β When we push the first element,topbecomes 0 β exactly the first valid index.isEmpty:top == -1β Directly follows from our convention. No elements = top is at -1.isFull:top == MAX - 1β Iftophas reached the last valid index, the array is full.push:s->data[++(s->top)] = valueβ The++is pre-increment: first incrementtop, then store at the new position. This ensures we write to the next empty slot, not overwrite the current top.pop:return s->data[(s->top)--]β The--is post-decrement: first read the value attop, then decrement. This returns the current top element and logically removes it (the data stays in memory buttopmoves down, so itβs treated as empty).- Overflow/Underflow checks β Every push/pop first checks for overflow/underflow. Forgetting these checks leads to array out-of-bounds bugs (writing past the array) or reading garbage values.
Pros: Simple, fast ($O(1)$ all operations), cache-friendly
Cons: Fixed size, can overflow
1.4 Stack Implementation Using Linked List
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
typedef struct {
Node *top;
} Stack;
void init(Stack *s) {
s->top = NULL;
}
int isEmpty(Stack *s) {
return s->top == NULL;
}
void push(Stack *s, int value) {
Node *newNode = (Node *)malloc(sizeof(Node));
if (!newNode) {
printf("Memory allocation failed!\n");
return;
}
newNode->data = value;
newNode->next = s->top; // New node points to old top
s->top = newNode; // Top now points to new node
}
int pop(Stack *s) {
if (isEmpty(s)) {
printf("Stack Underflow!\n");
return -1;
}
Node *temp = s->top;
int value = temp->data;
s->top = temp->next;
free(temp);
return value;
}
int peek(Stack *s) {
if (isEmpty(s)) {
printf("Stack is empty!\n");
return -1;
}
return s->top->data;
}
π Line-by-Line Logic β Linked List Stack:
s->top = NULLβ Empty stack = null pointer. No sentinel node needed.push:newNode->next = s->top; s->top = newNode;β Why insert at the head? Because the head of the linked list is the top of the stack. Inserting at the head is $O(1)$ β no traversal needed. The new node points to the old top, then becomes the new top. Think of it as stacking a new plate on top.pop:temp = s->top; s->top = temp->next; free(temp);β Save the top node, movetopto the next node, then free the old top. We must save the pointer intempfirst because afters->top = temp->next, weβd lose access to the old node and couldnβtfreeit β causing a memory leak.- No
isFullneeded β The linked list grows dynamically. The only βfullβ condition is whenmallocfails (system out of memory), which is extremely rare.- Every node costs extra memory β Each node stores a
nextpointer (typically 4-8 bytes) in addition to the data. For small data types likeint, this almost doubles the memory usage compared to arrays.
Pros: Dynamic size, no overflow (until memory exhausted)
Cons: Extra memory for pointers, slightly slower due to dynamic allocation
Array vs Linked List Stack
| Feature | Array Stack | Linked List Stack |
|---|---|---|
| Size | Fixed (static) | Dynamic |
| Overflow | Possible | Only when memory exhausted |
| Memory | Contiguous, compact | Extra pointer per node |
| Speed | Slightly faster (cache) | Slightly slower |
| Implementation | Simpler | More complex |
Practice Questions β Stack Basics
Q1. Define Stack as an ADT. List all operations with time complexities.
Q2. Implement a Stack using an array in C. Show push, pop, peek, isEmpty, isFull.
Q3. Implement a Stack using a linked list. What advantage does it have over array implementation?
Q4. Trace the following operations on an initially empty stack: push(5), push(3), pop(), push(7), push(2), pop(), peek().
Q5. What is Stack Overflow and Stack Underflow? When does each occur?
1.5 Two Stacks in One Array
Idea: Implement two stacks using a single array of size $n$. Stack 1 grows from the left (index 0 β right), and Stack 2 grows from the right (index $n-1$ β left). They share the free space between them.
Array: [S1β ... free space ... βS2]
0 top1 top2 n-1
Overflow occurs only when the two tops meet: top1 + 1 == top2.
#include <stdio.h>
#define MAX 100
typedef struct {
int data[MAX];
int top1; // Top of stack 1 (grows left β right)
int top2; // Top of stack 2 (grows right β left)
} TwoStacks;
void init(TwoStacks *ts) {
ts->top1 = -1;
ts->top2 = MAX;
}
int isFull(TwoStacks *ts) {
return ts->top1 + 1 == ts->top2;
}
void push1(TwoStacks *ts, int value) {
if (isFull(ts)) { printf("Stack Overflow!\n"); return; }
ts->data[++(ts->top1)] = value;
}
void push2(TwoStacks *ts, int value) {
if (isFull(ts)) { printf("Stack Overflow!\n"); return; }
ts->data[--(ts->top2)] = value;
}
int pop1(TwoStacks *ts) {
if (ts->top1 == -1) { printf("Stack 1 Underflow!\n"); return -1; }
return ts->data[(ts->top1)--];
}
int pop2(TwoStacks *ts) {
if (ts->top2 == MAX) { printf("Stack 2 Underflow!\n"); return -1; }
return ts->data[(ts->top2)++];
}
π Line-by-Line Logic:
top1 = -1,top2 = MAXβ Stack 1 is empty whentop1is before the array start. Stack 2 is empty whentop2is past the array end. They start at opposite extremes.isFull:top1 + 1 == top2β The stacks share the array. Overflow happens only when they collide β no space between them. This is more memory-efficient than splitting the array in half (which wastes space if one stack is much fuller than the other).push1:++(ts->top1)β Stack 1 grows right (increasing index). Pre-incrementtop1, then store value.push2:--(ts->top2)β Stack 2 grows left (decreasing index). Pre-decrementtop2, then store value.pop1:(ts->top1)--β Return value attop1, then decrement (shrink right β left).pop2:(ts->top2)++β Return value attop2, then increment (shrink left β right).- Why this approach? β Both stacks share the entire arrayβs free space. If stack 1 needs 90% of the space and stack 2 needs only 10%, that works fine. A naΓ―ve split (first half for S1, second half for S2) would waste space.
Trace (MAX=8): push1(10), push1(20), push2(50), push2(40), push1(30), pop2()
| Operation | Array | top1 | top2 |
|---|---|---|---|
| init | [_, _, _, _, _, _, _, _] | -1 | 8 |
| push1(10) | [10, _, _, _, _, _, _, _] | 0 | 8 |
| push1(20) | [10, 20, _, _, _, _, _, _] | 1 | 8 |
| push2(50) | [10, 20, _, _, _, _, _, 50] | 1 | 7 |
| push2(40) | [10, 20, _, _, _, _, 40, 50] | 1 | 6 |
| push1(30) | [10, 20, 30, _, _, _, 40, 50] | 2 | 6 |
| pop2()β40 | [10, 20, 30, _, _, _, _, 50] | 2 | 7 |
1.6 Stack Permutations & Catalan Number
Stack Permutation: Given an input sequence $1, 2, 3, \ldots, n$ being pushed onto a stack, a stack permutation is any output sequence that can be obtained by a series of push and pop operations (where each element is pushed exactly once and popped exactly once).
Example ($n = 3$, input: 1, 2, 3):
| Push/Pop Sequence | Output | Valid? |
|---|---|---|
| push 1, pop 1, push 2, pop 2, push 3, pop 3 | 1, 2, 3 | β |
| push 1, push 2, pop 2, pop 1, push 3, pop 3 | 2, 1, 3 | β |
| push 1, push 2, push 3, pop 3, pop 2, pop 1 | 3, 2, 1 | β |
| push 1, push 2, pop 2, push 3, pop 3, pop 1 | 2, 3, 1 | β |
| push 1, pop 1, push 2, push 3, pop 3, pop 2 | 1, 3, 2 | β |
| β | 3, 1, 2 | β |
Why is 3, 1, 2 INVALID? To output 3 first, we must push 1, 2, 3 onto the stack and pop 3. Now the stack has [1, 2] (2 on top). To output 1 next, weβd need to pop 2 first β but then 2 comes before 1 in the output, giving 3, 2, 1 (not 3, 1, 2).
Rule: A permutation $p_1, p_2, \ldots, p_n$ is a valid stack permutation if and only if there is no subsequence $p_i, p_j, p_k$ with $i < j < k$ where $p_k < p_i < p_j$ (i.e., no 231 pattern).
Catalan Number β Counting Valid Stack Permutations:
The number of valid stack permutations of $n$ elements is the $n$-th Catalan number:
\[C_n = \frac{1}{n+1}\binom{2n}{n} = \frac{(2n)!}{(n+1)! \cdot n!}\]| $n$ | $C_n$ | Valid permutations |
|---|---|---|
| 1 | 1 | {1} |
| 2 | 2 | {1,2}, {2,1} |
| 3 | 5 | {1,2,3}, {1,3,2}, {2,1,3}, {2,3,1}, {3,2,1} |
| 4 | 14 | 14 different permutations |
| 5 | 42 | 42 different permutations |
Catalan Numbers appear everywhere in CS:
- Number of valid stack permutations of $n$ elements
- Number of structurally different binary trees with $n$ nodes
- Number of ways to correctly match $n$ pairs of parentheses
- Number of ways to triangulate a polygon with $n+2$ sides
2 β Applications of Stack
2.1 Expression Types β Infix, Prefix, Postfix
| Notation | Operator Position | Example | No Parentheses Needed? |
|---|---|---|---|
| Infix | Between operands | A + B |
No β needs parentheses for precedence |
| Prefix (Polish) | Before operands | + A B |
Yes β |
| Postfix (Reverse Polish) | After operands | A B + |
Yes β |
Why Convert to Prefix/Postfix?
- Infix is natural for humans but ambiguous without parentheses and precedence rules
- Postfix/Prefix are unambiguous β no need for parentheses, no precedence rules
- Computers can evaluate postfix/prefix expressions very efficiently using a stack
Operator Precedence (highest to lowest)
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (highest) | ^ (exponentiation) |
Right to Left |
| 2 | *, / |
Left to Right |
| 3 | +, - |
Left to Right |
| 4 (lowest) | (, ) |
β |
Conversion Examples
| Infix | Postfix | Prefix |
|---|---|---|
A + B |
A B + |
+ A B |
A + B * C |
A B C * + |
+ A * B C |
(A + B) * C |
A B + C * |
* + A B C |
A + B * C - D |
A B C * + D - |
- + A * B C D |
A * (B + C) / D |
A B C + * D / |
/ * A + B C D |
(A + B) * (C - D) |
A B + C D - * |
* + A B - C D |
2.2 Infix to Postfix Conversion
Algorithm (Shunting-Yard Algorithm)
INPUT: Infix expression (string)
OUTPUT: Postfix expression (string)
1. Create an empty stack (for operators) and an empty output string
2. Scan the infix expression left to right:
a. If OPERAND (letter/number): Add directly to output
b. If '(': Push onto stack
c. If ')': Pop and add to output until '(' is found. Discard '('.
d. If OPERATOR:
- While stack is not empty AND top of stack is not '(' AND
(precedence of top β₯ precedence of current operator):
Pop from stack and add to output
- Push current operator onto stack
3. After scanning, pop all remaining operators from stack to output
Detailed Worked Example
Convert: A + B * C - D
| Step | Symbol | Stack | Output | Explanation |
|---|---|---|---|---|
| 1 | A | Β | A | Operand β output |
| 2 | + | + | A | Push operator |
| 3 | B | + | A B | Operand β output |
| 4 | * | + * | A B | * has higher precedence than +, push |
| 5 | C | + * | A B C | Operand β output |
| 6 | - | - | A B C * + | - has β€ precedence of * β pop *; β€ precedence of + β pop +; push - |
| 7 | D | - | A B C * + D | Operand β output |
| End | Β | Β | A B C * + D - | Pop remaining |
Result: A B C * + D - β
Convert: (A + B) * (C - D)
| Step | Symbol | Stack | Output | Explanation |
|---|---|---|---|---|
| 1 | ( | ( | Β | Push ( |
| 2 | A | ( | A | Operand |
| 3 | + | ( + | A | Push (inside parenthesis) |
| 4 | B | ( + | A B | Operand |
| 5 | ) | Β | A B + | Pop until (, discard ( |
| 6 | * | * | A B + | Push * |
| 7 | ( | * ( | A B + | Push ( |
| 8 | C | * ( | A B + C | Operand |
| 9 | - | * ( - | A B + C | Push (inside parenthesis) |
| 10 | D | * ( - | A B + C D | Operand |
| 11 | ) | * | A B + C D - | Pop until ( |
| End | Β | Β | A B + C D - * | Pop remaining |
Result: A B + C D - * β
2.3 Infix to Prefix Conversion
Algorithm
1. Reverse the infix expression (and swap '(' β ')')
2. Apply infix-to-postfix algorithm on the reversed expression
3. Reverse the result β this is the prefix expression
Convert: (A + B) * C to Prefix
- Reverse:
C * )B + A(β after swapping parentheses:C * (B + A) - Apply postfix conversion to
C * (B + A):- C β output:
C *β stack:*(β stack:* (- B β output:
C B +β stack:* ( +- A β output:
C B A )β pop until(: output:C B A +; stack:*- End: pop remaining: output:
C B A + *
- C β output:
- Reverse:
* + A B C
Result: * + A B C β
2.4 Postfix Expression Evaluation
Algorithm
1. Scan postfix expression left to right
2. If OPERAND: Push onto stack
3. If OPERATOR:
- Pop two operands (second popped is LEFT operand)
- Apply operator: result = left_operand OPERATOR right_operand
- Push result back onto stack
4. After scanning, the stack contains one element β the final result
Evaluate: 6 3 2 * + 4 -
| Step | Symbol | Stack | Action |
|---|---|---|---|
| 1 | 6 | 6 | Push operand |
| 2 | 3 | 6, 3 | Push operand |
| 3 | 2 | 6, 3, 2 | Push operand |
| 4 | * | 6, 6 | Pop 2 and 3; compute 3*2=6; push 6 |
| 5 | + | 12 | Pop 6 and 6; compute 6+6=12; push 12 |
| 6 | 4 | 12, 4 | Push operand |
| 7 | - | 8 | Pop 4 and 12; compute 12-4=8; push 8 |
Result: 8 β
C Implementation
int evaluatePostfix(char* exp) {
Stack s;
init(&s);
for (int i = 0; exp[i] != '\0'; i++) {
if (exp[i] >= '0' && exp[i] <= '9') {
push(&s, exp[i] - '0'); // Convert char to int
} else if (exp[i] != ' ') {
int right = pop(&s);
int left = pop(&s);
switch (exp[i]) {
case '+': push(&s, left + right); break;
case '-': push(&s, left - right); break;
case '*': push(&s, left * right); break;
case '/': push(&s, left / right); break;
}
}
}
return pop(&s); // Final result
}
π Line-by-Line Logic:
exp[i] - '0'β Converts ASCII character to integer. In ASCII,'0'=48, '1'=49, ..., '9'=57. So'5' - '0' = 53 - 48 = 5. This only works for single-digit numbers.exp[i] != ' 'β Skip spaces (used as delimiters between tokens).right = pop(&s)thenleft = pop(&s)β Order matters! The first pop gives the RIGHT operand (it was pushed last). The second pop gives the LEFT operand. For+and*this doesnβt matter (commutative), but for-and/the order is critical:left - right, notright - left.- Why a stack? β In postfix notation, when we encounter an operator, its operands are always the two most recently seen values. The stack naturally gives us βmost recent firstβ (LIFO), making it a perfect match.
return pop(&s)β After processing the entire expression, exactly one value remains on the stack β the final result. If thereβs more than one value left, the expression was malformed.
2.5 Prefix Expression Evaluation
Algorithm
1. Scan prefix expression from RIGHT to LEFT
2. If OPERAND: Push onto stack
3. If OPERATOR:
- Pop two operands (first popped is LEFT operand β opposite of postfix!)
- Apply operator: result = left_operand OPERATOR right_operand
- Push result
4. Stack contains final result
Evaluate: - + 6 * 3 2 4 (scanning right to left)
| Step | Symbol | Stack | Action |
|---|---|---|---|
| 1 | 4 | 4 | Push |
| 2 | 2 | 4, 2 | Push |
| 3 | 3 | 4, 2, 3 | Push |
| 4 | * | 4, 6 | Pop 3 and 2; compute 3*2=6; push |
| 5 | 6 | 4, 6, 6 | Push |
| 6 | + | 4, 12 | Pop 6 and 6; compute 6+6=12; push |
| 7 | - | 8 | Pop 12 and 4; compute 12-4=8; push |
Result: 8 β
2.6 Parenthesis Matching
A classic stack application β check if parentheses in an expression are balanced.
int isBalanced(char* exp) {
Stack s;
init(&s);
for (int i = 0; exp[i] != '\0'; i++) {
char ch = exp[i];
if (ch == '(' || ch == '[' || ch == '{') {
push(&s, ch);
}
else if (ch == ')' || ch == ']' || ch == '}') {
if (isEmpty(&s)) return 0; // No matching open bracket
char top = pop(&s);
if ((ch == ')' && top != '(') ||
(ch == ']' && top != '[') ||
(ch == '}' && top != '{'))
return 0; // Mismatched
}
}
return isEmpty(&s); // Stack should be empty if balanced
}
π Line-by-Line Logic:
- Opening brackets β push β Every opening bracket is a βpromiseβ that a matching closing bracket will come later. We store it on the stack.
- Closing bracket β pop and match β When we see a closing bracket, the most recent unmatched opening bracket (stack top) MUST be its matching type. If
}appears but stack top is(, thereβs a mismatch.isEmpty(&s)check before pop β If we see a closing bracket but the stack is empty, thereβs no opening bracket to match it β immediately unbalanced.return isEmpty(&s)β After processing all characters, the stack must be empty. If there are leftover opening brackets on the stack, they were never closed β unbalanced. This catch handles cases like"(()".- Why a stack? β Brackets must be matched in nested order: the most recently opened bracket must be closed first (LIFO). This exactly mirrors the stackβs behavior.
| Expression | Result | Reason |
|---|---|---|
{[()]} |
β Balanced | Every bracket has a match |
{[(])} |
β Unbalanced | ( is closed by ] |
((()) |
β Unbalanced | Extra ( left in stack |
Practice Questions β Expression Conversion & Evaluation
Q1. Convert the following infix expressions to postfix and prefix:
- (a)
A + B * C - D / E - (b)
(A + B) * (C - D) / E - (c)
A * (B + C * D) + E - (d)
((A + B) - C * (D / E)) + F
Q2. Evaluate the following postfix expressions:
- (a)
5 3 + 8 2 - * - (b)
2 3 1 * + 9 - - (c)
4 5 + 7 2 - *
Q3. Evaluate the following prefix expressions:
- (a)
+ * 2 3 4 - (b)
- + 7 * 4 5 + 2 0
Q4. Write C code to convert infix to postfix using a stack.
Q5. Write a C function to check if parentheses in an expression are balanced.
3 β Recursion and Stacks
3.1 What is Recursion?
Definition: Recursion is a technique where a function calls itself to solve a smaller instance of the same problem. Every recursive function must have:
- Base Case β the simplest case that can be solved directly (stops recursion)
- Recursive Case β the function calls itself with a smaller/simpler input
// Template for recursive functions:
returnType function(parameters) {
if (base_condition) // Base case
return base_value;
else // Recursive case
return function(smaller_parameters);
}
Types of Recursion:
- Direct Recursion β A function calls itself directly.
- Indirect Recursion β A function calls another function, which eventually calls the first function back.
// Direct Recursion β function A calls A
void directRecursion(int n) {
if (n <= 0) return;
printf("%d ", n);
directRecursion(n - 1); // A calls A
}
// Indirect Recursion β A calls B, B calls A
void funcB(int n); // Forward declaration
void funcA(int n) {
if (n <= 0) return;
printf("%d ", n);
funcB(n - 1); // A calls B
}
void funcB(int n) {
if (n <= 0) return;
printf("%d ", n);
funcA(n / 2); // B calls A
}
π Why This Matters:
- Direct recursion is the common case β
factorial(n)callsfactorial(n-1). The function name appears in its own body.- Indirect recursion creates a cycle: A β B β A β B β β¦ Each function in the chain must have a base case, otherwise the cycle never terminates.
- Forward declaration needed β In C,
funcAcallsfuncBwhich isnβt defined yet. We needvoid funcB(int n);as a prototype beforefuncAβs definition.- Both types use the call stack the same way β each call creates an activation record regardless of whether itβs direct or indirect.
3.2 How Recursion Uses the Stack
Every time a function is called, the system creates an activation record (also called a stack frame) on the call stack containing:
- Return address
- Local variables
- Parameters
- Saved registers
Example: factorial(4)
int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
Call Stack Trace:
Call: factorial(4)
Call: factorial(3)
Call: factorial(2)
Call: factorial(1)
Returns: 1 β base case hit
Returns: 2 * 1 = 2 β unwinds
Returns: 3 * 2 = 6
Returns: 4 * 6 = 24
Final Answer: 24
Stack frames at deepest point:
ββββββββββββββββββββ
β factorial(1) β β top (current)
β n=1, returns 1 β
ββββββββββββββββββββ€
β factorial(2) β
β n=2 β
ββββββββββββββββββββ€
β factorial(3) β
β n=3 β
ββββββββββββββββββββ€
β factorial(4) β
β n=4 β
ββββββββββββββββββββ€
β main() β β bottom
ββββββββββββββββββββ
Stack Overflow in Recursion: If the base case is missing or never reached, the function calls itself infinitely, filling up the call stack β Stack Overflow error!
// BAD β no base case!
int bad(int n) {
return n * bad(n); // Infinite recursion β Stack Overflow
}
3.3 Classic Recursive Problems
Factorial
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Time: O(n) Space: O(n) β recursion depth
π Why This Logic:
n! = n Γ (n-1)!is the mathematical definition itself. The base casen β€ 1returns 1 because0! = 1! = 1. Each call reducesnby 1, so after exactlyncalls we hit the base case. The space is $O(n)$ becausenstack frames exist simultaneously at the deepest point.
Fibonacci
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Time: O(2βΏ) β exponential! Space: O(n)
π Why This Logic:
if (n <= 1) return nβ Base cases:fib(0) = 0andfib(1) = 1. Returningnitself handles both.fibonacci(n-1) + fibonacci(n-2)β Directly mirrors the mathematical recurrence $F(n) = F(n-1) + F(n-2)$.- Why is it $O(2^n)$? β Each call makes TWO recursive calls, creating a binary tree of calls.
fib(5)callsfib(4)andfib(3), which each call two more, etc. Many sub-problems are computed repeatedly (e.g.,fib(3)is computed multiple times). This is grossly inefficient β memoization or iteration reduces it to $O(n)$.- Space is $O(n)$, not $O(2^n)$ β Because at any time, only one branch of the recursion tree is on the stack (depth = $n$). The tree is wide but the stack only tracks the current path.
Fibonacci Call Count Analysis:
For fib(n), the total number of function calls (additions) is exactly $\text{fib}(n+1) - 1$.
| $n$ | $\text{fib}(n)$ | Total calls | Call tree |
|---|---|---|---|
| 0 | 0 | 1 | Just returns |
| 1 | 1 | 1 | Just returns |
| 2 | 1 | 3 | fib(2) β fib(1), fib(0) |
| 3 | 2 | 5 | fib(3) β fib(2), fib(1) |
| 4 | 3 | 9 | fib(4) β fib(3), fib(2) |
| 5 | 5 | 15 | Grows exponentially! |
Why? Let $T(n)$ = number of calls for fib(n). Then:
- $T(0) = T(1) = 1$ (base cases β just 1 call)
- $T(n) = T(n-1) + T(n-2) + 1$ (two recursive calls + the call itself)
This recurrence solves to $T(n) = 2 \cdot F(n+1) - 1$ where $F$ is the Fibonacci sequence.
Tower of Hanoi
void hanoi(int n, char from, char to, char aux) {
if (n == 1) {
printf("Move disk 1 from %c to %c\n", from, to);
return;
}
hanoi(n - 1, from, aux, to); // Move n-1 disks to auxiliary
printf("Move disk %d from %c to %c\n", n, from, to);
hanoi(n - 1, aux, to, from); // Move n-1 disks to target
}
// Number of moves: 2βΏ - 1
// Time: O(2βΏ) Space: O(n)
π Line-by-Line Logic:
- The key insight: To move
ndisks from A to C, you need to: (1) moven-1disks from A to B (using C as auxiliary), (2) move the largest disk from A to C, (3) moven-1disks from B to C (using A as auxiliary).if (n == 1)β Base case: a single disk can be moved directly. No recursion needed.hanoi(n-1, from, aux, to)β Move the topn-1disks out of the way to the auxiliary peg. Notice how the roles of pegs change:auxbecomes the destination,tobecomes the auxiliary.printf("Move disk %d...")β Now the bottom disk (largest) is exposed and can be moved directly to the target.hanoi(n-1, aux, to, from)β Move then-1disks from auxiliary to target, completing the puzzle.- Why $2^n - 1$ moves? β The recurrence is $T(n) = 2T(n-1) + 1$, which solves to $T(n) = 2^n - 1$. For 3 disks: 7 moves. For 64 disks: $1.8 \times 10^{19}$ moves (the legend says the world ends when this is done!).
Sum of Array
int sumArray(int arr[], int n) {
if (n == 0) return 0;
return arr[n - 1] + sumArray(arr, n - 1);
}
// Time: O(n) Space: O(n)
π Why This Logic: The sum of
nelements = last element + sum of firstn-1elements. Base case: sum of 0 elements is 0. We usearr[n-1](notarr[n]) because the last element of ann-element array is at indexn-1. Each call reduces the problem size by 1.
Binary Search (Recursive)
int binarySearch(int arr[], int low, int high, int key) {
if (low > high) return -1;
int mid = low + (high - low) / 2;
if (arr[mid] == key) return mid;
if (arr[mid] < key)
return binarySearch(arr, mid + 1, high, key);
else
return binarySearch(arr, low, mid - 1, key);
}
// Time: O(log n) Space: O(log n)
π Line-by-Line Logic:
if (low > high) return -1β Base case: if search range is empty, the key doesnβt exist. This happens when weβve narrowed the window to nothing.low + (high - low) / 2β Computes midpoint without integer overflow. The naΓ―ve(low + high) / 2can overflow if both are large. This is a classic defensive coding trick.arr[mid] == keyβ Found it! Return the index.arr[mid] < keyβ search right half β If the middle element is too small, the key (if it exists) must be in the right half[mid+1, high].- else β search left half β Middle is too large, so search
[low, mid-1].- Space is $O(\log n)$ β Each recursive call adds a stack frame, and we halve the problem each time, so maximum recursion depth is $\log_2 n$. The iterative version in Unit 2 uses $O(1)$ space.
3.4 Nested Recursion & Tail Recursion
3. Nested Recursion β The argument of the recursive call is itself a recursive call:
\[f(n) = f(f(n - 1))\]The inner recursive call must complete first before the outer call can proceed. This makes the recursion expand deeply before collapsing.
// Nested Recursion Example
int nested(int n) {
if (n > 100)
return n - 10;
else
return nested(nested(n + 11)); // Argument is a recursive call!
}
π Line-by-Line Logic:
- This is the famous McCarthy 91 function. For any $n \leq 100$, it always returns 91.
- Why itβs tricky: To evaluate
nested(95), we first neednested(106) = 96, thennested(96), which again needsnested(107) = 97, thennested(97), and so on β eventually reachingnested(101) = 91. Sonested(95) = 91.- Key difference from direct/indirect: In direct recursion, the argument is a simple expression like
n-1. In nested recursion, the argument is another recursive call, making the evaluation order non-obvious.
Trace: nested(97)
nested(97)
= nested(nested(108)) // 108 > 100, so nested(108) = 98
= nested(98)
= nested(nested(109)) // nested(109) = 99
= nested(99)
= nested(nested(110)) // nested(110) = 100
= nested(100)
= nested(nested(111)) // nested(111) = 101
= nested(101)
= 91 // 101 > 100, return 101 - 10 = 91
For any $n \leq 100$: nested(n) = 91. For $n > 100$: nested(n) = n - 10.
Tail Recursion β A recursive call is tail recursive if the recursive call is the last operation performed by the function before returning. There is no pending computation after the recursive call returns.
// Tail Recursive β recursive call is the LAST thing
int factorialTail(int n, int acc) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc); // Nothing to do after this returns
}
// Usage: factorialTail(5, 1)
// Non-Tail Recursive β multiplication happens AFTER the recursive call
int factorialNonTail(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Must multiply n after call returns
}
π Why Tail Recursion Matters:
- Compiler optimization (TCO β Tail Call Optimization): A tail-recursive function can be automatically converted to a loop by the compiler, using $O(1)$ stack space instead of $O(n)$.
- Non-tail:
n * factorial(n-1)β the function must remembernon the stack because it needs to multiply after the recursive call returns. Each call adds a stack frame β $O(n)$ space.- Tail:
factorialTail(n-1, n*acc)β the result is fully computed in the argument. The current frame can be reused for the next call since nothing is needed later.- Rule of thumb: If you can convert a recursive function to use an accumulator parameter, itβs tail-recursive.
Summary of Recursion Types:
| Type | Description | Example |
|---|---|---|
| Direct | Function calls itself | f(n) = f(n-1) |
| Indirect | A calls B, B calls A | A(n) β B(n-1) β A(n-2) |
| Nested | Argument is a recursive call | f(n) = f(f(n-1)) |
| Tail | Recursive call is the last operation | f(n, acc) = f(n-1, n*acc) |
| Non-Tail (Head) | Work remains after recursive call | f(n) = n * f(n-1) |
3.5 Recursion vs Iteration
| Feature | Recursion | Iteration |
|---|---|---|
| Uses | Function calls (call stack) | Loops (for, while) |
| Termination | Base case | Loop condition becomes false |
| Memory | $O(n)$ or more (stack frames) | $O(1)$ (just loop variables) |
| Speed | Slower (function call overhead) | Faster |
| Code | Often shorter, more elegant | Can be longer but straightforward |
| Risk | Stack overflow for deep recursion | Infinite loop |
| Best for | Problems with natural recursive structure (trees, divide & conquer) | Simple repetitive tasks |
Every recursive solution can be converted to an iterative one (using an explicit stack if needed). However, some problems (like tree traversals, Tower of Hanoi) are much more naturally expressed with recursion.
Practice Questions β Recursion
Q1. Explain how the system stack is used during recursion. Draw the stack for factorial(5).
Q2. Write a recursive function to find the GCD of two numbers.
Q3. Write a recursive function to reverse a string.
Q4. Write a recursive function to check if a string is a palindrome.
Q5. Solve the Tower of Hanoi for $n = 3$ disks. How many moves are needed?
Q6. Compare recursion and iteration. When is recursion preferred?
Q7. What happens if you forget the base case? Explain with an example.
Q8. Convert the recursive Fibonacci into an iterative version. Compare time complexity.
4 β Queue
4.1 Queue as an ADT
Definition: A Queue is a linear data structure that follows the FIFO (First In, First Out) principle. Elements are added at the rear and removed from the front.
Real-Life Analogies:
- Queue at a ticket counter
- Print jobs waiting for a printer
- CPU task scheduling
- Customers at a bank
ADT Queue:
Data:
An ordered collection of elements with FIFO access
Two pointers: 'front' and 'rear'
Operations:
enqueue(element) β Add element at rear β O(1)
dequeue() β Remove and return front element β O(1)
front() / peek() β Return front element (no removal) β O(1)
isEmpty() β Return true if queue is empty β O(1)
isFull() β Return true if queue is full β O(1)
size() β Return number of elements β O(1)
Error Conditions:
dequeue() on empty queue β "Queue Underflow"
enqueue() on full queue β "Queue Overflow"
Visual:
dequeue β [10 | 20 | 30 | 40 | 50] β enqueue
β β
front rear
4.2 Queue Operations
| Operation | Action | Time |
|---|---|---|
enqueue(x) |
Add x at rear; increment rear |
$O(1)$ |
dequeue() |
Return element at front; increment front |
$O(1)$ |
peek() |
Return queue[front] |
$O(1)$ |
isEmpty() |
Check if front > rear (linear) or count == 0 |
$O(1)$ |
4.3 Linear Queue Implementation Using Array
#include <stdio.h>
#define MAX 100
typedef struct {
int data[MAX];
int front, rear;
} Queue;
void init(Queue *q) {
q->front = 0;
q->rear = -1;
}
int isEmpty(Queue *q) {
return q->front > q->rear;
}
int isFull(Queue *q) {
return q->rear == MAX - 1;
}
void enqueue(Queue *q, int value) {
if (isFull(q)) {
printf("Queue Overflow!\n");
return;
}
q->data[++(q->rear)] = value;
}
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("Queue Underflow!\n");
return -1;
}
return q->data[(q->front)++];
}
int peek(Queue *q) {
if (isEmpty(q)) {
printf("Queue is empty!\n");
return -1;
}
return q->data[q->front];
}
π Line-by-Line Logic:
front = 0,rear = -1βfrontpoints to the first element,rearto the last. Startingrearat-1means the queue is empty (sincefront > rear).isEmpty:front > rearβ Whenfrontovertakesrear, all elements have been dequeued.isFull:rear == MAX - 1β The rear has reached the last index. Note: this is the source of the false overflow problem β space beforefrontis wasted even though itβs technically free.++(q->rear)β Pre-incrementrearfirst, then place the value. Sincerearstarts at-1, the first enqueue puts the value at index 0.(q->front)++β Return the front element first (post-increment), then advancefront. This means the dequeued slot is abandoned forever.- Why this design fails in practice β After many enqueue/dequeue cycles,
frontmoves forward, leaving wasted slots behind. Even if only 1 element is in the queue,isFull()may return true. This is why circular queues exist.
4.4 Problem with Linear Queue β Need for Circular Queue
The Major Problem: In a linear queue, once elements are dequeued, the space at the front is wasted and cannot be reused (even if the queue is not actually full).
After several enqueue/dequeue operations:
[ _ | _ | _ | 40 | 50 ] front=3, rear=4
β β β
wasted space!
isFull() returns TRUE (rear == MAX-1) even though 3 slots are empty!
This problem is called βfalse overflowβ or βmemory wastageβ.
Solution: Use a Circular Queue where the rear wraps around to the beginning.
4.5 Circular Queue Implementation
In a Circular Queue, when rear reaches the end of the array, it wraps around to index 0 (if space is available). This is achieved using the modulo operator: rear = (rear + 1) % MAX.
Visual:
βββββ¬ββββ¬ββββ¬ββββ¬ββββ
β 40β 50β β 10β 20β Circular view:
βββββ΄ββββ΄ββββ΄ββββ΄ββββ
0 1 2 3 4 rear=1, front=3
β β
rear front
#include <stdio.h>
#define MAX 5
typedef struct {
int data[MAX];
int front, rear;
int count; // Track number of elements
} CircularQueue;
void init(CircularQueue *q) {
q->front = 0;
q->rear = -1;
q->count = 0;
}
int isEmpty(CircularQueue *q) {
return q->count == 0;
}
int isFull(CircularQueue *q) {
return q->count == MAX;
}
void enqueue(CircularQueue *q, int value) {
if (isFull(q)) {
printf("Queue Overflow!\n");
return;
}
q->rear = (q->rear + 1) % MAX; // Wrap around!
q->data[q->rear] = value;
q->count++;
}
int dequeue(CircularQueue *q) {
if (isEmpty(q)) {
printf("Queue Underflow!\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX; // Wrap around!
q->count--;
return value;
}
int peek(CircularQueue *q) {
if (isEmpty(q)) return -1;
return q->data[q->front];
}
π Line-by-Line Logic:
countfield β Instead of computing fullness fromfront/rear(which is error-prone in circular queues), we track the count explicitly.isEmpty=count == 0,isFull=count == MAX. Clean and unambiguous.rear = (rear + 1) % MAXβ The heart of the circular queue. Whenrearis at indexMAX-1(the last slot),(MAX-1 + 1) % MAX = 0β it wraps back to the beginning! This reuses slots that were freed by dequeue.front = (front + 1) % MAXβ Same wrap-around logic for the front pointer. After dequeuing from the last index, front wraps to 0.- Why modulo
%? β It creates a logical circle from a linear array. Index sequence:0, 1, 2, ..., MAX-1, 0, 1, 2, ...endlessly. This is the same trick used in hash tables and ring buffers.- No wasted space β Unlike the linear queue, every slot can be reused. The queue is truly full only when
count == MAX.- Why
rearstarts at-1? β So that the first(rear + 1) % MAXgives 0, placing the first element at index 0. Alternative designs startrearat 0 and userearas the next insertion point.
Dry Run
Circular Queue (MAX=5): enqueue(10), enqueue(20), enqueue(30), dequeue(), dequeue(), enqueue(40), enqueue(50), enqueue(60)
| Operation | Array State | front | rear | count |
|---|---|---|---|---|
| init | [_, _, _, _, _] | 0 | -1 | 0 |
| enqueue(10) | [10, _, _, _, _] | 0 | 0 | 1 |
| enqueue(20) | [10, 20, _, _, _] | 0 | 1 | 2 |
| enqueue(30) | [10, 20, 30, _, _] | 0 | 2 | 3 |
| dequeue()β10 | [_, 20, 30, _, _] | 1 | 2 | 2 |
| dequeue()β20 | [_, _, 30, _, _] | 2 | 2 | 1 |
| enqueue(40) | [_, _, 30, 40, _] | 2 | 3 | 2 |
| enqueue(50) | [_, _, 30, 40, 50] | 2 | 4 | 3 |
| enqueue(60) | [60, _, 30, 40, 50] | 2 | 0 | 4 |
Notice how rear wraps from index 4 to index 0! This is the power of circular queue.
4.6 Queue Implementation Using Linked List
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
typedef struct {
Node *front;
Node *rear;
} Queue;
void init(Queue *q) {
q->front = q->rear = NULL;
}
int isEmpty(Queue *q) {
return q->front == NULL;
}
void enqueue(Queue *q, int value) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = value;
newNode->next = NULL;
if (isEmpty(q)) {
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
}
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("Queue Underflow!\n");
return -1;
}
Node *temp = q->front;
int value = temp->data;
q->front = q->front->next;
if (q->front == NULL) // Queue became empty
q->rear = NULL;
free(temp);
return value;
}
π Line-by-Line Logic:
- Two pointers:
frontandrearβ Unlike a stack (which only needs one end), a queue operates on BOTH ends.frontfor dequeue,rearfor enqueue. Both start asNULL(empty queue).isEmpty:front == NULLβ If thereβs no front node, the queue is empty. NoisFull()needed β linked list grows dynamically until memory runs out.- Enqueue β special case
isEmptyβ For the very first element, bothfrontandrearmust point to it. After that, new nodes are always appended afterrear.q->rear->next = newNode; q->rear = newNodeβ Two-step append: (1) link the current last node to the new node, (2) updaterearto the new node. Order matters β if you updaterearfirst, you lose the link to the old last node.if (q->front == NULL) q->rear = NULLβ Critical edge case in dequeue! If we just removed the last element,frontbecomesNULL. Butrearstill points to the freed node (dangling pointer). We must resetreartoNULLtoo, otherwise the nextenqueuewould try to access freed memory.free(temp)β Prevents memory leak. We saved the front node intempbefore advancingfront, so we can safely free it.- Advantages over array queue β No fixed size, no false overflow, no wasted space. Trade-off: extra memory per node for the
nextpointer.
4.7 Linear Queue vs Circular Queue
| Feature | Linear Queue | Circular Queue |
|---|---|---|
| Structure | Straight line | Circle (wraps around) |
| Memory Utilization | Poor β wasted space after dequeue | Excellent β reuses freed space |
| Full Condition | rear == MAX - 1 (may be βfalse fullβ) |
count == MAX (truly full) |
| Empty Condition | front > rear |
count == 0 |
| Implementation | Simpler | Slightly complex (modulo arithmetic) |
| Overflow | Frequent (false overflow) | Only when actually full |
| Use Cases | Simple, one-time use | OS scheduling, buffering, BFS |
Always prefer Circular Queue over Linear Queue in practical applications. Linear Queue wastes memory and reports false overflow. Circular Queue solves both problems elegantly using the modulo operator.
Practice Questions β Queue
Q1. Define Queue as an ADT. List all operations.
Q2. Implement a Linear Queue using arrays. Identify its drawback.
Q3. Implement a Circular Queue using arrays. Show how it solves the memory wastage problem.
Q4. Implement a Queue using a linked list. What are the advantages?
Q5. Trace the following on a Circular Queue of size 4: enqueue(1), enqueue(2), enqueue(3), dequeue(), dequeue(), enqueue(4), enqueue(5), enqueue(6).
Q6. Compare Linear Queue and Circular Queue in detail.
Q7. What is the difference between a Stack and a Queue? Give real-world examples of each.
Q8. Can you implement a Queue using two Stacks? Write the algorithm.
4.8 Deque (Double-Ended Queue)
Definition: A Deque (pronounced βdeckβ) or Double-Ended Queue is a generalized queue where insertion and deletion can occur at both ends (front and rear).
insertFront / deleteFront ββ [10 | 20 | 30 | 40 | 50] ββ insertRear / deleteRear
β β
front rear
Two Restricted Variants:
| Type | Insertion | Deletion |
|---|---|---|
| Input-Restricted Deque | Only at one end (rear) | At both ends |
| Output-Restricted Deque | At both ends | Only at one end (front) |
#include <stdio.h>
#define MAX 100
typedef struct {
int data[MAX];
int front, rear, count;
} Deque;
void init(Deque *dq) {
dq->front = 0;
dq->rear = -1;
dq->count = 0;
}
int isEmpty(Deque *dq) { return dq->count == 0; }
int isFull(Deque *dq) { return dq->count == MAX; }
void insertRear(Deque *dq, int value) {
if (isFull(dq)) { printf("Deque Overflow!\n"); return; }
dq->rear = (dq->rear + 1) % MAX;
dq->data[dq->rear] = value;
dq->count++;
}
void insertFront(Deque *dq, int value) {
if (isFull(dq)) { printf("Deque Overflow!\n"); return; }
dq->front = (dq->front - 1 + MAX) % MAX;
dq->data[dq->front] = value;
dq->count++;
}
int deleteFront(Deque *dq) {
if (isEmpty(dq)) { printf("Deque Underflow!\n"); return -1; }
int value = dq->data[dq->front];
dq->front = (dq->front + 1) % MAX;
dq->count--;
return value;
}
int deleteRear(Deque *dq) {
if (isEmpty(dq)) { printf("Deque Underflow!\n"); return -1; }
int value = dq->data[dq->rear];
dq->rear = (dq->rear - 1 + MAX) % MAX;
dq->count--;
return value;
}
π Line-by-Line Logic:
(front - 1 + MAX) % MAXβ Moving front backward (inserting at front). AddingMAXbefore%prevents negative indices. E.g., iffront = 0: $(0 - 1 + 100) \% 100 = 99$ β wraps to the end.(rear - 1 + MAX) % MAXβ Same wrap-around logic for deleting from rear.- Deque is a superset β It can act as a Stack (insert and delete from one end only) OR a Queue (insert at one end, delete from the other). This makes it the most versatile linear data structure.
- Input-restricted deque = essentially a queue that allows deletion from both ends.
- Output-restricted deque = essentially a queue that allows insertion at both ends.
Deque generalizes both Stack and Queue:
- Use only
insertRear+deleteRearβ Stack behavior (LIFO) - Use
insertRear+deleteFrontβ Queue behavior (FIFO) - Use all four operations β Full Deque flexibility
4.9 Priority Queue β Overview
Definition: A Priority Queue is an abstract data type where each element has an associated priority. Elements are dequeued in order of priority (not insertion order).
- Max-Priority Queue: Highest priority element is dequeued first.
- Min-Priority Queue: Lowest priority element is dequeued first.
| Implementation | Insert | Delete (highest priority) |
|---|---|---|
| Unsorted Array | $O(1)$ | $O(n)$ |
| Sorted Array | $O(n)$ | $O(1)$ |
| Binary Heap | $O(\log n)$ | $O(\log n)$ |
| BST | $O(\log n)$ avg | $O(\log n)$ avg |
When to use Priority Queue:
- CPU scheduling (higher priority processes first)
- Dijkstraβs shortest path algorithm
- Huffman encoding
- Event-driven simulation
Priority Queues are covered in depth in Unit V under Binary Heaps.
4.10 Implementing Stack Using Two Queues
Problem: Implement a Stack (LIFO) using only two Queues (FIFO). This demonstrates how one ADT can simulate another.
Approach β Make push costly:
- Push(x): Enqueue
xto empty queueq2. Then transfer all elements fromq1toq2one by one. Swapq1andq2. - Pop(): Simply dequeue from
q1.
Push(1): q1: [1] q2: []
Push(2): Enqueue 2 to q2: [2], transfer q1βq2: [2, 1], swap β q1: [2, 1] q2: []
Push(3): Enqueue 3 to q2: [3], transfer q1βq2: [3, 2, 1], swap β q1: [3, 2, 1] q2: []
Pop(): Dequeue from q1 β 3 β (LIFO!)
#include <stdio.h>
#define MAX 100
typedef struct {
int data[MAX];
int front, rear, count;
} Queue;
void initQ(Queue *q) { q->front = 0; q->rear = -1; q->count = 0; }
int isEmptyQ(Queue *q) { return q->count == 0; }
void enqueue(Queue *q, int val) {
q->rear = (q->rear + 1) % MAX;
q->data[q->rear] = val;
q->count++;
}
int dequeue(Queue *q) {
int val = q->data[q->front];
q->front = (q->front + 1) % MAX;
q->count--;
return val;
}
// Stack using two queues
typedef struct {
Queue q1, q2;
} StackUsing2Q;
void initStack(StackUsing2Q *s) {
initQ(&s->q1);
initQ(&s->q2);
}
void push(StackUsing2Q *s, int value) {
// Step 1: Enqueue to q2
enqueue(&s->q2, value);
// Step 2: Transfer all from q1 to q2
while (!isEmptyQ(&s->q1)) {
enqueue(&s->q2, dequeue(&s->q1));
}
// Step 3: Swap q1 and q2
Queue temp = s->q1;
s->q1 = s->q2;
s->q2 = temp;
}
int pop(StackUsing2Q *s) {
if (isEmptyQ(&s->q1)) {
printf("Stack Underflow!\n");
return -1;
}
return dequeue(&s->q1);
}
π Line-by-Line Logic:
- Key idea: After each
push,q1always has elements in LIFO (stack) order, with the most recently pushed element at the front of the queue.- Push is $O(n)$ β We transfer all existing elements to maintain the order. Every push moves $n$ elements.
- Pop is $O(1)$ β The front of
q1is always the βtopβ of the stack. Just dequeue.- Swap trick β Instead of copying queues, we swap the struct values. After the swap,
q1has the correct stack order andq2is empty (ready for the next push).- Alternative approach β Make
popcostly instead: push always enqueues toq1in $O(1)$, but pop transfers $n-1$ elements toq2, dequeues the last one, then swaps. This gives $O(1)$ push and $O(n)$ pop.
| Operation | q1 (main) | q2 (helper) | Output |
|---|---|---|---|
| push(10) | [10] | [] | β |
| push(20) | [20, 10] | [] | β |
| push(30) | [30, 20, 10] | [] | β |
| pop() | [20, 10] | [] | 30 |
| pop() | [10] | [] | 20 |
| push(40) | [40, 10] | [] | β |
| pop() | [10] | [] | 40 |
4.11 Implementing Queue Using Two Stacks
Problem: Implement a Queue (FIFO) using only two Stacks (LIFO).
This is the symmetric problem to implementing Stack using two Queues.
Approach β Dequeue-Costly (Lazy Transfer):
- Enqueue: Always push onto
s1β $O(1)$ - Dequeue: If
s2is empty, pop all elements froms1and push ontos2(reverses order). Then pop froms2.
typedef struct {
Stack s1; // Input stack (enqueue goes here)
Stack s2; // Output stack (dequeue comes from here)
} QueueUsing2S;
void initQueue(QueueUsing2S *q) {
initStack(&q->s1);
initStack(&q->s2);
}
void enqueue(QueueUsing2S *q, int value) {
push(&q->s1, value); // Always push to s1
}
int dequeue(QueueUsing2S *q) {
if (isEmpty(&q->s2)) {
// Transfer all from s1 to s2 (reverses order β FIFO)
while (!isEmpty(&q->s1)) {
push(&q->s2, pop(&q->s1));
}
}
if (isEmpty(&q->s2)) {
printf("Queue Underflow!\n");
return -1;
}
return pop(&q->s2);
}
π Line-by-Line Logic:
s1is the inbox,s2is the outbox. Elements enter throughs1(newest on top), and leave throughs2(oldest on top).- Why transfer reverses order: If
s1has [3, 2, 1] (3 on top, 1 at bottom), popping all and pushing tos2givess2= [1, 2, 3] (1 on top). Nowpop(s2)gives 1 β the first element enqueued (FIFO!).- Lazy transfer: We only move elements to
s2whens2is empty. Ifs2still has elements, those are in the correct FIFO order already β just pop froms2.- Amortized $O(1)$: Each element is pushed and popped from
s1exactly once, and pushed and popped froms2exactly once. Total: 4 operations per element across its lifetime β amortized $O(1)$ per enqueue/dequeue.
Trace:
| Operation | s1 (topβ) | s2 (topβ) | Output |
|---|---|---|---|
| enqueue(10) | [10] | [] | β |
| enqueue(20) | [20, 10] | [] | β |
| enqueue(30) | [30, 20, 10] | [] | β |
| dequeue() | [] | [10, 20, 30] | 10 (transferred, then popped) |
| dequeue() | [] | [20, 30] | 20 |
| enqueue(40) | [40] | [20, 30] | β |
| dequeue() | [40] | [30] | 30 (s2 not empty, just pop) |
| dequeue() | [] | [40] | 40 (transferred, then popped) |
Comparison β Both Approaches:
| Approach | Enqueue | Dequeue | Best When |
|---|---|---|---|
| Enqueue-costly | $O(n)$ β transfer all to s2, push, transfer back | $O(1)$ | Mostly dequeue operations |
| Dequeue-costly (lazy) | $O(1)$ | Amortized $O(1)$, worst-case $O(n)$ | General use (preferred) |
The lazy transfer approach is preferred because it gives amortized $O(1)$ for both operations.
Comprehensive Unit III Practice Set
Short Answer
1. Define Stack and Queue. Give two real-world examples of each.
2. What is LIFO and FIFO? How do they differ?
3. Explain overflow and underflow conditions for both stack and queue.
4. What are the three types of expression notation? Why are postfix and prefix preferred by computers?
5. Explain the role of the system stack in recursion.
Tracing Problems
6. Convert to postfix and evaluate: (5 + 3) * 2 - 8 / 4
7. Convert to prefix: A * B + C / D - E
8. Evaluate postfix: 3 4 * 2 5 * + 1 -
9. Trace recursion for fibonacci(5). Draw the call tree and show stack frames.
10. Trace Circular Queue (size 5): enqueue(A), enqueue(B), enqueue(C), dequeue(), dequeue(), enqueue(D), enqueue(E), enqueue(F), enqueue(G), dequeue()
Implementation
11. Write a C program that converts an infix expression to postfix and evaluates it.
12. Implement a Stack that supports getMin() in $O(1)$ time (Min Stack).
13. Implement a Queue using two Stacks. Analyze the time complexity of enqueue and dequeue.
Long Answer
14. Compare array-based and linked-list-based implementations of Stack and Queue. Discuss pros, cons, and when to use each.
15. Explain recursion with examples of: Factorial, Fibonacci, Tower of Hanoi. For each, write the recurrence relation, draw the recursion tree, and analyze time and space complexity.
16. Explain the Circular Queue completely β why itβs needed, how it works, implementation with C code, and a full trace example.