Back to blog

Stacks & Queues: LIFO and FIFO Data Structures

algorithmsdata-structuresstacksqueuesinterview-prepdsa
Stacks & Queues: LIFO and FIFO Data Structures

Introduction

Stacks and queues are two of the most fundamental and widely used data structures in computer science. They're everywhere — from your browser's back button to task scheduling systems, from expression evaluation to BFS graph traversal.

What makes them special is their restricted access model: unlike arrays where you can access any element, stacks and queues enforce a specific order of operations. This constraint is exactly what makes them powerful.

What You'll Learn

✅ Understand the LIFO and FIFO principles
✅ Implement stacks and queues using arrays and linked lists
✅ Master the Monotonic Stack pattern for interview problems
✅ Use a Deque for sliding window maximum problems
✅ Implement a queue using two stacks
✅ Solve classic interview problems step-by-step

Prerequisites


1. Stacks — Last In, First Out (LIFO)

A stack is a linear data structure where elements are inserted and removed from the same end — called the top. The last element added is always the first to be removed.

Think of a stack of plates: you add plates to the top, and you remove plates from the top.

Core Stack Operations

OperationDescriptionTime Complexity
push(x)Add element to topO(1)
pop()Remove and return top elementO(1)
peek() / top()View top element without removingO(1)
isEmpty()Check if stack is emptyO(1)
size()Return number of elementsO(1)

1.1 Stack Implementation — Array-Based

The simplest approach: use the end of an array as the top.

// TypeScript - Stack using Array
class Stack<T> {
  private items: T[] = [];
 
  push(item: T): void {
    this.items.push(item);
  }
 
  pop(): T | undefined {
    if (this.isEmpty()) throw new Error("Stack is empty");
    return this.items.pop();
  }
 
  peek(): T {
    if (this.isEmpty()) throw new Error("Stack is empty");
    return this.items[this.items.length - 1];
  }
 
  isEmpty(): boolean {
    return this.items.length === 0;
  }
 
  size(): number {
    return this.items.length;
  }
 
  toString(): string {
    return `[${this.items.join(", ")}] <- top`;
  }
}
 
