Problem overview
A queue gives values back in the order they arrived, like a line at a shop: the first value pushed is the first to leave. A stack does the opposite: the last value pushed leaves first. The task is to build a queue out of two stacks, using only what a stack offers: add a value on top, look at or remove the top value, and check whether it is empty.
The class MyQueue has four methods:
push(x: Int): Unitaddsxat the back of the queue.peek(): Intreturns the value at the front without removing it.pop(): Intremoves the value at the front and returns it.empty(): Booleantells whether the queue holds no values.
pop and peek are only called when the queue is not empty. Each operation must take O(1) time amortized.
Example: after push(1) and push(2), peek() returns 1 and pop() returns 1, and empty() is then false, because 2 is still waiting.
Approach
The simple way and its cost
Use one stack for everything. A push goes on top. The front of the queue, however, is at the bottom, so every pop would have to move all the values to a second stack, take the bottom one, and move everything back. That is O(n) work per call, where n is the number of values in the queue.
Two stacks, with the order reversed once
Moving values from one stack to another reverses their order. If values 1, 2, 3 were pushed in that order, 3 is on top of the first stack. Popping them one by one onto a second stack puts 3 at the bottom and 1 on top. Now the oldest value is on top and a pop gives 1, which is what a queue needs.
So the class has:
inStack, where every pushed value goes. Its top is the newest value.outStack, where values leave from. Its top is the oldest value that is still in the queue.
The central rule: move values only when outStack is empty, and then move all of them. While outStack still holds values, all of them arrived before anything in inStack, so its top is still the front. Moving more values on top of them would put newer values in front of older ones and break the order.
A picture of the stacks (the top is written first):
after push 1, 2, 3: in Stack(3, 2, 1) out Stack()
after peek(): in Stack() out Stack(1, 2, 3)
after push 4: in Stack(4) out Stack(1, 2, 3)
Then four pops return 1, 2, 3, 4: the first three come from outStack, and the fourth finds it empty and moves 4 over first.
Why it is amortized O(1)
One peek or pop may move many values, so a single call can be slow. But count the moves per value instead of per call. A value is pushed on inStack once, moved to outStack at most once, and popped from outStack once. That is at most three stack operations per value in its whole life. So n operations cost O(n) in total, and the average per operation, the amortized cost, is O(1).
Counting the moves in a run bears this out: pushing 100,000 values and then popping all of them makes exactly 100,000 moves, one per value, not 100,000 times 100,000.
Step by step
- Declare
inStack, avalholding a new, emptyscala.collection.mutable.Stack[Int](). Every pushed value goes on top of it. - Declare
outStackthe same way: the stack the values leave from. - In
push, callinStack.push(x). - In
peek, start aniffor whenoutStack.isEmpty. - Inside it, loop while
inStack.nonEmpty. - Move one value per pass:
outStack.push(inStack.pop()). The newest value leavesinStackfirst and lands deepest inoutStack; the oldest leaves last and ends on top. - After the
if, returnoutStack.top. Whether or not values were just moved, the oldest value is on top, andtopremoves nothing. - In
pop, callpeek()and store the result infront. It moves the values if needed and finds the front, sopopdoes not repeat that. - Remove the top of
outStackwithoutStack.pop(). - Return
front, which was read before it was removed. - In
empty, returninStack.isEmpty && outStack.isEmpty. The values are split between the stacks, so the queue is empty only when both are.
The solution in Scala
class MyQueue {
val inStack = scala.collection.mutable.Stack[Int]()
val outStack = scala.collection.mutable.Stack[Int]()
def push(x: Int): Unit = {
inStack.push(x)
}
def peek(): Int = {
if (outStack.isEmpty) {
while (inStack.nonEmpty) {
outStack.push(inStack.pop())
}
}
return outStack.top
}
def pop(): Int = {
val front = peek()
outStack.pop()
return front
}
def empty(): Boolean = {
return inStack.isEmpty && outStack.isEmpty
}
}
Using the class on the second example:
val q = new MyQueue
q.push(1)
q.push(2)
println(q.pop())
q.push(3)
println(q.peek())
println(q.pop())
println(q.pop())
println(q.empty())
1
2
2
3
true
Scala details:
scala.collection.mutable.Stack. It haspush,pop(),top,isEmptyandnonEmpty. Its string form lists the top first, as in the picture above.pop()andpush(...)return values.pop()returns the removed value, and inpopof the queue its result is dropped on purpose, becausefrontalready holds it.pushreturns the stack itself, but the queue'spushis declaredUnit, so the result is thrown away.- Methods call each other by name. Inside the class,
popcallspeek()with no prefix. - Empty-parentheses methods. The methods are declared with
(), such asdef peek(): Int, and called asq.peek(). - Using a
valfor a mutable stack.valfixes which stack object the name refers to; the stack's contents still change.
Complexity
Time. push, empty and a peek or pop that finds outStack non-empty are O(1). A peek or pop that finds it empty moves every value of inStack, which is O(n) for that single call. Each value moves at most once, so a sequence of n operations costs O(n) in total: O(1) amortized per operation.
Space. O(n) for the n values in the two stacks.
Edge cases and common mistakes
- Moving values when
outStackis not empty. Pushing 1 and 2, popping once, then pushing 3 shows the problem. If everypeekmoved the values ofinStackon top ofoutStack, the pops would return 1, 3 and 2 instead of 1, 2 and 3. Theif (outStack.isEmpty)test prevents it. - Moving only part of
inStack. Moving the whole stack at once keeps the oldest value on top ofoutStack. A partial move leaves the order wrong. - Checking only one stack in
empty. Afterpush(7)the value sits ininStackandoutStackis empty; afterpeek()it is the other way round. Both checks are needed. - Doing
peek's work twice inpop. Callingpeek()first makes sureoutStackhas the front on top, sooutStack.pop()removes the right value. - Popping an empty queue.
topon an empty stack throwsNoSuchElementException. The statement promises thatpopandpeekare never called on an empty queue. - Using
topas if it removed.topreads and leaves the value in place. Onlypop()removes.
Testing
- Push 1, push 2, then
peek(),pop(),empty()give1,1,false. - Push 1, push 2,
pop(), push 3,peek(),pop(),pop(),empty()give1,2,2,3,true. - On a new queue:
empty(), push 7,empty(),pop(),empty()givetrue,false,7,true.
