Stacks & QueuesProblem 5 of 38

Implement N stacks in an Array

Brute Force
Time: O(1)
Space: O(n*k)
Optimal
Time: O(1)
Space: O(n+k)

Problem Statement

Implement k stacks in a single array of size n. All stacks should be able to grow and shrink dynamically. The space should be used efficiently - a stack should only throw overflow when the entire array is full.

Example:

Array size n = 10, Number of stacks k = 3 push(2, 10) -> Stack 2 gets element 10 push(0, 5) -> Stack 0 gets element 5 push(1, 15) -> Stack 1 gets element 15 push(2, 20) -> Stack 2 gets element 20 pop(2) -> Returns 20 from Stack 2

Approach 1: Divide Array Equally (Brute Force)

Intuition

Divide the array of size n into k equal parts. Each stack gets n/k slots. This is simple but wastes space - one stack might overflow while others have plenty of space.

Algorithm

  1. Divide array into k equal parts, each of size n/k
  2. Stack i uses indices [i*(n/k), (i+1)*(n/k) - 1]
  3. Maintain separate top pointers for each stack
  4. Each stack is independent and limited to its partition
java
public class KStacksBruteForce {
    private int[] arr;
    private int[] tops;
    private int n, k;
    private int stackSize;
    
    public KStacksBruteForce(int n, int k) {
        this.n = n;
        this.k = k;
        this.stackSize = n / k;
        this.arr = new int[n];
        this.tops = new int[k];
        
        for (int i = 0; i < k; i++) {
            tops[i] = i * stackSize - 1;
        }
    }
    
    public void push(int stackNum, int x) {
        int start = stackNum * stackSize;
        int end = start + stackSize - 1;
        
        if (tops[stackNum] >= end) {
            throw new RuntimeException("Stack " + stackNum + " Overflow");
        }
        
        arr[++tops[stackNum]] = x;
    }
    
    public int pop(int stackNum) {
        int start = stackNum * stackSize;
        
        if (tops[stackNum] < start) {
            throw new RuntimeException("Stack " + stackNum + " Underflow");
        }
        
        return arr[tops[stackNum]--];
    }
    
    public int top(int stackNum) {
        int start = stackNum * stackSize;
        
        if (tops[stackNum] < start) {
            throw new RuntimeException("Stack " + stackNum + " is empty");
        }
        
        return arr[tops[stackNum]];
    }
    
    public boolean isEmpty(int stackNum) {
        return tops[stackNum] < stackNum * stackSize;
    }
    
    public static void main(String[] args) {
        KStacksBruteForce ks = new KStacksBruteForce(9, 3);
        
        ks.push(0, 10);
        ks.push(0, 20);
        ks.push(1, 30);
        ks.push(2, 40);
        
        System.out.println("Pop from stack 0: " + ks.pop(0)); // 20
        System.out.println("Pop from stack 1: " + ks.pop(1)); // 30
        System.out.println("Pop from stack 2: " + ks.pop(2)); // 40
    }
}

Complexity Analysis

  • Time Complexity: O(1) for all operations
  • Space Complexity: O(n*k) conceptually wastes space as each stack is limited

Approach 2: Using Free List (Optimal)

Intuition

Instead of dividing the array, use a free list to track available slots. Each slot can belong to any stack. Use two auxiliary arrays:

  • next[]: For each index, stores the next index in same stack OR next free slot
  • top[]: Stores the top index for each stack

Algorithm

  1. Initialize all slots as free, linking them: next[i] = i+1
  2. Maintain freeTop pointing to first free slot
  3. On push: use freeTop slot, update freeTop to next[freeTop], link new slot to stack's top
  4. On pop: add slot back to free list, update stack's top to next of popped slot
java
public class KStacks {
    private int[] arr;     // Actual array to store elements
    private int[] next;    // For occupied: next in stack, For free: next free
    private int[] top;     // Top index for each stack
    private int freeTop;   // Index of first free slot
    private int n, k;
    
    public KStacks(int n, int k) {
        this.n = n;
        this.k = k;
        
        arr = new int[n];
        next = new int[n];
        top = new int[k];
        
        // Initialize all slots as free
        for (int i = 0; i < n - 1; i++) {
            next[i] = i + 1;
        }
        next[n - 1] = -1;
        
        // All stacks are empty initially
        for (int i = 0; i < k; i++) {
            top[i] = -1;
        }
        
        // First free slot is 0
        freeTop = 0;
    }
    
    public void push(int stackNum, int x) {
        if (isFull()) {
            throw new RuntimeException("Stack Overflow - All stacks full");
        }
        
        // Get index of first free slot
        int i = freeTop;
        
        // Update freeTop to next free slot
        freeTop = next[i];
        
        // Store the element
        arr[i] = x;
        
        // Link this slot to previous top of this stack
        next[i] = top[stackNum];
        
        // Update top of this stack
        top[stackNum] = i;
    }
    
    public int pop(int stackNum) {
        if (isEmpty(stackNum)) {
            throw new RuntimeException("Stack " + stackNum + " Underflow");
        }
        
        // Get index of top element
        int i = top[stackNum];
        
        // Update top to next element in stack
        top[stackNum] = next[i];
        
        // Add this slot to free list
        next[i] = freeTop;
        freeTop = i;
        
        return arr[i];
    }
    
    public int peek(int stackNum) {
        if (isEmpty(stackNum)) {
            throw new RuntimeException("Stack " + stackNum + " is empty");
        }
        return arr[top[stackNum]];
    }
    
    public boolean isEmpty(int stackNum) {
        return top[stackNum] == -1;
    }
    
    public boolean isFull() {
        return freeTop == -1;
    }
    
    public static void main(String[] args) {
        KStacks ks = new KStacks(10, 3);
        
        // Push elements to different stacks
        ks.push(2, 15);
        ks.push(2, 45);
        ks.push(0, 11);
        ks.push(1, 17);
        ks.push(0, 39);
        ks.push(1, 49);
        
        System.out.println("Popped from stack 2: " + ks.pop(2)); // 45
        System.out.println("Popped from stack 1: " + ks.pop(1)); // 49
        System.out.println("Popped from stack 0: " + ks.pop(0)); // 39
        
        // Stack 2 can use more space now
        ks.push(2, 100);
        ks.push(2, 200);
        ks.push(2, 300);
        
        System.out.println("Top of stack 2: " + ks.peek(2)); // 300
    }
}

Complexity Analysis

  • Time Complexity: O(1) for all operations
  • Space Complexity: O(n + k) - n for array and next[], k for top[]

Key Takeaways

  1. Free list approach allows optimal space utilization - any stack can grow until array is full
  2. The next[] array serves dual purpose: links elements in a stack AND tracks free slots
  3. Think of free slots as a "free stack" - freeTop is its top, next[] links free slots
  4. When pushing: remove from free list, add to stack
  5. When popping: remove from stack, add to free list
  6. This is O(1) for all operations with O(n + k) extra space
  7. The brute force division approach wastes space and is not practical for uneven usage patterns