How to prepare for software interview questions - Simulate a queue with two stacks

- 10 mins

Continuing the series on How to prepare for software interview questions.

The previous article can be seen here.

Today, I will break down the following question:

Implement a queue with 2 stacks. Your queue should have an enqueue and a
dequeue method and it should be "first in first out" (FIFO).

I actually got this problem myself in one of my interviews with a big company (sorry, can’t say which one because I signed an NDA - but I can tell you that I got the internship at them after, so you can check out my resume.

Framework

Again, the framework that we should always keep in mind during the interviews is:

  1. Clarify the problem
  2. Find the simplest solution.
  3. Iterate on the solution until consensus is reached with your interviewer and he’s happy.

Also keep in mind the following when you’re going through the interviews:

1. Clarifications

You could ask:

You see, for this problem there are not a lot of questions you can ask, really. There are some problems that are very easy and clear to be stated, but think about different corner cases and so on. However, it’s not bad to ask about corner cases later when you do the actual coding.

2. Simplest solution.

This is rather a simulation problem, so I always like to start them with an example. In fact, it’s always great to start with an example to see what you have to deal with.

Let’s suppose I want to simulate the following operations on my queue:

q.enqueue(1)      # front -> [1] <- back
q.enqueue(2)      # front -> [1, 2] <- back
q.enqueue(3)      # front -> [1, 2, 3] <- back
q.dequeue()       # should return 1, the front of the queue
q.enqueue(5)      # front -> 2 3 5 <- back

Now that we know how this queue works, let’s see how we can use a stack. We have two stacks, so let’s think about the easiest solution. Well an obvious one is to push to one queue for each enqueue operation. Let’s see how that works for this example.

q.enqueue(1)      # stk1 -> [1] <- top
q.enqueue(2)      # stk1 -> [1, 2,] <- top
q.enqueue(3)      # stk1 -> [1, 2, 3] <- top

q.dequeue()       # now, we see that we need to return the first element, 1
                  # but in our stk1, we can only get the top, that is 3
                  # However, we do have another queue, so why don't we just move
                  # all of them to the other one one by one so that we can get the first element (bottom)

                  # we keep adding the top of stk1 to stk2
                  # stk1 -> [1, 2] <- top     stk2 -> [3] <- top
                  # stk1 -> [1]  <- top       stk2 -> [3, 2] <- top
                  # stk1 -> []  <- top        stk2 -> [3, 2, 1] <- top

                  # Now we see that stk2 is actually stk1 reversed, so we can just return the first element (ie 1)
                  # Then we put everything back the way it was, without the first element from stk1 which was dequeued

                  # stk1 -> []  <- top        stk2 -> [3, 2] <- top
                  # stk1 -> [2]  <- top       stk2 -> [3] <- top
                  # stk1 -> [2, 3]  <- top    stk2 -> [] <- top

                  # We are back in the same state we were initially, just without 1, which is great

However, it looks like our complexity is pretty back. For each enqueue, we do one push to the first stack, but for each dequeue, we have to move all elements, which is O(N) time - awful.

3. Iterate

The first and only iteration that you can see here is that you don’t really need to move everything from stk2 back to stk1, because they are already in reversed, and subsequent dequeue calls can return the stk2 head directly if that stack is non-empty. If it is empty, we have to move everything from stk1 to stk2 and then return the top of stk2.

So let’s simulate again

q.enqueue(1)      # stk1 -> [1] <- top        stk2 -> []
q.enqueue(2)      # stk1 -> [1, 2,] <- top    stk2 -> []
q.enqueue(3)      # stk1 -> [1, 2, 3] <- top  stk2 -> []

q.dequeue()       # since stk2 is empty, we move evrything into stk2
                  # stk1 -> [1, 2, 3] <- top  stk2 -> []
                  # stk1 -> [1, 2] <- top     stk2 -> [3]
                  # stk1 -> [1] <- top        stk2 -> [3, 2]
                  # stk1 -> [] <- top         stk2 -> [3, 2, 1]

                  # then, return stk2.top() => 1 and pop it

                  # stk1 -> [] <- top         stk2 -> [3, 2]

q.dequeue()       # on subsequent calls to dequeu, we can simply return stk2.top(), which is 2
                  # stk1 -> [] <- top         stk2 -> [3]

q.enqueue(4)      # we always push to first stk -> stk1 => [4]
q.enqueue(r)      # stk1 => [4, 5]

q.dequeue()       # now stk2 is [3], so we return it and remove it => stk2 = []
q.dequeue()       # now stk2 is [], empty, so we need to move everything to stk2 again
                  # stk1 -> [4, 5] <- top      stk2 -> []
                  # stk1 -> [4] <- top         stk2 -> [5]
                  # stk1 -> [] <- top         stk2 -> [5, 4]

                  # Return 4 and pop it
                  # stk1 -> [] <- top         stk2 -> [5]

That’s it. In some sense, we have an “input stack” and an “output stack”.

The complexity is O(1) per operation, averaged. Meaning that if we insert N elements and do N pops, because every element is moved from one stack to the other exactly once.

Here’s the code

class DutyQueue:

  def __init__(self):
    self.stk1 = [] # simulate the stack with one list
    self.stk2 = []

  def enqueue(self, val):
    self.stk1.append(val)

  def dequeue(self):
    if len(self.stk2) > 0:
      return self.stk2.pop()

    # if output stack is empty, we move everything
    while len(self.stk1) > 0:
      el = self.stk1.pop()      # top of stk1
      self.stk2.append(el)      # push to stk2

    # Handle only corner case
    if len(self.stk2) == 0:
      raise Exception("Called dequeue on empty queue")

    return self.stk2.pop()

Last but not least, write some tests if you can - you’ll get bonus points.

q = DutyQueue()

q.enqueue(1)              # q = [1]         stk1 = [1]          stk2 = []
q.enqueue(2)              # q = [1, 2]      stk1 = [1, 2]       stk2 = []
q.enqueue(3)              # q = [1, 2, 3]   stk1 = [1, 2, 3]    stk2 = []

assert(q.dequeue() == 1)
                          # q = [1, 2, 3]   stk1 = []    stk2 = [3, 2, 1]
                          # return 1
                          # q = [2, 3]   stk1 = []    stk2 = [3, 2]

q.enqueue(4)              # q = [2, 3, 4]   stk1 = [4]    stk2 = [3, 2]
assert(q.dequeue() == 2)  # q = [3, 4]      stk1 = [4]    stk2 = [3]

assert(q.dequeue() == 3)  # q = [4]         stk1 = [4]    stk2 = []

assert(q.dequeue() == 4)  # q = [4]         stk1 = []    stk2 = [4]
                          # return 4
                          # q = []         stk1 = []    stk2 = []
try:
  q.dequeue()             # The queue is empty, so this should throw an error
except Exception:
  pass
else:
  assert(False)

print("All tests passed")

You can play around with the code in the following repl.it.

That would be all, let me know what you think.

PS. I have a great community of engineers that want to get into freelancing or top-tier software companies like Google, Apple, Amazon, Microsoft or Facebook. You can check it out at https://academy.dutylabs.ro. Looking forward to seeing you there.

PPS. If you want to get exclusive articles like this on your inbox fast, sign up with your email below and never miss an article like this - I plan to do a lot more in the future.

Stay healthy, safe and motivated!

Cosmin

Cosmin-Ionuț Rusu

Cosmin-Ionuț Rusu

A simple action-taker who likes to just do stuff

comments powered by Disqus
rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora