πŸ”’ Private Site

This site is password-protected.

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


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 initial top means β€œno elements exist.” When we push the first element, top becomes 0 β€” exactly the first valid index.
  • isEmpty: top == -1 β€” Directly follows from our convention. No elements = top is at -1.
  • isFull: top == MAX - 1 β€” If top has reached the last valid index, the array is full.
  • push: s->data[++(s->top)] = value β€” The ++ is pre-increment: first increment top, 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 at top, then decrement. This returns the current top element and logically removes it (the data stays in memory but top moves 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, move top to the next node, then free the old top. We must save the pointer in temp first because after s->top = temp->next, we’d lose access to the old node and couldn’t free it β€” causing a memory leak.
  • No isFull needed β€” The linked list grows dynamically. The only β€œfull” condition is when malloc fails (system out of memory), which is extremely rare.
  • Every node costs extra memory β€” Each node stores a next pointer (typically 4-8 bytes) in addition to the data. For small data types like int, 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 when top1 is before the array start. Stack 2 is empty when top2 is 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-increment top1, then store value.
  • push2: --(ts->top2) β€” Stack 2 grows left (decreasing index). Pre-decrement top2, then store value.
  • pop1: (ts->top1)-- β€” Return value at top1, then decrement (shrink right β†’ left).
  • pop2: (ts->top2)++ β€” Return value at top2, 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

  1. Reverse: C * )B + A( β†’ after swapping parentheses: C * (B + A)
  2. 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 + *
  3. 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) then left = 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, not right - 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:

  1. Base Case β€” the simplest case that can be solved directly (stops recursion)
  2. 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:

  1. Direct Recursion β€” A function calls itself directly.
  2. 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) calls factorial(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, funcA calls funcB which isn’t defined yet. We need void funcB(int n); as a prototype before funcA’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 case n ≀ 1 returns 1 because 0! = 1! = 1. Each call reduces n by 1, so after exactly n calls we hit the base case. The space is $O(n)$ because n stack 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) = 0 and fib(1) = 1. Returning n itself 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) calls fib(4) and fib(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 n disks from A to C, you need to: (1) move n-1 disks from A to B (using C as auxiliary), (2) move the largest disk from A to C, (3) move n-1 disks 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 top n-1 disks out of the way to the auxiliary peg. Notice how the roles of pegs change: aux becomes the destination, to becomes 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 the n-1 disks 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 n elements = last element + sum of first n-1 elements. Base case: sum of 0 elements is 0. We use arr[n-1] (not arr[n]) because the last element of an n-element array is at index n-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) / 2 can 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 need nested(106) = 96, then nested(96), which again needs nested(107) = 97, then nested(97), and so on β€” eventually reaching nested(101) = 91. So nested(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 remember n on 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 β€” front points to the first element, rear to the last. Starting rear at -1 means the queue is empty (since front > rear).
  • isEmpty: front > rear β€” When front overtakes rear, 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 before front is wasted even though it’s technically free.
  • ++(q->rear) β€” Pre-increment rear first, then place the value. Since rear starts at -1, the first enqueue puts the value at index 0.
  • (q->front)++ β€” Return the front element first (post-increment), then advance front. This means the dequeued slot is abandoned forever.
  • Why this design fails in practice β€” After many enqueue/dequeue cycles, front moves 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:

  • count field β€” Instead of computing fullness from front/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. When rear is at index MAX-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 rear starts at -1? β€” So that the first (rear + 1) % MAX gives 0, placing the first element at index 0. Alternative designs start rear at 0 and use rear as 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: front and rear β€” Unlike a stack (which only needs one end), a queue operates on BOTH ends. front for dequeue, rear for enqueue. Both start as NULL (empty queue).
  • isEmpty: front == NULL β€” If there’s no front node, the queue is empty. No isFull() needed β€” linked list grows dynamically until memory runs out.
  • Enqueue β€” special case isEmpty β€” For the very first element, both front and rear must point to it. After that, new nodes are always appended after rear.
  • q->rear->next = newNode; q->rear = newNode β€” Two-step append: (1) link the current last node to the new node, (2) update rear to the new node. Order matters β€” if you update rear first, 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, front becomes NULL. But rear still points to the freed node (dangling pointer). We must reset rear to NULL too, otherwise the next enqueue would try to access freed memory.
  • free(temp) β€” Prevents memory leak. We saved the front node in temp before advancing front, 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 next pointer.

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). Adding MAX before % prevents negative indices. E.g., if front = 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:

  1. Push(x): Enqueue x to empty queue q2. Then transfer all elements from q1 to q2 one by one. Swap q1 and q2.
  2. 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, q1 always 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 q1 is always the β€œtop” of the stack. Just dequeue.
  • Swap trick β€” Instead of copying queues, we swap the struct values. After the swap, q1 has the correct stack order and q2 is empty (ready for the next push).
  • Alternative approach β€” Make pop costly instead: push always enqueues to q1 in $O(1)$, but pop transfers $n-1$ elements to q2, 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 s2 is empty, pop all elements from s1 and push onto s2 (reverses order). Then pop from s2.
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:

  • s1 is the inbox, s2 is the outbox. Elements enter through s1 (newest on top), and leave through s2 (oldest on top).
  • Why transfer reverses order: If s1 has [3, 2, 1] (3 on top, 1 at bottom), popping all and pushing to s2 gives s2 = [1, 2, 3] (1 on top). Now pop(s2) gives 1 β€” the first element enqueued (FIFO!).
  • Lazy transfer: We only move elements to s2 when s2 is empty. If s2 still has elements, those are in the correct FIFO order already β€” just pop from s2.
  • Amortized $O(1)$: Each element is pushed and popped from s1 exactly once, and pushed and popped from s2 exactly 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.


πŸ“ View Complete Solutions for Unit III β†’

← Back to DSA Index