Stacks & Queues · 2 of 38
Implement Queue from Scratch
Build a fair waiting line where people join at the back and leave from the front, without anyone having to shuffle forward.
- Simple way
- time O(n) per dequeue
room O(capacity) - Clever way
- time O(1) per operation
room O(n)
The problem, simply
Build your own queue. A queue is a waiting line: the first one in is the first one out.
It must support:
enqueue(x):xjoins at the back.dequeue(): the one at the front leaves, and you give it back.peek(): look at the front without removing it.isEmpty()andsize().
Every action should take the same small amount of time, however long the line gets.
A story to remember it
Picture it
Three kids in line: 10 came first, then 20, then 30. They leave from the front, which is 10. Inside, the line can be a chain of boxes with a front tag and a rear tag.
- looking now
Examples
A normal run
- In
enqueue(1), enqueue(2), enqueue(3), peek(), dequeue(), size()- Out
peek = 1, dequeue = 1, size = 2
1 joined first, so it is at the front and leaves first.
Join and leave mixed
- In
enqueue(5), dequeue(), enqueue(6), enqueue(7), dequeue()- Out
dequeues give 5, 6
After 5 leaves, the line is empty. Then 6 and 7 join, and 6 is at the front.
Empty, then used again
- In
enqueue(1), dequeue(), enqueue(2), peek()- Out
dequeue = 1, peek = 2
This catches a sneaky bug. When the line becomes empty, the rear tag must be reset too, or the next join goes to a box that is gone.
Leave an empty line
- In
dequeue()- Out
error: queue underflow
Nobody is waiting, so say so clearly.
Way 1: the simple way
Use an array. New kids go in the next free box at the end. When the front kid leaves, take arr[0] and shift every other kid one box to the left.
- Keep
count, the number of kids. - Enqueue: write into
arr[count], then add one tocount. - Dequeue: save
arr[0], copyarr[1..count-1]one step left, subtract one fromcount.
After 10 leaves, 20, 30 and 40 must all slide one box left. With a long line, that is a lot of sliding for every single dequeue.
- checking
class ShiftQueue {
private final int[] arr;
private int count = 0;
ShiftQueue(int capacity) { arr = new int[capacity]; }
void enqueue(int x) {
if (count == arr.length) throw new RuntimeException("queue overflow");
arr[count++] = x; // join at the back
}
int dequeue() {
if (count == 0) throw new RuntimeException("queue underflow");
int val = arr[0];
for (int i = 1; i < count; i++) { // everyone steps forward
arr[i - 1] = arr[i];
}
count--;
return val;
}
int peek() {
if (count == 0) throw new RuntimeException("queue is empty");
return arr[0];
}
boolean isEmpty() { return count == 0; }
int size() { return count; }
}How long it takes: enqueue is O(1), but every dequeue shifts the whole line, O(n). Extra room: the fixed array, O(capacity).
Way 2: the clever way
Nobody steps forward. Make the line a chain of boxes, a linked list, and keep two tags:
frontsits on the box that leaves next.rearsits on the last box, where new boxes are hooked on.
- Enqueue: make a new box. If the line is empty, both tags go to it. Otherwise hook it after
rear, then moverearto it. - Dequeue: take the value in
front, then movefrontto the next box. Iffrontbecame null, the line is empty, so setrearto null too. - Keep a counter for
size().
A line with a front tag and a rear tag
01 / 10
The line is empty. Both front and rear point at null.
- size
- 0
- looking now
- rejected
- just added
class LinkedQueue {
private static class Node {
int val;
Node next;
Node(int val) { this.val = val; }
}
private Node front, rear; // leave here, join there
private int count;
void enqueue(int x) {
Node box = new Node(x);
if (rear == null) {
front = rear = box; // first kid in an empty line
} else {
rear.next = box; // hook on at the back
rear = box;
}
count++;
}
int dequeue() {
if (front == null) throw new RuntimeException("queue underflow");
int val = front.val;
front = front.next; // slide the front tag
if (front == null) rear = null; // line is empty again
count--;
return val;
}
int peek() {
if (front == null) throw new RuntimeException("queue is empty");
return front.val;
}
boolean isEmpty() { return front == null; }
int size() { return count; }
}How long it takes: every action moves at most two tags, O(1). Extra room: one box per waiting value, O(n).
Watch out for
- Enqueue at the head and dequeue at the tail does not work well with a singly linked list. Removing the tail needs the box before it, which takes a walk. Join at the tail, leave at the head.
- An array where
frontonly moves forward and never wraps around wastes the boxes at the start. That leads to a "full" queue that is mostly empty. The fix is a circular array. - Peek or dequeue on an empty line must not crash with a null pointer.
What interviewers ask next
- Can you do it with an array and still get
O(1)dequeue? Yes, with a circular array. Letfrontandrearwrap around with% capacityand keep acountto tell full from empty. - What about a queue that can grow without limit using an array? Use a circular array and double it when full. When copying, unroll it so
frontlands at index 0. - How does Java do it?
ArrayDequeis a growable circular array.LinkedListalso works as aQueuebut uses more memory per item. - Build a queue from two stacks. Push into one stack, pop from the other, and pour the first into the second only when the second is empty.
- Where are queues used? Breadth first search, printer jobs, task schedulers, and message buffers between programs.
Remember this
Grown-up words: the waiting line is a queue, first in first out is FIFO, joining is enqueue, leaving is dequeue, the chain of boxes is a singly linked list, and the two name tags are the head and tail pointers.