Skip to content
Commitwise
Menu
Coding Interview

Stacks and queues

Use last-in-first-out stacks and first-in-first-out queues for bracket matching, undo, BFS and monotonic-stack problems, with the right types in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Your code editor knows that { [ ( ) ] } is fine and { [ ( ] ) } isn’t. The back button knows where to go back to. A print queue prints documents in the order they were sent. Under all three is one of the two simplest data structures there are: a stackStackA collection where the last element added is the first one removed (last in, first out).Open in glossary → or a queueQueueA collection where the first element added is the first one removed (first in, first out).Open in glossary →.

They’re simple enough to learn in five minutes, and they’re behind a surprising number of interview problems, from Valid Parentheses to “Daily Temperatures”.

New to some of these? Open what you need, or skip ahead.

Basics:What are lists, sets and maps?

Collections hold many values:

  • A list (Swift Array, Kotlin List, Java List, a TypeScript array) keeps items in order, and allows duplicates: ["Ana", "Ben", "Ana"].
  • A set holds each value at most once and answers “is this in it?” very quickly.
  • A map (Swift Dictionary, Kotlin, Java and TypeScript Map) stores values under keys: "Ana" → 30, looked up by the key.

Lists use indexes starting at 0: in ["a", "b", "c"], "a" is at index 0 and "c" at index 2.

Basics:How do strings and characters work?

A string is text: a sequence of characters. "racecar" has 7 characters, and you can compare them, change their case (lowercased(), lowercase(), toLowerCase()) and check whether one is a letter or a digit.

One difference to know: Kotlin, Java and TypeScript let you read a character by position, s[3] or s.charAt(3) (in TypeScript, positions count UTF-16 units, so an emoji takes two). Swift doesn’t allow integer positions on String (because some characters, like emoji, take more space than others), so code often converts the string to an array first: Array(s).

Basics:What does O(n) mean?

Big-O describes how the amount of work grows with the size of the input n:

  • O(1): the same work whatever the size (reading array[5]).
  • O(log n): grows very slowly (about 20 steps for a million items).
  • O(n): grows in step with the input (one pass over the array).
  • O(n²): comparing every item with every other item, which gets slow fast.

You’ll see it next to every solution, for time and for extra space (memory). The Big-O tutorial explains it properly.

Basics:What is recursion?

A recursive function calls itself on a smaller version of the problem, until it reaches a case small enough to answer directly (the base case):

countdown(3) → prints 3, calls countdown(2) → prints 2, calls countdown(1) → prints 1, calls countdown(0) → stops

Two rules keep it correct: always have a base case, and make sure every call gets closer to it. Each call waits on the call stack until the smaller call finishes, so very deep recursion (hundreds of thousands of levels) can run out of stack space.

Basics:What is boxing (int vs Integer)?

Java has fast primitive types like int, double and boolean, which aren’t objects. Collections and generics can only hold objects, so Java wraps primitives in objects: int → Integer, double → Double. That wrapping is called boxing; reading the number back out is unboxing.

Two things to know: an Integer can be null (and unboxing null crashes), and comparing two Integers with == compares the objects, not the numbers. Kotlin hides most of this (its Int becomes int or Integer as needed), and Swift has no boxing at all for its number types. TypeScript has a single number type, so there’s nothing to box.

Part Not started

Last in, first out vs first in, first out

  • A stack is a pile of plates. You put a plate on top (push), and you take the plate from the top (pop). The last plate you put down is the first one you take: LIFO, last in, first out. Undo, the back button and your program’s function calls all work like this.
  • A queue is the line at a bakery. People join at the back (enqueue) and are served from the front (dequeue). The first to arrive is the first served: FIFO, first in, first out. Print jobs, messages and “visit these places in order” work like this.

Both only allow access at the ends, and that’s the point: pushing, popping, enqueuing and dequeuing are all O(1).

