Implement a queue in which push_rear(), pop_front() and get_min() are all constant time operations

28,998

Solution 1

You can implement a stack with O(1) pop(), push() and get_min(): just store the current minimum together with each element. So, for example, the stack [4,2,5,1] (1 on top) becomes [(4,4), (2,2), (5,2), (1,1)].

Then you can use two stacks to implement the queue. Push to one stack, pop from another one; if the second stack is empty during the pop, move all elements from the first stack to the second one.

E.g for a pop request, moving all the elements from first stack [(4,4), (2,2), (5,2), (1,1)], the second stack would be [(1,1), (5,1), (2,1), (4,1)]. and now return top element from second stack.

To find the minimum element of the queue, look at the smallest two elements of the individual min-stacks, then take the minimum of those two values. (Of course, there's some extra logic here is case one of the stacks is empty, but that's not too hard to work around).

It will have O(1) get_min() and push() and amortized O(1) pop().

Solution 2

Okay - I think I have an answer that gives you all of these operations in amortized O(1), meaning that any one operation could take up to O(n), but any sequence of n operations takes O(1) time per operation.

The idea is to store your data as a Cartesian tree. This is a binary tree obeying the min-heap property (each node is no bigger than its children) and is ordered in a way such that an inorder traversal of the nodes gives you back the nodes in the same order in which they were added. For example, here's a Cartesian tree for the sequence 2 1 4 3 5:

       1
     /   \
    2      3
          / \
         4   5

It is possible to insert an element into a Cartesian tree in O(1) amortized time using the following procedure. Look at the right spine of the tree (the path from the root to the rightmost leaf formed by always walking to the right). Starting at rightmost node, scan upward along this path until you find the first node smaller than the node you're inserting.
Change that node so that its right child is this new node, then make that node's former right child the left child of the node you just added. For example, suppose that we want to insert another copy of 2 into the above tree. We walk up the right spine past the 5 and the 3, but stop below the 1 because 1 < 2. We then change the tree to look like this:

       1
     /   \
    2      2
          /
         3
        / \
       4   5

Notice that an inorder traversal gives 2 1 4 3 5 2, which is the sequence in which we added the values.

This runs in amortized O(1) because we can create a potential function equal to the number of nodes in the right spine of the tree. The real time required to insert a node is 1 plus the number of nodes in the spine we consider (call this k). Once we find the place to insert the node, the size of the spine shrinks by length k - 1, since each of the k nodes we visited are no longer on the right spine, and the new node is in its place. This gives an amortized cost of 1 + k + (1 - k) = 2 = O(1), for the amortized O(1) insert. As another way of thinking about this, once a node has been moved off the right spine, it's never part of the right spine again, and so we will never have to move it again. Since each of the n nodes can be moved at most once, this means that n insertions can do at most n moves, so the total runtime is at most O(n) for an amortized O(1) per element.

To do a dequeue step, we simply remove the leftmost node from the Cartesian tree. If this node is a leaf, we're done. Otherwise, the node can only have one child (the right child), and so we replace the node with its right child. Provided that we keep track of where the leftmost node is, this step takes O(1) time. However, after removing the leftmost node and replacing it with its right child, we might not know where the new leftmost node is. To fix this, we simply walk down the left spine of the tree starting at the new node we just moved to the leftmost child. I claim that this still runs in O(1) amortized time. To see this, I claim that a node is visited at most once during any one of these passes to find the leftmost node. To see this, note that once a node has been visited this way, the only way that we could ever need to look at it again would be if it were moved from a child of the leftmost node to the leftmost node. But all the nodes visited are parents of the leftmost node, so this can't happen. Consequently, each node is visited at most once during this process, and the pop runs in O(1).

We can do find-min in O(1) because the Cartesian tree gives us access to the smallest element of the tree for free; it's the root of the tree.

Finally, to see that the nodes come back in the same order in which they were inserted, note that a Cartesian tree always stores its elements so that an inorder traversal visits them in sorted order. Since we always remove the leftmost node at each step, and this is the first element of the inorder traversal, we always get the nodes back in the order in which they were inserted.

In short, we get O(1) amortized push and pop, and O(1) worst-case find-min.

If I can come up with a worst-case O(1) implementation, I'll definitely post it. This was a great problem; thanks for posting it!

Solution 3

Ok, here is one solution.

First we need some stuff which provide push_back(),push_front(),pop_back() and pop_front() in 0(1). It's easy to implement with array and 2 iterators. First iterator will point to front, second to back. Let's call such stuff deque.

Here is pseudo-code:

class MyQueue//Our data structure
{
    deque D;//We need 2 deque objects
    deque Min;

