Queue and Stack
Today let’s study two important data structures together: queue and stack.
This article is based on LeetCode’s Explore tutorial, Introduction to Data Structure - Queue & Stack.
Introduction
The most commonly used collection is the array, and its most commonly used data-access operation is random access, usually called subscript access in C++.
But sometimes we want to restrict the order in which data is processed. The most common restrictions are First in first out and Last in first out. They correspond to two data structures: Queue and Stack.
We will study queue and stack from three angles: definition, implementation, and the built-in operations of each data structure.
Learning goals:
- Understand the principles of FIFO and LIFO data-processing order;
- Manually implement the data structures;
- Become familiar with the built-in Queue and Stack in the language;
- Solve basic Queue-related problems, especially BFS;
- Solve basic Stack-related problems;
- Understand how the system stack helps you solve DFS and other recursive problems.
Queue: First-in-first-out Data Structure
Definition
The most common metaphor for First in first out is waiting in line, that is, a queue. The person who enters the queue earliest is served earliest.
So a queue has only two modify methods:
- enqueue
- dequeue
Implementation
Queue is not a primitive data structure. We can implement it using a built-in array. In C++, queue is a container adapter, not a real container; internally, it is actually a deque.
Implementation example:
1 |
|
Circular Queue
In the previous implementation, the memory before p_start is wasted. To make full use of it, we can internally reuse the array in a circular way.
Circular Queue is also called a “Ring Buffer”.
Ring Buffer implementation:
1 | class MyCircularQueue { |
Applications of Queue
The most typical application is BFS.
BFS (Breadth-first Search) is generally used to find the shortest distance from a root node to a target node.
Scenarios where BFS is used:
- do traversal
- find the shortest path
Common data structures in these scenarios:
- graph
- tree
In concrete applications, nodes in BFS may be real nodes or states, and edges may be real edges or state transitions.
The BFS template must be memorized to improve speed and the chance of being bug-free in interviews and problem solving.
template 1
1 | /** |
template 2
1 | /** |
- In each round, the nodes in the queue are waiting to be processed.
- Each time the outer
whileloop runs, we move one step farther fromroot, sostep++.
template 2
In a graph, it is important to ensure that each node is not visited multiple times. Otherwise, BFS can fall into an infinite loop. At this point, we add a hashset to mark whether a node has already been visited.
1 | /** |
When can we skip visited?
- You are sure there will be no repeated visits, for example when traversing a tree.
- You intentionally want to add a node to the queue multiple times.
Stack
When I mention stack, I think of Jay Chou’s Qilixiang. In my head, one line becomes: my stack overflowed like rainwater.
In a LIFO data structure, the most recently added element is processed first. In a stack, the operation for adding an element is called push, and the operation for removing an element is called pop. Although C++ queue operations use the same names, in most languages, push and pop are stack-specific.
Like queue, most languages provide a built-in stack library. You do not need to reinvent the wheel; you only need to know the common stack operations, including push, pop, and top (getting the top element of the stack).
Applications of Monotonic Stack
https://leetcode.com/explore/featured/card/queue-stack/230/usage-stack/1363/
Intuition: maintain a monotonically decreasing stack. Traverse array T; when placing an element into the stack, pop all elements in the stack that are smaller than it and compute the corresponding interval.
Time complexity: O(n),
space complexity: O(n).
1 | class Solution { |
stack and DFS
DFS is one of the important applications of stack. It can be used to find a path from a root node to a target node, though not necessarily the shortest path. DFS is a backtracking algorithm. It only backtracks after reaching the deepest node, then tries other paths.
DFS template 1, recursive version
1 | /* |
DFS template 2, iterative version
The advantage of the recursive version is that it is easier to implement. The disadvantage is that if the recursion depth is too large, it can cause stack overflow.
At that point, you may want to use BFS or implement DFS with an explicit stack.
1 | /* |
The implementation logic is the same as the recursive solution. We simply use a while loop and an explicit stack to mimic the system stack.
Implement Queue using Stacks
Implement a queue using stacks.
I remember seeing the same problem in Cracking the Coding Interview. It feels like a classic problem. It requires the interviewee to be very familiar with both queue and stack.
Intuition: queue is FIFO and stack is LIFO, so we can implement one queue with two stacks. Define the operation of pouring one stack into another stack as reversing. Through reversing, we can turn LIFO into FIFO, and we only need to perform the reversing operation when we need to dequeue.
1 | class MyQueue { |
Implement Stack using Queues
So how do we use FIFO to implement LIFO? The solution may not be that obvious, but it is very simple. Every time we push, re-enqueue the existing elements in the queue once, which puts the most recently enqueued element at the first position.
Summary
DFS and BFS are simple in idea but not easy to implement, especially if you want to implement them bug-free and quickly.
Because they test a wide range of programming fundamentals, interviewers especially like asking these kinds of problems. You must memorize several templates fluently.
Queue and stack are even more fundamental data structures. They appear everywhere in computer science. Although they are often not tested directly, many algorithms rely on them. Being able to handwrite stack and queue, and to use built-in stack and queue fluently, is foundational for every qualified programmer.