Stacks and queues are restricted sequences with O(1) operations at the ends: a stack exposes one end (LIFO), a queue adds at one end and removes at the other (FIFO), and a deque allows both ends. They’re the natural structures for depth-first (stack, including the call stack) and breadth-first (queue) traversal, for matching nested structure, and, in their monotonic variants, for “next greater/smaller element” problems in O(n).

In practice the interesting parts are choosing the right type per language (Java’s Stack and LinkedList are legacy choices; Swift has no standard deque), recursion depth as a hidden stack that can overflow, and the amortized analysis of a queue built from two stacks. All in the Detailed version.

import DequeModule                   // swift-collections, for the queue below

// Stack: an Array, using the end as the top
var stack: [String] = []
stack.append("home")                 // push
stack.append("product")
let current = stack.popLast()        // pop → "product" (nil if empty)
let top = stack.last                 // peek → "home"

// Queue: Deque from swift-collections (Swift has no queue in the standard library)
var queue: Deque<String> = []
queue.append("job 1")                // enqueue at the back
queue.append("job 2")
let next = queue.popFirst()          // dequeue from the front → "job 1"
// ArrayDeque works as both a stack and a queue
val stack = ArrayDeque<String>()
stack.addLast("home")                // push
stack.addLast("product")
val current = stack.removeLast()     // pop → "product"
val top = stack.lastOrNull()         // peek → "home"

val queue = ArrayDeque<String>()
queue.addLast("job 1")               // enqueue at the back
queue.addLast("job 2")
val next = queue.removeFirst()       // dequeue from the front → "job 1"
// ArrayDeque works as both a stack and a queue (prefer it to Stack and LinkedList)
Deque<String> stack = new ArrayDeque<>();
stack.push("home");                  // push adds to the FRONT
stack.push("product");
String current = stack.pop();        // pop → "product"
String top = stack.peek();           // peek → "home"

Queue<String> queue = new ArrayDeque<>();
queue.offer("job 1");                // enqueue at the back
queue.offer("job 2");
String next = queue.poll();          // dequeue from the front → "job 1" (null if empty)
// Stack: an array, using the end as the top
const stack: string[] = [];
stack.push("home");                  // push
stack.push("product");
const current = stack.pop();         // pop → "product" (undefined if empty)
const top = stack.at(-1);            // peek → "home"

// Queue: no built-in queue type; an array plus a front index
const queue: string[] = [];
let head = 0;
queue.push("job 1");                 // enqueue at the back
queue.push("job 2");
const next = queue[head++];          // dequeue from the front → "job 1"

Each language has a quirk:

  • Swift has no queue type in the standard library. Use Deque from Apple’s swift-collections package, or keep an index to the front of an array. Don’t use removeFirst() on an Array as a queue: it shifts every element, O(n).
  • Kotlin’s ArrayDeque does both jobs. Pick one end for a stack (usually the last) and use both ends for a queue.
  • Java’s ArrayDeque is the recommended type for both. Its stack methods push/pop/peek work on the front, which matters when you print it (see the senior puzzle). The old Stack class is synchronized and slower.
  • TypeScript arrays are stacks out of the box (push, pop). There’s no queue type: shift() re-indexes every element (O(n)), so for long queues keep a head index, as above, or a small ring buffer.

Same pushes, opposite order

Predict the output

1, 2 and 3 are added to a stack and to a queue, then taken out until both are empty. What gets printed?

var stack: [Int] = []
var queue: [Int] = []
for n in [1, 2, 3] {
    stack.append(n)
    queue.append(n)
}

var fromStack: [Int] = []
while let n = stack.popLast() { fromStack.append(n) }

var fromQueue: [Int] = []
while !queue.isEmpty { fromQueue.append(queue.removeFirst()) }   // fine for 3 items

print(fromStack.map(String.init).joined(separator: " "))
print(fromQueue.map(String.init).joined(separator: " "))
val stack = ArrayDeque<Int>()
val queue = ArrayDeque<Int>()
for (n in listOf(1, 2, 3)) {
    stack.addLast(n)
    queue.addLast(n)
}