    push(element)//pushing element to MyQueue
    {
        D.push_back(element);
        while(Min.is_not_empty() and Min.back()>element)
             Min.pop_back();
        Min.push_back(element);
    }
    pop()//poping MyQueue
    {
         if(Min.front()==D.front() )
            Min.pop_front();
         D.pop_front();
    }

    min()
    {
         return Min.front();
    }
}

Explanation:

Example let's push numbers [12,5,10,7,11,19] and to our MyQueue

1)pushing 12

D [12]
Min[12]

2)pushing 5

D[12,5]
Min[5] //5>12 so 12 removed

3)pushing 10

D[12,5,10]
Min[5,10]

4)pushing 7

D[12,5,10,7]
Min[5,7]

6)pushing 11

D[12,5,10,7,11]
Min[5,7,11]

7)pushing 19

D[12,5,10,7,11,19]
Min[5,7,11,19]

Now let's call pop_front()

we got

 D[5,10,7,11,19]
 Min[5,7,11,19]

The minimum is 5

Let's call pop_front() again

Explanation: pop_front will remove 5 from D, but it will pop front element of Min too, because it equals to D's front element (5).

 D[10,7,11,19]
 Min[7,11,19]

And minimum is 7. :)

Solution 4

Use one deque (A) to store the elements and another deque (B) to store the minimums.

When x is enqueued, push_back it to A and keep pop_backing B until the back of B is smaller than x, then push_back x to B.

when dequeuing A, pop_front A as return value, and if it is equal to the front of B, pop_front B as well.

when getting the minimum of A, use the front of B as return value.

dequeue and getmin are obviously O(1). For the enqueue operation, consider the push_back of n elements. There are n push_back to A, n push_back to B and at most n pop_back of B because each element will either stay in B or being popped out once from B. Over all there are O(3n) operations and therefore the amortized cost is O(1) as well for enqueue.

Lastly the reason this algorithm works is that when you enqueue x to A, if there are elements in B that are larger than x, they will never be minimums now because x will stay in the queue A longer than any elements in B (a queue is FIFO). Therefore we need to pop out elements in B (from the back) that are larger than x before we push x into B.

from collections import deque


class MinQueue(deque):
    def __init__(self):
        deque.__init__(self)
        self.minq = deque()

    def push_rear(self, x):
        self.append(x)
        while len(self.minq) > 0 and self.minq[-1] > x:
            self.minq.pop()
        self.minq.append(x)

    def pop_front(self):
        x = self.popleft()
        if self.minq[0] == x:
            self.minq.popleft()
        return(x)

    def get_min(self):
        return(self.minq[0])

Solution 5

If you don't mind storing a bit of extra data, it should be trivial to store the minimum value. Push and pop can update the value if the new or removed element is the minimum, and returning the minimum value is as simple as getting the value of the variable.

This is assuming that get_min() does not change the data; if you would rather have something like pop_min() (i.e. remove the minimum element), you can simply store a pointer to the actual element and the element preceding it (if any), and update those accordingly with push_rear() and pop_front() as well.

Edit after comments:

Obviously this leads to O(n) push and pop in the case that the minimum changes on those operations, and so does not strictly satisfy the requirements.

Share:
28,998

Related videos on Youtube

bits
Author by

bits

Updated on July 05, 2022