// Usage
const stack = new Stack<number>();
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack.toString()); // [10, 20, 30] <- top
console.log(stack.peek());     // 30
console.log(stack.pop());      // 30
console.log(stack.size());     // 2
# Python - Stack using List
class Stack:
    def __init__(self):
        self.items = []
 
    def push(self, item):
        self.items.append(item)
 
    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self.items.pop()
 
    def peek(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self.items[-1]
 
    def is_empty(self):
        return len(self.items) == 0
 
    def size(self):
        return len(self.items)
 
    def __repr__(self):
        return f"{self.items} <- top"
 
# Usage
stack = Stack()
stack.push(10)
stack.push(20)
stack.push(30)
print(stack)         # [10, 20, 30] <- top
print(stack.peek())  # 30
print(stack.pop())   # 30
print(stack.size())  # 2

1.2 Stack Implementation — Linked List-Based

Using a linked list where head is always the top:

// TypeScript - Stack using Linked List
class ListNode<T> {
  val: T;
  next: ListNode<T> | null;
  constructor(val: T) {
    this.val = val;
    this.next = null;
  }
}
 
class LinkedStack<T> {
  private head: ListNode<T> | null = null;
  private _size = 0;
 
  push(item: T): void {
    const node = new ListNode(item);
    node.next = this.head;
    this.head = node;
    this._size++;
  }
 
  pop(): T {
    if (!this.head) throw new Error("Stack is empty");
    const val = this.head.val;
    this.head = this.head.next;
    this._size--;
    return val;
  }
 
  peek(): T {
    if (!this.head) throw new Error("Stack is empty");
    return this.head.val;
  }
 
  isEmpty(): boolean {
    return this.head === null;
  }
 
  size(): number {
    return this._size;
  }
}

Array vs Linked List for Stack:

AspectArray-BasedLinked List-Based
MemoryContiguous block, may resizeDynamic, node overhead
push/popO(1) amortizedO(1) always
Cache performanceBetter (locality)Worse (scattered)
Practical choice✅ Usually preferredFor fixed-size or memory control

2. Queues — First In, First Out (FIFO)

A queue is a linear data structure where elements are inserted at the rear (back) and removed from the front. The first element added is always the first to be removed.

Think of a checkout line at a store: the first person in line is the first to be served.

Core Queue Operations

OperationDescriptionTime Complexity
enqueue(x)Add element to rearO(1)
dequeue()Remove and return front elementO(1)
front() / peek()View front elementO(1)
isEmpty()Check if queue is emptyO(1)
size()Return number of elementsO(1)

2.1 Queue Implementation — Array-Based (Naïve)

The simplest approach uses an array but has a problem: shift() is O(n).

// TypeScript - Naive Queue (O(n) dequeue — avoid in interviews)
class NaiveQueue<T> {
  private items: T[] = [];
 
  enqueue(item: T): void {
    this.items.push(item); // O(1) amortized
  }
 
  dequeue(): T | undefined {
    return this.items.shift(); // ⚠️ O(n) — shifts all elements
  }
}

2.2 Queue Implementation — Linked List-Based (Correct)

Using a linked list with both head (front) and tail (rear) pointers gives O(1) for all operations:

// TypeScript - Queue using Linked List (O(1) for all ops)
class QueueNode<T> {
  val: T;
  next: QueueNode<T> | null = null;
  constructor(val: T) { this.val = val; }
}
 
class Queue<T> {
  private head: QueueNode<T> | null = null; // front
  private tail: QueueNode<T> | null = null; // rear
  private _size = 0;
 
  enqueue(item: T): void {
    const node = new QueueNode(item);
    if (!this.tail) {
      this.head = this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
    this._size++;
  }
 
  dequeue(): T {
    if (!this.head) throw new Error("Queue is empty");
    const val = this.head.val;
    this.head = this.head.next;
    if (!this.head) this.tail = null; // queue became empty
    this._size--;
    return val;
  }
 
  front(): T {
    if (!this.head) throw new Error("Queue is empty");
    return this.head.val;
  }
 
  isEmpty(): boolean {
    return this.head === null;
  }
 
  size(): number {
    return this._size;
  }
}
 
// Usage
const q = new Queue<number>();
q.enqueue(10);
q.enqueue(20);
q.enqueue(30);
console.log(q.front());    // 10
console.log(q.dequeue());  // 10
console.log(q.front());    // 20
console.log(q.size());     // 2
# Python - Queue using collections.deque (O(1) for all ops)
from collections import deque
 
class Queue:
    def __init__(self):
        self.items = deque()
 
    def enqueue(self, item):
        self.items.append(item)       # Add to right (rear)
 
    def dequeue(self):
        if self.is_empty():
            raise IndexError("Queue is empty")
        return self.items.popleft()   # Remove from left (front) — O(1)
 
    def front(self):
        if self.is_empty():
            raise IndexError("Queue is empty")
        return self.items[0]
 
    def is_empty(self):
        return len(self.items) == 0
 
    def size(self):
        return len(self.items)
 
# Usage
q = Queue()
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)
print(q.front())    # 10
print(q.dequeue())  # 10
print(q.front())    # 20

Python tip: Always use collections.deque for queues. Python lists have O(n) pop(0) — never use a plain list as a queue.

2.3 Circular Queue

A circular queue reuses empty slots from dequeued elements, making it memory-efficient with fixed capacity:

// TypeScript - Circular Queue (ring buffer)
class CircularQueue<T> {
  private items: (T | null)[];
  private head = 0;
  private tail = 0;
  private _size = 0;
  private capacity: number;
 
  constructor(capacity: number) {
    this.capacity = capacity;
    this.items = new Array(capacity).fill(null);
  }
 
  enqueue(item: T): boolean {
    if (this._size === this.capacity) return false; // full
    this.items[this.tail] = item;
    this.tail = (this.tail + 1) % this.capacity;   // wrap around
    this._size++;
    return true;
  }
 
  dequeue(): T | null {
    if (this._size === 0) return null; // empty
    const val = this.items[this.head] as T;
    this.items[this.head] = null;
    this.head = (this.head + 1) % this.capacity;   // wrap around
    this._size--;
    return val;
  }
 
  isFull(): boolean {
    return this._size === this.capacity;
  }
 
  isEmpty(): boolean {
    return this._size === 0;
  }
}

3. Deque — Double-Ended Queue

A deque (pronounced "deck") allows insertion and deletion at both ends in O(1). It generalizes both stacks and queues.

Deque Implementation

// TypeScript - Deque using Doubly Linked List
class DequeNode<T> {
  val: T;
  prev: DequeNode<T> | null = null;
  next: DequeNode<T> | null = null;
  constructor(val: T) { this.val = val; }
}
 
class Deque<T> {
  private head: DequeNode<T> | null = null;
  private tail: DequeNode<T> | null = null;
  private _size = 0;
 
  addFront(item: T): void {
    const node = new DequeNode(item);
    if (!this.head) {
      this.head = this.tail = node;
    } else {
      node.next = this.head;
      this.head.prev = node;
      this.head = node;
    }
    this._size++;
  }
 
  addRear(item: T): void {
    const node = new DequeNode(item);
    if (!this.tail) {
      this.head = this.tail = node;
    } else {
      node.prev = this.tail;
      this.tail.next = node;
      this.tail = node;
    }
    this._size++;
  }
 
  removeFront(): T {
    if (!this.head) throw new Error("Deque is empty");
    const val = this.head.val;
    this.head = this.head.next;
    if (this.head) this.head.prev = null;
    else this.tail = null;
    this._size--;
    return val;
  }
 
  removeRear(): T {
    if (!this.tail) throw new Error("Deque is empty");
    const val = this.tail.val;
    this.tail = this.tail.prev;
    if (this.tail) this.tail.next = null;
    else this.head = null;
    this._size--;
    return val;
  }
 
  peekFront(): T {
    if (!this.head) throw new Error("Deque is empty");
    return this.head.val;
  }
 
  peekRear(): T {
    if (!this.tail) throw new Error("Deque is empty");
    return this.tail.val;
  }
 
  isEmpty(): boolean { return this._size === 0; }
  size(): number { return this._size; }
}
# Python - Deque using collections.deque
from collections import deque
 
dq = deque()
dq.appendleft(10)  # addFront
dq.append(20)      # addRear
dq.appendleft(5)   # addFront
print(list(dq))    # [5, 10, 20]
dq.popleft()       # removeFront -> 5
dq.pop()           # removeRear  -> 20
print(list(dq))    # [10]

4. Real-World Applications

Understanding when to use stacks vs queues is as important as knowing how to implement them.

Stack Applications

Queue Applications


5. Classic Interview Problems

Problem 1: Valid Parentheses (Stack)

LeetCode #20 — Given a string containing (, ), {, }, [, ], determine if it's valid.

A string is valid if:

  • Every open bracket has a matching closing bracket
  • Brackets close in the correct order

Key insight: Use a stack. Push opening brackets. When you see a closing bracket, check if it matches the top of the stack.

// TypeScript
function isValid(s: string): boolean {
  const stack: string[] = [];
  const matching: Record<string, string> = {
    ')': '(',
    '}': '{',
    ']': '[',
  };
 
  for (const char of s) {
    if ('({['.includes(char)) {
      stack.push(char);           // Push opening bracket
    } else {
      // Closing bracket: must match top of stack
      if (stack.length === 0 || stack[stack.length - 1] !== matching[char]) {
        return false;
      }
      stack.pop();
    }
  }
 
  return stack.length === 0;      // Stack must be empty at end
}
 
// Examples
console.log(isValid("()"));       // true
console.log(isValid("()[]{}"));   // true
console.log(isValid("(]"));       // false
console.log(isValid("([)]"));     // false
console.log(isValid("{[]}"));     // true
# Python
def is_valid(s: str) -> bool:
    stack = []
    matching = {')': '(', '}': '{', ']': '['}
 
    for char in s:
        if char in '({[':
            stack.append(char)
        else:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
 
    return len(stack) == 0
 
# Examples
print(is_valid("()"))       # True
print(is_valid("()[]{}"))   # True
print(is_valid("(]"))       # False

Trace through "([)]":

char='(' → push → stack=['(']
char='[' → push → stack=['(', '[']
char=')' → matching[')']='(' but top='[' → MISMATCH → return false

Complexity: Time O(n), Space O(n)


Problem 2: Evaluate Reverse Polish Notation (Stack)

LeetCode #150 — Evaluate an expression in Reverse Polish Notation (postfix). Operators: +, -, *, /.

Examples:

  • ["2","1","+","3","*"]((2 + 1) * 3) = 9
  • ["4","13","5","/","+"](4 + (13 / 5)) = 6
// TypeScript
function evalRPN(tokens: string[]): number {
  const stack: number[] = [];
  const ops = new Set(['+', '-', '*', '/']);
 
  for (const token of tokens) {
    if (ops.has(token)) {
      const b = stack.pop()!;  // Second operand
      const a = stack.pop()!;  // First operand
      let result: number;
      if (token === '+') result = a + b;
      else if (token === '-') result = a - b;
      else if (token === '*') result = a * b;
      else result = Math.trunc(a / b); // Truncate toward zero
      stack.push(result);
    } else {
      stack.push(parseInt(token));
    }
  }
 
  return stack[0];
}
 
console.log(evalRPN(["2","1","+","3","*"]));     // 9
console.log(evalRPN(["4","13","5","/","+"]));    // 6
console.log(evalRPN(["10","6","9","3","+","-1","*","+"])); // 12
# Python
def eval_rpn(tokens: list[str]) -> int:
    stack = []
    ops = {'+', '-', '*', '/'}
 
    for token in tokens:
        if token in ops:
            b = stack.pop()
            a = stack.pop()
            if token == '+': stack.append(a + b)
            elif token == '-': stack.append(a - b)
            elif token == '*': stack.append(a * b)
            else: stack.append(int(a / b))  # Truncate toward zero
        else:
            stack.append(int(token))
 
    return stack[0]

Complexity: Time O(n), Space O(n)


Problem 3: Daily Temperatures — Monotonic Stack (Stack)

LeetCode #739 — Given daily temperatures, find how many days until a warmer temperature. Return 0 if no warmer day exists.

Example: [73,74,75,71,69,72,76,73][1,1,4,2,1,1,0,0]

Key insight: Use a monotonic decreasing stack that stores indices. When the current temperature is warmer than the stack's top, we've found the answer for that index.

// TypeScript - Monotonic Stack
function dailyTemperatures(temperatures: number[]): number[] {
  const n = temperatures.length;
  const result = new Array(n).fill(0);
  const stack: number[] = []; // stores indices
 
  for (let i = 0; i < n; i++) {
    // While current temp is warmer than the temp at stack top
    while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
      const idx = stack.pop()!;
      result[idx] = i - idx; // days until warmer
    }
    stack.push(i);
  }
 
  return result;
}
 