val fromStack = buildList { while (stack.isNotEmpty()) add(stack.removeLast()) }
val fromQueue = buildList { while (queue.isNotEmpty()) add(queue.removeFirst()) }

println(fromStack.joinToString(" "))
println(fromQueue.joinToString(" "))
Deque<Integer> stack = new ArrayDeque<>();
Queue<Integer> queue = new ArrayDeque<>();
for (int n : List.of(1, 2, 3)) {
    stack.push(n);
    queue.offer(n);
}

StringJoiner fromStack = new StringJoiner(" ");
while (!stack.isEmpty()) fromStack.add(String.valueOf(stack.pop()));

StringJoiner fromQueue = new StringJoiner(" ");
while (!queue.isEmpty()) fromQueue.add(String.valueOf(queue.poll()));

System.out.println(fromStack);
System.out.println(fromQueue);
const stack: number[] = [];
const queue: number[] = [];
for (const n of [1, 2, 3]) {
  stack.push(n);
  queue.push(n);
}

const fromStack: number[] = [];
while (stack.length > 0) fromStack.push(stack.pop()!);

const fromQueue: number[] = [];
while (queue.length > 0) fromQueue.push(queue.shift()!);   // fine for 3 items

console.log(fromStack.join(" "));
console.log(fromQueue.join(" "));

Queues from stacks, and why amortized matters

A queue can be built from two stacks: push onto inbox; to dequeue, pop from outbox, refilling it from inbox (which reverses the order) only when it’s empty. Each element moves at most twice, so dequeue is O(1) amortized, though a single dequeue can be O(n). That’s fine for throughput and wrong for a latency-sensitive path, the same trade-off as dynamic-array growth.

Stacks you don’t see

  • The call stack. Recursion is a stack the runtime manages for you, with a hard size limit: roughly 512 KB to 8 MB depending on platform and thread (secondary threads get less); a JavaScript engine typically allows around ten thousand frames. Recursion depth proportional to input size (a linked list, a degenerate tree, user-controlled nesting) can crash with a stack overflow; converting to an explicit stack moves the data to the heap.
  • BFS queues hold a whole frontier: for a wide graph that’s O(V) memory, which matters on mobile.
  • Bounded queues (ArrayBlockingQueue, Channel(capacity), AsyncStream buffering policies, a Node stream’s highWaterMark) are how producer/consumer systems apply backpressure instead of growing until they run out of memory.

Printing a Java stack

Predict the output

1, 2, 3 are pushed onto a stack. What does printing it show?

var stack: [Int] = []
stack.append(1)
stack.append(2)
stack.append(3)
print(stack)
val stack = ArrayDeque<Int>()
stack.addLast(1)
stack.addLast(2)
stack.addLast(3)
println(stack)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack);
const stack: number[] = [];
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack);
SwiftKotlinJavaTypeScript
StackArray: append, popLast, lastArrayDeque: addLast, removeLast, last()ArrayDeque: push, pop, peek (front!)array: push, pop, at(-1)
QueueDeque (swift-collections) or array + head indexArrayDeque: addLast, removeFirstArrayDeque/Queue: offer, poll, peekarray + head index, or a ring buffer
AvoidArray.removeFirst() as a queue (O(n))ArrayList.removeAt(0) (O(n))Stack (synchronized), LinkedList (cache-unfriendly)shift() on long queues (O(n))
Blocking/boundedAsyncStream buffering policyChannel(capacity)ArrayBlockingQueue, LinkedBlockingQueuestream highWaterMark, async iterators

Gotchas: check leftovers after matching, Java push iterates top-first, recursion depth from user input, Character == in Java.

Check this part

  1. Which order does a stack return elements in?

  2. What is wrong with using removeFirst() on a Swift Array as a queue?What is wrong with using removeAt(0) on an ArrayList as a queue?What is wrong with using remove(0) on an ArrayList as a queue?What is wrong with using shift() on a long array as a queue?

  3. You append 1, 2, 3 to an Array used as a stack and print it. What shows?You addLast 1, 2, 3 to an ArrayDeque used as a stack and print it. What shows?You push 1, 2, 3 onto an ArrayDeque and print it. What shows?You push 1, 2, 3 onto an array used as a stack and log it. What shows?

  4. A queue made of two stacks: what is the cost of dequeue?

