Interview Cracker GitHub

Stacks, Queues and Heaps

Three small structures that unlock a surprising number of problems once you learn to recognise their signals.


🟢 Q. Stack vs queue?

Answer. Both restrict where you may add and remove; they differ in which end.

All four operations are O(1) in a good implementation.

Where each shows up:

Stack Queue
Function call stack BFS / level-order traversal
Undo history Task and message queues
Bracket matching Print spoolers, request buffers
DFS (explicit or via recursion) Rate limiting, buffering
Expression evaluation Producer–consumer pipelines

The rule of thumb: DFS uses a stack, BFS uses a queue. That one fact decides a lot of problems.


🟢 Q. How do you implement a queue efficiently?

Answer. Not with a plain array and pop(0) — that shifts every element, so it is O(n) per dequeue and O(n²) overall.

Three correct options:

The classic puzzle — implement a queue with two stacks — is worth knowing: push onto in; to pop, if out is empty pour all of in into it (reversing order) then pop from out. Each element moves at most twice, so it is O(1) amortised.


🟡 Q. What is a monotonic stack and what does it solve?

Answer. A stack kept in sorted order — you pop elements that break the order before pushing. It answers "for each element, what is the next element greater (or smaller) than it?" in O(n) instead of O(n²).

def next_greater(nums):
    result = [-1] * len(nums)
    stack = []                       # holds indices, values decreasing
    for i, n in enumerate(nums):
        while stack and nums[stack[-1]] < n:
            result[stack.pop()] = n  # n is the answer for that index
        stack.append(i)
    return result

# [2, 1, 3] -> [3, 3, -1]

Each index is pushed once and popped once, so despite the inner while the total work is O(n).

The tell: "next greater / previous smaller element", "largest rectangle in a histogram", "daily temperatures", "trapping rain water". If a problem asks you to relate each element to the nearest element satisfying a comparison, try a monotonic stack.


🟢 Q. What is a heap, and what is it good at?

Answer. A complete binary tree satisfying the heap property: in a min-heap, every parent is ≤ its children (so the minimum sits at the root).

It is stored in a flat array, not with pointers — for index i, children are at 2i+1 and 2i+2, parent at (i-1)//2. That is why heaps are fast: no allocation, excellent cache locality.

Operation Cost
Peek min/max O(1)
Insert O(log n) — sift up
Extract min/max O(log n) — sift down
Build from n items O(n), not O(n log n)
Search for arbitrary value O(n) — no ordering between siblings

Two points interviewers probe:


🟡 Q. How do you find the k largest elements in a stream of n items?

Answer. A min-heap of size k.

import heapq

def k_largest(stream, k):
    heap = []                              # min-heap of the best k so far
    for item in stream:
        if len(heap) < k:
            heapq.heappush(heap, item)
        elif item > heap[0]:               # bigger than the smallest kept
            heapq.heapreplace(heap, item)
    return sorted(heap, reverse=True)

O(n log k) time, O(k) space.

The counter-intuitive part, and the point of the question: to track the k largest you use a min-heap. The root is the weakest of your current champions, so it is both the cheapest to check against and the right one to evict.

Why not sort? Sorting is O(n log n) time and O(n) space — impossible if the stream does not fit in memory. When k is small and n is huge, O(n log k) with O(k) space is the meaningful win.


🟡 Q. What is a priority queue and how does it differ from a heap?

Answer. A priority queue is an abstract data type: "give me the highest-priority item next". A heap is the usual concrete implementation.

The distinction matters because other implementations exist with different trade-offs — a sorted array gives O(1) extraction but O(n) insertion; a Fibonacci heap gives O(1) amortised insert and decrease-key, which is what makes the textbook Dijkstra bound O(E + V log V).

In interviews, "priority queue" almost always means "binary heap". It is the engine inside Dijkstra's algorithm, A*, Huffman coding, and event-driven simulation.


🟡 Q. How do you check for balanced brackets?

Answer. The canonical stack problem. Push opening brackets; on a closing bracket, the top of the stack must be its partner.

PAIRS = {")": "(", "]": "[", "}": "{"}

def is_balanced(s):
    stack = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)
        elif ch in PAIRS:
            if not stack or stack.pop() != PAIRS[ch]:
                return False           # wrong partner, or nothing open
    return not stack                   # anything left open is unbalanced

O(n) time, O(n) space. The two edge cases that separate a correct answer from a nearly-correct one: a closing bracket when the stack is empty, and leftover openers at the end. Candidates routinely forget the second and return True for "(((".