Comments

  • bits
    bits almost 2 years

    I came across this question: Implement a queue in which push_rear(), pop_front() and get_min() are all constant time operations.

    I initially thought of using a min-heap data structure which has O(1) complexity for a get_min(). But push_rear() and pop_front() would be O(log(n)).

    Does anyone know what would be the best way to implement such a queue which has O(1) push(), pop() and min()?

    I googled about this, and wanted to point out this Algorithm Geeks thread. But it seems that none of the solutions follow constant time rule for all 3 methods: push(), pop() and min().

    Thanks for all the suggestions.

  • templatetypedef
    templatetypedef about 13 years
    Doesn't this give you an O(n) pop, since you have to scan all the elements to find the new min?
  • bits
    bits about 13 years
    I think get_min() doesn't actually pop the data. But pop_front() does pop the data. Lets say the front node is also the min node, so its popped. Now how can we maintain the min property in constant time?
  • Andy Mikula
    Andy Mikula about 13 years
    Ah, good call...though you're right, @bits, it's only O(n) in the case that you push a new minimum or pop your current minimum. If it has to be worst-case O(1), I don't know that it's possible, but I would love to see otherwise.
  • templatetypedef
    templatetypedef about 13 years
    How does using two stacks to implement the queue give you amortized O(1) pop?
  • adamax
    adamax about 13 years
    @template Each element can be moved from one stack to another only once.
  • Olhovsky
    Olhovsky about 13 years
    If you store the "current minimum together with the elements", and you pop the minimum from the queue, how would you know what the new minimum is, in O(1) time?
  • Olhovsky
    Olhovsky about 13 years
    I know, I'm asking: If you pop an item, and that item happens to be the minimum, how do you know what the new minimum is?
  • adamax
    adamax about 13 years
    @Kdoto: the minimum for the queue is the minimum of minimums for each stack. The minimum for the stack is the minimum stored at the top of the stack.
  • adamax
    adamax about 13 years
    @Kdoto: Sorry, my answer wasn't clear. I added a clarification.
  • Olhovsky
    Olhovsky about 13 years
    After thinking about this answer for a while, I think that this works. Nice. +1
  • Chris Hopman
    Chris Hopman about 13 years
    push() is O(1) (not amortized).
  • bits
    bits about 13 years
    @Matthieu: I see you stored current minimum as pairs. You explained very well about queue() and dequeue(). Can you explain how are we going to implement get_min()? Because I can't understand how your own example [(4,4), (2,2), (5,2), (1,1)] is going to work.
  • bits
    bits about 13 years
    Yup, (sorry for giving off the wrong credit). But to continue one the issue: Why would you pop from from rear? Its a queue, which means that we are going to pop from front (4,4) and then (2,2)... but after we pop (2,2), we are left with [(5,2),(1,1)]. So in other words, I still can't get how get_min() is implemented.
  • UmmaGumma
    UmmaGumma about 13 years
    @adamax please provide us with example. It is too difficult to understand what are you doing.
  • UmmaGumma
    UmmaGumma about 13 years
    @adamax I can't understand 3rd part. How your minimum is working. As you see there are too many comments here. Why just not provide a little example, with your algorithms steps. It will help understand your algorithm.
  • UmmaGumma
    UmmaGumma about 13 years
    @adamax OK, lets I pushed elements [1,5,2] to first stack and got [ (1,1), (5,1), (2,1)]. Now I want to pop element. I need to move all elements from first stack to second empty stack. After it I will have empty first stack and second stack with first stack old values but reversed in order( [(2,1),(5,1),(1,1)]). Now I'm poping second stack. After it I will have [(2,1),(5,1)] in second stack and empty first stack. Now, what is minimal element and how should I get it? So what I'm doing wrong?
  • adamax
    adamax about 13 years
    @Ashot No, you pop the element 2 from the first stack, push it to the second stack, it becomes [(2,2)]. Then you pop element 5, push it to the second stack, it becomes [(2,2), (5,2)]. Etc.
  • bits
    bits about 13 years
    @adamax: I still think we are not on the same page. Because I can't figure out from your solution how get_min() will work properly in constant time. As Ashot requested, can you please post an example of how get_min() will work? If you think its difficult to explain in comments, can you write your solution in pastebin.com and post the link here.
  • adamax
    adamax about 13 years
    @bits I've already given an example of how it works. I'm not going to write the code. Stop thinking about underlying structure of the stack for a bit. Think of it as an interface: pop(), push() and get_min(). Do you understand that if we have this interface, then we have the queue with get_min?
  • bits
    bits about 13 years
    @adamax: I am not requesting you to write code. I was just hoping that you would post some step by step example to support your solution. Showing the contents of your 2 stacks in each step and how get_min() would work after each step would be sufficient. Its not too much to ask. You think you have a solution, I just want to validate it and put it out clearly for StackOverflow visitors. Thanks for the help.
  • adamax
    adamax about 13 years
    @bits Ashot's example: [(1,1), (5,1), (2,1)] [] When we extract the element, we first pop elements one by one from the first stack and push to another one. It becomes: [] [(2,2), (5,2), (1,1)]. Then we pop: [] [(2,2), (5,2)]. The minimum is 2 at the top of the stack.
  • UmmaGumma
    UmmaGumma about 13 years
    @adamax I finally understand your solution. You should add to your explanation, that we should recalculate values of second elements, when we moving elements from first structure to second. By the way as I show in my answer It's possible to do all this operations in o(1) and not in amortized O(1). :)
  • Richard
    Richard about 10 years
    It's not good to post code without an accompanying, clearly-stated explanation of why the code is right.
  • TheMan
    TheMan almost 10 years
    That code is very self explanatory. If you want explanation, you could ask instead of down voting, please?
  • Ankit Roy
    Ankit Roy almost 7 years
    AFAICT, this is the same algorithm as that already given as source code and described by jianglai more than a month earlier.

Related