A stack is last in, first out (push and pop at the top); a queue is first in, first out (add at the back, take from the front). Both are O(1), as long as you pick a type that doesn't shift elements.

Part Not started

Matching brackets

In { [ ( ) ] }, each closing bracket must match the most recently opened bracket that’s still open. That’s exactly what sits on top of a stack: push every opening bracket, and for each closing one, pop and check that it’s the partner.

The check everyone forgets

Here’s a simpler version that only handles round brackets. It passes most tests. Which input breaks it?

Spot the bugBalanced, apparently

Tap the line with the bug.

func isBalanced(_ s: String) -> Bool {
    var stack: [Character] = []
    for ch in s {
        if ch == "(" {
            stack.append(ch)
        } else if ch == ")" {
            if stack.isEmpty { return false }
            stack.removeLast()
        }
    }
    return true
}
fun isBalanced(s: String): Boolean {
    val stack = ArrayDeque<Char>()
    for (ch in s) {
        if (ch == '(') {
            stack.addLast(ch)
        } else if (ch == ')') {
            if (stack.isEmpty()) return false
            stack.removeLast()
        }
    }
    return true
}
boolean isBalanced(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char ch : s.toCharArray()) {
        if (ch == '(') {
            stack.push(ch);
        } else if (ch == ')') {
            if (stack.isEmpty()) return false;
            stack.pop();
        }
    }
    return true;
}
function isBalanced(s: string): boolean {
  const stack: string[] = [];
  for (const ch of s) {
    if (ch === "(") {
      stack.push(ch);
    } else if (ch === ")") {
      if (stack.length === 0) return false;
      stack.pop();
    }
  }
  return true;
}

Valid Parentheses

Problemeasy

Valid Parentheses

Given a string containing only (, ), [, ], { and }, return true if every bracket is closed by the same type of bracket, in the correct order.

"()[]{}"   → true
"{[()]}"   → true
"([)]"     → false   (the ( is closed by ] )
"(("       → false   (never closed)
"]"        → false   (nothing to close)
›

When you meet a closing bracket, which opening bracket must it match? The most recent one that’s still open.

›

“Most recent first” is a stack. Push opening brackets. For a closing bracket, pop and check that it’s the matching opener. At the end, nothing may be left open.

SolutionTry it yourself firstTime O(n)Space O(n)

Push every opening bracket. For a closing bracket, the stack’s top must be its partner; if the stack is empty or the top is a different type, the string is invalid. After the loop, the stack must be empty, or some bracket was never closed.

func isValid(_ s: String) -> Bool {
    let partner: [Character: Character] = [")": "(", "]": "[", "}": "{"]
    var stack: [Character] = []
    for ch in s {
        if let open = partner[ch] {                 // a closing bracket
            guard stack.popLast() == open else { return false }
        } else {
            stack.append(ch)                        // an opening bracket
        }
    }
    return stack.isEmpty
}
fun isValid(s: String): Boolean {
    val partner = mapOf(')' to '(', ']' to '[', '}' to '{')
    val stack = ArrayDeque<Char>()
    for (ch in s) {
        val open = partner[ch]
        if (open != null) {                         // a closing bracket
            if (stack.removeLastOrNull() != open) return false
        } else {
            stack.addLast(ch)                       // an opening bracket
        }
    }
    return stack.isEmpty()
}
boolean isValid(String s) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : s.toCharArray()) {
        switch (c) {
            case '(', '[', '{' -> stack.push(c);    // an opening bracket
            default -> {                            // a closing bracket
                if (stack.isEmpty()) return false;
                char open = stack.pop();
                if ((c == ')' && open != '(') || (c == ']' && open != '[') || (c == '}' && open != '{')) {
                    return false;
                }
            }
        }
    }
    return stack.isEmpty();
}
function isValid(s: string): boolean {
  const partner: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
  const stack: string[] = [];
  for (const ch of s) {
    const open = partner[ch];
    if (open !== undefined) {                     // a closing bracket
      if (stack.pop() !== open) return false;
    } else {
      stack.push(ch);                             // an opening bracket
    }
  }
  return stack.length === 0;
}

