Problem overview
A queue gives values back in the order they arrived, like a line at a shop. A stack gives back the newest value first. The task is to build the class MyQueue out of two stacks, using only what a stack does: add on top, look at or remove the top, and check whether it is empty. The class needs push(_:) (add at the back), peek() (the front value, kept), pop() (the front value, removed) and empty().
In the first example, after push(1) and push(2), peek() returns 1, pop() removes and returns 1, and empty() is false because 2 is still there. The statement asks for O(1) amortized time per operation. pop and peek are only called on a non-empty queue.
Approach: reverse the order once, and only when needed
A stack and a queue disagree about which value is at the exit. Popping a stack gives the newest value, but the queue needs the oldest, which is at the bottom. Moving values from one stack onto another reverses their order: the one on top goes first, so it lands at the bottom. After the move, the oldest value is on top of the second stack.
The simple version moves everything over and back on every pop. That costs O(n) per call. The better version notices that the second stack, once filled, is already in the right order, so it can serve many pops without any more moves. The class uses:
inStack: every pushed value goes on top of it;outStack: values leave from it. Its top is the oldest value.
Only when outStack is empty and the front is needed are all the values moved from inStack to outStack. While outStack still has values, they all arrived before anything in inStack, so its top is still the front of the queue. New pushes can keep piling up on inStack and wait for their turn.
With push(1), push(2), push(3), inStack is [1, 2, 3] and outStack is empty. The first peek() moves the values, leaving inStack empty and outStack as [3, 2, 1] with the top (1) at the end. It returns 1. A later push(4) gives inStack = [4], and the next three pops return 1, 2, 3 without any move, because outStack still has values. The fourth pop then moves the 4.
Step by step
- Declare
inStackas avarof type[Int], set to an empty array. The top of a stack is the end of the array, whereappend,lastandremoveLast()work. - Declare
outStackthe same way. - In
push, appendxtoinStack. - In
peek, start anifthat runs whenoutStack.isEmpty. Values move only then. - Inside it, loop while
!inStack.isEmpty, because every value must move so the oldest one ends up on top. - Move one value:
outStack.append(inStack.removeLast()). - After the
if, returnoutStack.last!, the oldest value. Reading it removes nothing. - In
pop, callpeek()and keep the result infrontwithlet.peekalready moves values if needed. - Remove the top of
outStackwithremoveLast(). - Return
front. - In
empty, return whether both stacks are empty, joined with&&.
The solution in Swift
class MyQueue {
var inStack: [Int] = []
var outStack: [Int] = []
func push(_ x: Int) {
inStack.append(x)
}
func peek() -> Int {
if outStack.isEmpty {
while !inStack.isEmpty {
outStack.append(inStack.removeLast())
}
}
return outStack.last!
}
func pop() -> Int {
let front = peek()
outStack.removeLast()
return front
}
func empty() -> Bool {
return inStack.isEmpty && outStack.isEmpty
}
}
The Swift details worth a closer look:
- Arrays as stacks. An array has no
pushorpop.appendadds at the end,lastreads the end andremoveLast()removes it. All three areO(1)on average, so the end of the array is the top. lastis an optional. It isnilon an empty array, and the!unwraps it. That is safe inpeekbecause the lesson only calls it on a non-empty queue, and by thenoutStackhas at least one value.- The unused result of
removeLast(). Inpop, the lineoutStack.removeLast()throws away the removed value, which is already infront. Swift accepts that becauseremoveLast()is marked as allowed to have an unused result. - Calling another method. Inside the class,
peek()is called by name alone. This keeps the moving logic in one place. removeLast()on an empty array stops the program with a run-time error, so callingpopon an empty queue would crash. The lesson never does that.- A class. The methods change the stored arrays. In a struct they would need
mutating; in a class they don't.
Complexity
Time: amortized O(1) per operation. A single peek or pop can be expensive: when outStack is empty and inStack holds a thousand values, it moves all of them. But look at one value's life. It is pushed once, moved from inStack to outStack at most once, and popped once. A value never goes back, so each value causes at most three stack operations in total. For n operations, the work is at most about 3n, that is O(n) overall and O(1) on average per call.
A counted run shows it. After 1,000 pushes and one pop, there have been 1,001 operations and 1,000 moves: the first pop paid for everything. After 999 more pops, the total is 2,000 operations and still 1,000 moves: those pops moved nothing. Then 500 pairs of a push and a pop make 3,000 operations and 1,500 moves in total, which is still no more than one move per value.
Space. O(n): each value is in exactly one of the two stacks at any time.
Edge cases and common mistakes
- Moving while
outStackstill has values. That puts newer values under older ones and breaks the order. With 1 and 2 pushed,pop()returns 1, thenpush(3): the nextpeek()andpop()must return 2, and only then 3. The queue then ends up empty. - Moving back and forth on every call. It works, but costs
O(n)per call. - Checking only one stack in
empty. After onepeek, all the values may sit inoutStack, or all ininStack. The queue is empty only when both are. - A new queue.
empty()is true. Afterpush(7)it is false, and afterpop()returns 7 it is true again. - Moving only part of
inStack. All of its values must move, or the oldest one stays buried at the bottom. - Reading the wrong end. The top of a stack made from an array is its last element, not
first.
Testing
push(1),push(2), thenpeek(),pop(),empty()give 1, 1, false.push(1),push(2),pop(),push(3), thenpeek(),pop(),pop(),empty(): the first pop gives 1, then the last four give 2, 2, 3 and true.- On a new queue,
empty()is true;push(7),empty(),pop(),empty()give false, 7, true. push(1),push(2),push(3),push(4), then four pops, give 1, 2, 3, 4.
