Problem overview
You write a class MyQueue that behaves like a queue, but may only use stacks. push(x) adds x at the back, peek() returns the front value without removing it, pop() removes and returns the front value, and empty() says whether the queue holds nothing. A queue gives values back in arrival order; a stack gives back the newest first. The stacks may only add on top, look at or remove the top, and check for emptiness.
In the second example, push 1 and 2, then pop() returns 1. Push 3, and peek() and the next pop() both return 2, because 2 has been waiting longer than 3. The following pop() returns 3, and empty() is then true. pop and peek are only called when the queue is not empty.
Approach
One stack is not enough: the oldest value is at the bottom, out of reach. A first idea uses two stacks and moves everything between them on every pop: empty the first stack onto the second to reach the bottom value, remove it, then move everything back. That takes O(n) per pop.
The better idea notices that moving values from one stack onto another reverses their order. If values 1, 2, 3 were pushed onto inStack, with 3 on top, popping them one by one onto outStack leaves 1 on top there. So outStack has the values in queue order, oldest on top. The design is:
pushalways goes ontoinStack.- When the front is needed and
outStackis empty, move all ofinStackover tooutStack. IfoutStackstill has values, do not move anything: they all arrived before everything ininStack, so the top ofoutStackis already the front. - The front is the top of
outStack.
Moving back is never needed. A value in outStack leaves from there, and new values wait in inStack until outStack runs dry.
Why it is amortized O(1)
A single pop or peek can move many values, so it can take O(n). But count the moves per value instead of per call. Each value is pushed onto inStack once, moved to outStack at most once (values never go back), and removed from outStack at most once. That is at most three stack operations per value, however long it stays in the queue. So n operations cost O(n) in total, which is O(1) per operation on average. That's what "amortized" means.
Step by step
- Two stacks. Declare
inStackandoutStackas emptyArrayDeque<Int>(). The end of the deque is the top of the stack:addLast,last()andremoveLast()all work there. push.inStack.addLast(x).peek. IfoutStack.isEmpty(), loop whileinStack.isNotEmpty()and move each value withoutStack.addLast(inStack.removeLast()). The newest lands deepest and the oldest lands on top.- Return the top of
outStackwithlast(). Reading removes nothing. pop. Callpeek()and keep its answer infront: it moves values if needed and finds the front, sopopdoesn't repeat that. ThenoutStack.removeLast()and returnfront.empty. The values are split between the stacks, so the queue is empty only if both are:inStack.isEmpty() && outStack.isEmpty().
The solution in Kotlin
class MyQueue {
val inStack = ArrayDeque<Int>()
val outStack = ArrayDeque<Int>()
fun push(x: Int) {
inStack.addLast(x)
}
fun peek(): Int {
if (outStack.isEmpty()) {
while (inStack.isNotEmpty()) {
outStack.addLast(inStack.removeLast())
}
}
return outStack.last()
}
fun pop(): Int {
val front = peek()
outStack.removeLast()
return front
}
fun empty(): Boolean {
return inStack.isEmpty() && outStack.isEmpty()
}
}
Kotlin details worth knowing:
ArrayDequeis Kotlin's own. Without an import it iskotlin.collections.ArrayDeque, a growable array that works at both ends. Used only at its end, it behaves as a stack.- The deque is also a queue. Kotlin's
ArrayDequealso hasremoveFirst(), so a real queue would be one line. This exercise forbids it: the point is to build the behaviour from stack operations alone. last()andremoveLast()throw on an empty deque (NoSuchElementException). The code never calls them on an emptyoutStack, becausepopandpeekare only called on a non-empty queue.- Calling a sibling method. Inside the class,
peek()inpopis a call on the same object. Nothis.is needed. &&short-circuits. IfinStackisn't empty,empty()returnsfalsewithout looking at the second stack.
Seeing it run
This code pushes three values and prints both stacks (the last number is the top) after the operations:
fun main() {
val queue = MyQueue()
queue.push(1)
queue.push(2)
queue.push(3)
println("in = ${queue.inStack}, out = ${queue.outStack}")
println(queue.peek())
println("in = ${queue.inStack}, out = ${queue.outStack}")
println(queue.pop())
queue.push(4)
println("in = ${queue.inStack}, out = ${queue.outStack}")
println(queue.pop())
println(queue.pop())
println("in = ${queue.inStack}, out = ${queue.outStack}")
println(queue.pop())
println(queue.empty())
}
It prints:
in = [1, 2, 3], out = []
1
in = [], out = [3, 2, 1]
1
in = [4], out = [3, 2]
2
3
in = [4], out = []
4
true
After three pushes everything sits in inStack. The first peek() moves all three values over, and 1 ends up on top of outStack. After pop() returns 1 and 4 is pushed, 4 waits in inStack while 2 and 3 leave from outStack. Only when outStack is empty does the next pop() move 4 across and return it. Values leave in the order 1, 2, 3, 4, the order they were pushed.
Complexity
- Time:
pushandemptyare O(1).peekandpopare O(1) amortized, and O(n) in the worst single call, whenoutStackis empty and all n values move. Over any sequence of operations, each value is moved at most once, so the total is proportional to the number of operations. - Space: O(n) for n values in the queue.
Edge cases and common mistakes
- Moving while
outStackstill has values. The new values would be placed on top of older ones, andpopwould return a newer value before an older one. Move only whenoutStackis empty. - Moving only part of
inStack. Move all of it, or the order insideoutStackbreaks. - Moving back and forth. Putting everything back in
inStackafter eachpopworks but makes every call O(n). - Checking only one stack in
empty. After apeek, all the values can be inoutStackwhileinStackis empty, and the other way round after apush. - Removing in
peek.peekonly reads. Removing belongs topop. - A new queue. Both stacks are empty, so
empty()is true.