console.log(dailyTemperatures([73,74,75,71,69,72,76,73]));
// [1, 1, 4, 2, 1, 1, 0, 0]
# Python
def daily_temperatures(temperatures: list[int]) -> list[int]:
    n = len(temperatures)
    result = [0] * n
    stack = []  # stores indices
 
    for i, temp in enumerate(temperatures):
        while stack and temp > temperatures[stack[-1]]:
            idx = stack.pop()
            result[idx] = i - idx
        stack.append(i)
 
    return result

Trace through [73, 74, 75, 71, 69, 72, 76, 73]:

i=0, temp=73 → stack=[] → push 0 → stack=[0]
i=1, temp=74 → 74>73(idx=0) → result[0]=1-0=1, stack=[] → push 1 → stack=[1]
i=2, temp=75 → 75>74(idx=1) → result[1]=2-1=1, stack=[] → push 2 → stack=[2]
i=3, temp=71 → 71<75 → push 3 → stack=[2,3]
i=4, temp=69 → 69<71 → push 4 → stack=[2,3,4]
i=5, temp=72 → 72>69(idx=4)→result[4]=5-4=1, 72>71(idx=3)→result[3]=5-3=2, 72<75 → push 5 → stack=[2,5]
i=6, temp=76 → 76>72(idx=5)→result[5]=6-5=1, 76>75(idx=2)→result[2]=6-2=4, stack=[] → push 6 → stack=[6]
i=7, temp=73 → 73<76 → push 7 → stack=[6,7]
Final: result = [1,1,4,2,1,1,0,0]