Java note

Comparing two Character objects with == compares references, and only works by accident for small values cached by the JVM. Unbox to char first (as char open = stack.pop() does) or use equals.

Tracing Valid Parentheses

Here’s the solution running on "{[()]}":

CharacterKindStack beforeActionStack after
{open—push{
[open{push{ [
(open{ [push{ [ (
)close{ [ (pop (: matches{ [
]close{ [pop [: matches{
}close{pop {: matchesempty

The stack is empty at the end, so it’s valid. Now try "([)]" in your head: at ), the top is [, which doesn’t match, so it returns false right away.

Recursion as a hidden stack

Spot the bugA comment thread that crashes the app

Tap the line with the bug.

func countComments(_ comment: Comment) -> Int {
    var total = 1
    for reply in comment.replies {
        total += countComments(reply)
    }
    return total
}
fun countComments(comment: Comment): Int {
    var total = 1
    for (reply in comment.replies) {
        total += countComments(reply)
    }
    return total
}
int countComments(Comment comment) {
    int total = 1;
    for (Comment reply : comment.replies()) {
        total += countComments(reply);
    }
    return total;
}
function countComments(comment: Comment): number {
  let total = 1;
  for (const reply of comment.replies) {
    total += countComments(reply);
  }
  return total;
}

  • Matching and nesting are stacks: brackets, tags, undo. Check the leftovers after the loop.
  • Recursion is a size-limited stack: when depth follows user data (comment threads, JSON nesting), use an explicit stack on the heap, and consider capping nesting at the source.

Check this part

  1. Why is a stack the right tool for matching brackets?

  2. Your bracket checker returns true for "(((". What did it forget?

  3. Why can a recursive traversal of a comment thread crash on user-generated data?

  4. With only one bracket type, what can replace the stack?

A closing bracket must match the most recently opened one, which is exactly the top of a stack. Reject a closer with nothing to close, and check that nothing is left open at the end.

Part Not started

When to reach for which

  1. Something must be undone or matched in reverse order (brackets, HTML tags, undo, “go back”): a stack.
  2. Things must be handled in the order they arrived (messages, tasks, levels of a tree one by one): a queue.
  3. You walk a tree or graph: a stack (or recursion) goes deep first; a queue goes wide first, level by level, which also finds shortest paths in unweighted graphs.
  4. You need “the next bigger (or smaller) element” for every element: a stack that keeps elements in sorted order, the monotonic stack in the senior section.
Exercisemedium

Min Stack

Design a stack that supports push(x), pop(), top() and getMin(), all in O(1) time. getMin() returns the smallest element currently in the stack.

push(-2), push(0), push(-3)
getMin() → -3
pop()
top()    → 0
getMin() → -2
›

Keeping one min variable isn’t enough: after you pop the minimum, what was the minimum before it?

›

Store, next to each element, the minimum of the stack at the moment it was pushed. Then the minimum is always at the top, and popping restores the previous minimum automatically.

SolutionTry it yourself firstTime O(1) per operationSpace O(n)
struct MinStack {
    private var items: [(value: Int, min: Int)] = []

    mutating func push(_ value: Int) {
        let currentMin = items.last.map { Swift.min($0.min, value) } ?? value
        items.append((value, currentMin))
    }

    mutating func pop() { items.removeLast() }
    func top() -> Int { items.last!.value }
    func getMin() -> Int { items.last!.min }
}
class MinStack {
    private val values = ArrayDeque<Int>()
    private val mins = ArrayDeque<Int>()             // mins[i] = minimum of values[0..i]

    fun push(value: Int) {
        values.addLast(value)
        mins.addLast(if (mins.isEmpty()) value else minOf(mins.last(), value))
    }

    fun pop() {
        values.removeLast()
        mins.removeLast()
    }

    fun top(): Int = values.last()
    fun getMin(): Int = mins.last()
}
class MinStack {
    private final Deque<Integer> values = new ArrayDeque<>();
    private final Deque<Integer> mins = new ArrayDeque<>();   // top = minimum of everything below

    void push(int value) {
        values.push(value);
        mins.push(mins.isEmpty() ? value : Math.min(mins.peek(), value));
    }

    void pop() {
        values.pop();
        mins.pop();
    }

    int top() { return values.peek(); }
    int getMin() { return mins.peek(); }
}
class MinStack {
  private readonly items: { value: number; min: number }[] = [];

  push(value: number) {
    const top = this.items.at(-1);
    this.items.push({ value, min: top ? Math.min(top.min, value) : value });
  }

  pop() { this.items.pop(); }
  top(): number { return this.items.at(-1)!.value; }
  getMin(): number { return this.items.at(-1)!.min; }
}

Trace the example: after the three pushes, the min column is -2, -2, -3. Popping -3 also pops its min, and the top of the min column is -2 again. The idea of storing “the answer so far” next to each element works for other questions too, like a running maximum or sum.

Monotonic stacks

A monotonic stack keeps its elements in increasing (or decreasing) order. Before pushing a new element, you pop everything that breaks the order, and the moment an element is popped, you’ve found its answer: the new element is the first one to its right that’s bigger (or smaller). Every element is pushed and popped once, so “next greater element for every position” is O(n) instead of O(n²).

It’s the engine behind Daily Temperatures, Next Greater Element, Largest Rectangle in Histogram, Trapping Rain Water (one of several approaches), Online Stock Span and “remove k digits to make the smallest number”.

Exercisemedium

Daily Temperatures

Given daily temperatures, return an array where answer[i] is the number of days you have to wait after day i for a warmer temperature. If there’s no future warmer day, answer[i] is 0.

[73, 74, 75, 71, 69, 72, 76, 73]  → [1, 1, 4, 2, 1, 1, 0, 0]
[30, 40, 50, 60]                  → [1, 1, 1, 0]
[30, 20, 10]                      → [0, 0, 0]

Find an O(n) solution.

›

Walk forward. Which days are still “waiting” for a warmer day? What do their temperatures look like, from oldest to newest?

›

The waiting days always have decreasing temperatures (otherwise a warmer one would have answered them). Keep their indices on a stack; when today is warmer than the top, today is that day’s answer: pop it and record the distance. Repeat, then push today.

SolutionTry it yourself firstTime O(n)Space O(n)
func dailyTemperatures(_ temps: [Int]) -> [Int] {
    var answer = Array(repeating: 0, count: temps.count)
    var waiting: [Int] = []                          // indices; temperatures decrease toward the top
    for today in temps.indices {
        while let day = waiting.last, temps[day] < temps[today] {
            waiting.removeLast()
            answer[day] = today - day
        }
        waiting.append(today)
    }
    return answer
}
fun dailyTemperatures(temps: IntArray): IntArray {
    val answer = IntArray(temps.size)
    val waiting = ArrayDeque<Int>()                  // indices; temperatures decrease toward the top
    for (today in temps.indices) {
        while (waiting.isNotEmpty() && temps[waiting.last()] < temps[today]) {
            val day = waiting.removeLast()
            answer[day] = today - day
        }
        waiting.addLast(today)
    }
    return answer                                     // days still waiting keep 0
}
int[] dailyTemperatures(int[] temps) {
    int[] answer = new int[temps.length];
    Deque<Integer> waiting = new ArrayDeque<>();      // indices; temperatures decrease toward the top
    for (int today = 0; today < temps.length; today++) {
        while (!waiting.isEmpty() && temps[waiting.peek()] < temps[today]) {
            int day = waiting.pop();
            answer[day] = today - day;
        }
        waiting.push(today);
    }
    return answer;
}
function dailyTemperatures(temps: number[]): number[] {
  const answer = new Array<number>(temps.length).fill(0);
  const waiting: number[] = [];                   // indices; temperatures decrease toward the top
  for (let today = 0; today < temps.length; today++) {
    while (waiting.length > 0 && temps[waiting.at(-1)!] < temps[today]) {
      const day = waiting.pop()!;
      answer[day] = today - day;
    }
    waiting.push(today);
  }
  return answer;
}

Each day is pushed once and popped at most once, so the whole thing is O(n). On the first example, day 2 (75) waits while 71 and 69 arrive; 72 answers 69 and 71 but not 75; 76 finally answers 72 and 75 (4 days). Follow-ups: iterate backwards with a stack of candidate warmer days (same complexity, sometimes easier to reason about), and Online Stock Span, the same idea for a stream where you can’t look ahead.

Deep dives

Deep diveEvaluating expressions with two stacksInterview favorite

“Evaluate 3 + 4 * (2 - 1)” (Basic Calculator) is a stack problem. Dijkstra’s shunting-yard idea uses two stacks: numbers and operators. When an operator arrives, first apply every operator on the stack with higher or equal precedence; a ( is pushed, and a ) applies operators until its (. At the end, apply what’s left.

Each token is pushed and popped once, so it’s O(n). The same machinery converts infix to postfix (reverse Polish) notation, which is how many calculators and compilers evaluate expressions.

Patterns: matching and nesting (stack), BFS and level order (queue), DFS without recursion (stack), next greater/smaller (monotonic stack), sliding max/min (monotonic deque), queue from two stacks (amortized O(1)).

var next = Array(repeating: -1, count: nums.count)
var stack: [Int] = []                                      // indices with decreasing values
for i in nums.indices {
    while let top = stack.last, nums[top] < nums[i] { next[stack.removeLast()] = nums[i] }
    stack.append(i)
}
val next = IntArray(nums.size) { -1 }
val stack = ArrayDeque<Int>()                              // indices with decreasing values
for (i in nums.indices) {
    while (stack.isNotEmpty() && nums[stack.last()] < nums[i]) next[stack.removeLast()] = nums[i]
    stack.addLast(i)
}
int[] next = new int[nums.length];
Arrays.fill(next, -1);
Deque<Integer> stack = new ArrayDeque<>();                 // indices with decreasing values
for (int i = 0; i < nums.length; i++) {
    while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) next[stack.pop()] = nums[i];
    stack.push(i);
}
const next = new Array<number>(nums.length).fill(-1);
const stack: number[] = [];                                 // indices with decreasing values
for (let i = 0; i < nums.length; i++) {
  while (stack.length > 0 && nums[stack.at(-1)!] < nums[i]) next[stack.pop()!] = nums[i];
  stack.push(i);
}

Check this part

  1. You need to visit a tree level by level. Which structure?

  2. How does Min Stack answer getMin() in O(1)?

  3. In a monotonic stack for "next greater element", when do you learn an element's answer?

  4. Daily Temperatures has a while loop inside a for loop. What is its complexity?

Reverse order or matching: a stack. Arrival order or level by level: a queue. Next greater or smaller element for every position: a monotonic stack, where each element learns its answer when it's popped.

Key takeaways

  • A stack is LIFO (push and pop at the top); a queue is FIFO (add at the back, remove from the front). Both are O(1).
  • Use a stack for matching and undo, a queue for “in the order they arrived”.
  • Swift: Array as a stack, Deque as a queue. Kotlin and Java: ArrayDeque for both. TypeScript: an array as a stack, an array with a head index as a queue.
  • After matching brackets, check that nothing is left on the stack.
  • Monotonic stacks answer “next greater/smaller” for every element in O(n): the answer is found when an element is popped.
  • Recursion is a hidden, size-limited stack; use an explicit stack when depth follows user data.
  • Know each language’s API quirks: Java ArrayDeque.push adds to the front, Stack and LinkedList are legacy choices.
  • Amortized O(1) (two-stack queues, dynamic arrays) can still produce occasional O(n) operations; bounded queues provide backpressure.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.