Stacks & QueuesProblem 3 of 38

Implement 2 stack in an array

Brute Force
Time: O(1)
Space: O(n)
Optimal
Time: O(1)
Space: O(n)

Problem Statement

Implement two stacks in a single array efficiently. Both stacks should be able to grow until the entire array is full (not just half of it).

Example:

Array size: 10 Stack1: push(1), push(2) -> Uses indices 0, 1 Stack2: push(10), push(20) -> Uses indices 9, 8 Stack1: [1, 2, _, _, _, _, _, _, 20, 10] :Stack2 (grows towards center)

Approach 1: Divide Array in Two Halves (Brute Force)

Intuition

Simply divide the array into two halves. First stack uses the first half (0 to n/2-1), second stack uses the second half (n/2 to n-1). This is simple but wasteful as one stack might overflow while the other has space.

Algorithm

  1. Divide array of size n into two halves
  2. Stack1 uses indices [0, n/2-1], Stack2 uses [n/2, n-1]
  3. Stack1 grows left to right, Stack2 also grows left to right in its half
  4. Each stack can only use n/2 space even if other stack is empty
java
public class TwoStacksBruteForce {
    private int[] arr;
    private int capacity;
    private int top1, top2;
    private int mid;
    
    public TwoStacksBruteForce(int n) {
        capacity = n;
        arr = new int[n];
        mid = n / 2;
        top1 = -1;
        top2 = mid - 1;
    }
    
    public void push1(int x) {
        if (top1 >= mid - 1) {
            throw new RuntimeException("Stack1 Overflow");
        }
        arr[++top1] = x;
    }
    
    public void push2(int x) {
        if (top2 >= capacity - 1) {
            throw new RuntimeException("Stack2 Overflow");
        }
        arr[++top2] = x;
    }
    
    public int pop1() {
        if (top1 < 0) {
            throw new RuntimeException("Stack1 Underflow");
        }
        return arr[top1--];
    }
    
    public int pop2() {
        if (top2 < mid) {
            throw new RuntimeException("Stack2 Underflow");
        }
        return arr[top2--];
    }
    
    public static void main(String[] args) {
        TwoStacksBruteForce ts = new TwoStacksBruteForce(10);
        ts.push1(1);
        ts.push1(2);
        ts.push2(10);
        ts.push2(20);
        System.out.println("Pop from Stack1: " + ts.pop1()); // 2
        System.out.println("Pop from Stack2: " + ts.pop2()); // 20
    }
}

Complexity Analysis

  • Time Complexity: O(1) for all operations
  • Space Complexity: O(n), but wastes space as each stack limited to n/2

Approach 2: Two Ends Approach (Optimal)

Intuition

Start Stack1 from the beginning (index 0) and Stack2 from the end (index n-1). Stack1 grows towards right, Stack2 grows towards left. Both stacks can use the entire array space as long as they don't overlap.

Algorithm

  1. Initialize top1 = -1 (Stack1 starts before index 0)
  2. Initialize top2 = n (Stack2 starts after index n-1)
  3. For push1: increment top1, check if top1 < top2
  4. For push2: decrement top2, check if top1 < top2
  5. Overflow when top1 + 1 == top2
java
public class TwoStacks {
    private int[] arr;
    private int capacity;
    private int top1, top2;
    
    public TwoStacks(int n) {
        capacity = n;
        arr = new int[n];
        top1 = -1;
        top2 = n;
    }
    
    public void push1(int x) {
        // Check for overflow (stacks meeting)
        if (top1 + 1 == top2) {
            throw new RuntimeException("Stack Overflow");
        }
        arr[++top1] = x;
    }
    
    public void push2(int x) {
        // Check for overflow (stacks meeting)
        if (top1 + 1 == top2) {
            throw new RuntimeException("Stack Overflow");
        }
        arr[--top2] = x;
    }
    
    public int pop1() {
        if (top1 < 0) {
            throw new RuntimeException("Stack1 Underflow");
        }
        return arr[top1--];
    }
    
    public int pop2() {
        if (top2 >= capacity) {
            throw new RuntimeException("Stack2 Underflow");
        }
        return arr[top2++];
    }
    
    public int peek1() {
        if (top1 < 0) {
            throw new RuntimeException("Stack1 is empty");
        }
        return arr[top1];
    }
    
    public int peek2() {
        if (top2 >= capacity) {
            throw new RuntimeException("Stack2 is empty");
        }
        return arr[top2];
    }
    
    public boolean isEmpty1() {
        return top1 < 0;
    }
    
    public boolean isEmpty2() {
        return top2 >= capacity;
    }
    
    public boolean isFull() {
        return top1 + 1 == top2;
    }
    
    public static void main(String[] args) {
        TwoStacks ts = new TwoStacks(10);
        
        // Push elements to both stacks
        ts.push1(1);
        ts.push1(2);
        ts.push1(3);
        ts.push2(10);
        ts.push2(20);
        ts.push2(30);
        
        System.out.println("Pop from Stack1: " + ts.pop1()); // 3
        System.out.println("Pop from Stack2: " + ts.pop2()); // 30
        System.out.println("Peek Stack1: " + ts.peek1());    // 2
        System.out.println("Peek Stack2: " + ts.peek2());    // 20
        
        // Test space efficiency
        ts.push1(4);
        ts.push1(5);
        ts.push1(6);
        ts.push1(7);
        
        System.out.println("Full: " + ts.isFull());
    }
}

Complexity Analysis

  • Time Complexity: O(1) for all operations
  • Space Complexity: O(n) with optimal space utilization - no wastage

Key Takeaways

  1. Two-ends approach is optimal as it allows both stacks to use entire array space
  2. Overflow occurs only when total elements in both stacks equal array size
  3. Stack1 grows from left (0 to right), Stack2 grows from right (n-1 to left)
  4. The condition top1 + 1 == top2 indicates array is full
  5. This technique is useful when you need to minimize memory usage with multiple stacks
  6. Cannot be easily extended to more than 2 stacks (see N stacks in array for that)