Complexity: Time O(n), Space O(n)


Problem 4: Implement Queue Using Two Stacks (Stack + Queue)

LeetCode #232 — Implement a FIFO queue using only two stacks.

Key insight: Two reversals equal the original order. Stack s1 receives pushes. Stack s2 serves pops — refill from s1 only when s2 is empty (lazy transfer).

// TypeScript
class MyQueue {
  private inbox: number[] = [];   // for push
  private outbox: number[] = [];  // for pop/peek
 
  push(x: number): void {
    this.inbox.push(x);
  }
 
  pop(): number {
    this.transfer();
    return this.outbox.pop()!;
  }
 
  peek(): number {
    this.transfer();
    return this.outbox[this.outbox.length - 1];
  }
 
  empty(): boolean {
    return this.inbox.length === 0 && this.outbox.length === 0;
  }
 
  private transfer(): void {
    // Only transfer when outbox is empty (lazy)
    if (this.outbox.length === 0) {
      while (this.inbox.length > 0) {
        this.outbox.push(this.inbox.pop()!);
      }
    }
  }
}
 
// Usage
const queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.push(3);
console.log(queue.peek()); // 1 (FIFO: first pushed)
console.log(queue.pop());  // 1
console.log(queue.pop());  // 2
# Python
class MyQueue:
    def __init__(self):
        self.inbox = []   # for push
        self.outbox = []  # for pop/peek
 
    def push(self, x: int) -> None:
        self.inbox.append(x)
 
    def pop(self) -> int:
        self._transfer()
        return self.outbox.pop()
 
    def peek(self) -> int:
        self._transfer()
        return self.outbox[-1]
 
    def empty(self) -> bool:
        return not self.inbox and not self.outbox
 
    def _transfer(self) -> None:
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())

Amortized complexity: Each element is pushed and popped from each stack at most once → O(1) amortized per operation.


Problem 5: Sliding Window Maximum — Monotonic Deque (Deque)

LeetCode #239 — Given array nums and window size k, find the maximum in each sliding window.

Brute force: O(n·k) — check every window. Can we do O(n)?

Key insight: Use a monotonic decreasing deque that stores indices. The front always holds the index of the current window maximum. Remove from rear elements smaller than the current (they can never be the maximum). Remove from front indices outside the window.

// TypeScript - Monotonic Deque
function maxSlidingWindow(nums: number[], k: number): number[] {
  const result: number[] = [];
  const deque: number[] = []; // stores indices, front = max
 
  for (let i = 0; i < nums.length; i++) {
    // Remove indices outside current window
    while (deque.length > 0 && deque[0] < i - k + 1) {
      deque.shift();
    }
 
    // Remove smaller elements from rear (they can't be max)
    while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
 
    deque.push(i);
 
    // Start recording results once first window is complete
    if (i >= k - 1) {
      result.push(nums[deque[0]]); // front is always the max
    }
  }
 
  return result;
}
 
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
// [3, 3, 5, 5, 6, 7]
# Python
from collections import deque
 
def max_sliding_window(nums: list[int], k: int) -> list[int]:
    result = []
    dq = deque()  # stores indices
 
    for i, num in enumerate(nums):
        # Remove out-of-window indices from front
        while dq and dq[0] < i - k + 1:
            dq.popleft()
 
        # Remove smaller elements from rear
        while dq and nums[dq[-1]] < num:
            dq.pop()
 
        dq.append(i)
 
        if i >= k - 1:
            result.append(nums[dq[0]])
 
    return result

Trace through [1,3,-1,-3,5,3,6,7], k=3:

i=0, num=1  → deque=[0]
i=1, num=3  → 3>1 remove 0, deque=[1]
i=2, num=-1 → deque=[1,2], window=[1,3,-1] → result=[nums[1]]=3
i=3, num=-3 → deque=[1,2,3], window=[3,-1,-3] → result=[3,nums[1]]=3,3
i=4, num=5  → 5>-3 remove 3, 5>-1 remove 2, 5>3 remove 1, deque=[4], window→max=5
i=5, num=3  → deque=[4,5], window→max=nums[4]=5
i=6, num=6  → 6>3 remove 5, 6>5 remove 4, deque=[6], window→max=6
i=7, num=7  → 7>6 remove 6, deque=[7], window→max=7
result = [3, 3, 5, 5, 6, 7]

Complexity: Time O(n), Space O(k)


Problem 6: Min Stack — Stack with O(1) getMin (Stack)

LeetCode #155 — Design a stack that supports push, pop, top, and getMin in O(1).

Key insight: Maintain a second "min stack" that tracks the minimum at each level.

// TypeScript
class MinStack {
  private stack: number[] = [];
  private minStack: number[] = [];
 
  push(val: number): void {
    this.stack.push(val);
    const currentMin = this.minStack.length === 0
      ? val
      : Math.min(val, this.minStack[this.minStack.length - 1]);
    this.minStack.push(currentMin);
  }
 
  pop(): void {
    this.stack.pop();
    this.minStack.pop();
  }
 
  top(): number {
    return this.stack[this.stack.length - 1];
  }
 
  getMin(): number {
    return this.minStack[this.minStack.length - 1];
  }
}
 
// Usage
const minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
console.log(minStack.getMin()); // -3
minStack.pop();
console.log(minStack.top());    // 0
console.log(minStack.getMin()); // -2
# Python
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []
 
    def push(self, val: int) -> None:
        self.stack.append(val)
        current_min = val if not self.min_stack else min(val, self.min_stack[-1])
        self.min_stack.append(current_min)
 
    def pop(self) -> None:
        self.stack.pop()
        self.min_stack.pop()
 
    def top(self) -> int:
        return self.stack[-1]
 
    def get_min(self) -> int:
        return self.min_stack[-1]

Complexity: Time O(1) for all operations, Space O(n)


Problem 7: Next Greater Element (Monotonic Stack)

LeetCode #496 — For each element in nums1, find the next greater element in nums2. If none exists, return -1.

// TypeScript - Monotonic Stack
function nextGreaterElement(nums1: number[], nums2: number[]): number[] {
  // Build next-greater map for nums2
  const nextGreater = new Map<number, number>();
  const stack: number[] = [];
 
  for (const num of nums2) {
    while (stack.length > 0 && stack[stack.length - 1] < num) {
      nextGreater.set(stack.pop()!, num);
    }
    stack.push(num);
  }
  // Remaining elements in stack have no next greater → -1 (Map default)
 
  return nums1.map(n => nextGreater.get(n) ?? -1);
}
 
console.log(nextGreaterElement([4,1,2], [1,3,4,2])); // [-1,3,-1]
console.log(nextGreaterElement([2,4], [1,2,3,4]));   // [3,-1]
# Python
def next_greater_element(nums1: list[int], nums2: list[int]) -> list[int]:
    next_greater = {}
    stack = []
 
    for num in nums2:
        while stack and stack[-1] < num:
            next_greater[stack.pop()] = num
        stack.append(num)
 
    return [next_greater.get(n, -1) for n in nums1]

Complexity: Time O(n + m), Space O(n) where n = len(nums2), m = len(nums1)


6. Pattern Recognition Guide

Knowing which pattern to apply is half the battle in interviews.

Problem PatternUseSignal Words / Clues
Parenthesis matchingStack"valid", "balanced", brackets
Undo/Redo historyStack"undo", "back", "history"
Expression evaluationStackpostfix, operators, precedence
Next greater/smaller elementMonotonic Stack"next", "previous", "warmer", "larger"
Process in order receivedQueue"order", "schedule", "serve"
BFS traversalQueuelevel-by-level, shortest path
Sliding window max/minMonotonic Deque"window maximum", "window minimum"
O(1) getMin/getMaxStack + auxiliary"constant time", "minimum", "maximum"
FIFO with stack ops onlyTwo Stacks"queue using stacks"

7. Complexity Summary

Stack

OperationArray-BasedLinked List-Based
pushO(1) amortizedO(1)
popO(1) amortizedO(1)
peekO(1)O(1)
SpaceO(n)O(n)

Queue

OperationNaive ArrayLinked List / deque
enqueueO(1) amortizedO(1)
dequeue❌ O(n)O(1)
frontO(1)O(1)
SpaceO(n)O(n)

Deque (Double-ended Queue)

OperationLinked List / deque
addFrontO(1)
addRearO(1)
removeFrontO(1)
removeRearO(1)
SpaceO(n)

8. Practice Problems

Work through these problems in order. Each one reinforces the patterns from this post.

Easy

ProblemKey ConceptLeetCode
Valid ParenthesesStack#20
Implement Stack using QueuesTwo Queues#225
Implement Queue using StacksTwo Stacks#232
Min StackAuxiliary Stack#155
Next Greater Element IMonotonic Stack#496

Medium

ProblemKey ConceptLeetCode
Daily TemperaturesMonotonic Stack#739
Evaluate Reverse Polish NotationStack#150
Decode StringStack#394
Design Circular QueueCircular Queue#622
Next Greater Element IIMonotonic Stack + Circular#503
Number of Visible People in a QueueMonotonic Stack#1944

Hard

ProblemKey ConceptLeetCode
Sliding Window MaximumMonotonic Deque#239
Largest Rectangle in HistogramMonotonic Stack#84
Trapping Rain WaterMonotonic Stack / Two Pointers#42

Summary and Key Takeaways

Stack (LIFO):
✅ Use when you need to process elements in reverse order of arrival
✅ Perfect for undo/redo, parsing, expression evaluation, DFS
✅ Monotonic stack solves "next greater/smaller" patterns in O(n)

Queue (FIFO):
✅ Use when you need to process elements in arrival order
✅ Essential for BFS, scheduling, buffering
✅ Always use deque in Python — never list.pop(0)

Deque:
✅ Generalizes both stack and queue — add/remove from both ends
✅ Monotonic deque solves sliding window max/min in O(n)

Key Interview Patterns:
✅ See brackets → think Stack
✅ See "next greater element" → think Monotonic Stack
✅ See BFS / level-order → think Queue
✅ See sliding window max/min → think Monotonic Deque


What's Next?

You've now completed Phase 2 of the DSA series — Linear Data Structures. Next up is Phase 3: Non-Linear Data Structures, starting with the most important: Trees.

Continue the DSA series:

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.