[Approach 1] Making Enqueue Operation Costly - Enqueue in O(n) and Dequeue() in O(1)
A queue can be implemented using two stacks. Let the queue be represented as q, and the stacks used for its implementation be s1 and s2.
In this approach, the enqueue operation is made costly by transferring elements from s1 to s2 before adding the new element. This ensures that the elements in s2 are in the correct order for dequeuing. The dequeue operation remains efficient, as it simply involves popping elements from s2.
Enqueue(x):
- While s1 is not empty, move all elements from s1 to s2.
- Push x into s1.
- Move everything back from s2 to s1.
- This guarantees that the front of the queue is always on top of s1.
Dequeue():
- If s1 is empty → queue is empty (underflow).
- Otherwise, pop from s1.
Front():
- If s1 is empty → return -1.
- Otherwise, return the top element of s1 (since it always represents the front of the queue).
Size():
- Return the current size of s1 (both stacks together always hold exactly n elements).
OutputFront: 1
Size: 3
Front: 2
Size: 2
Time Complexity:
- enqueue(x) : O(n) - Move all elements from s1 to s2, push the new element, then move back. Linear in the number of elements n.
- dequeue() : O(1) - Just pop the top element from s1.
- front() : O(1) - Return the top element of s1 without removing it.
- size() : O(1) - Return the number of elements in s1.in the number of elements n.
Space Complexity: O(n) - All elements are stored in the two stacks s1 and s2. No extra space beyond that is needed.
[Approach 2] Making Dequeue Operation Costly - Enqueue in O(1) and Dequeue() in O(n)
In this approach, the new element is pushed onto the top of s1 during the enqueue operation. For the dequeue operation, if s2 is not empty then top of s2 needs to be returned. Otherwise all elements are transferred from s1 to s2, and the element at the top of s2 is returned.
enqueue(q, x)
dequeue(q)
- If both s1 and s2 are empty → queue underflow.
- If s2 is empty while s1 is not empty, pop elements from s1 and push them to s2.
- Pop the element from s2 and return it.
front()
- If s2 is not empty → top of s2 is the front.
- Else, if s1 is not empty → bottom of s1 is the front (can be accessed indirectly via transfer).
- If both empty → queue is empty.
size()
- Total size = s1.size() + s2.size()
OutputFront: 1
Size: 3
Front: 2
Size: 2
Time Complexity:
- enqueue(x) : O(1) - Simply push onto s1.
- dequeue() : Amortized O(1), Worst-case O(n)
=> Worst-case: All elements from s1 are moved to s2 O(n).
=> Amortized: Each element is moved at most once from s1 to s2 O(1) per operation. - front() : Amortized O(1), Worst-case O(n)
=> Similar to dequeue: may need to move elements from s1 to s2. - size() : O(1) Just return the sum of lengths of s1 and s2.
Space Complexity: O(n)- s1 and s2 together hold at most n elements, where n is the number of elements in the queue.
[Approach 3] Queue Implementation Using One Stack and Recursion
A queue can be implemented using one stack and recursion. The recursion uses the call stack to temporarily hold elements while accessing the bottom element of the stack, which represents the front of the queue.
enqueue(x) :
- Push new elements on top of the stack.
- The stack top represents the rear of the queue.
dequeue() :
- Recursively reach the bottom element (which is the front of the queue).
- Remove it and return it.
- Push back all other elements in the same order.
Front() :
- Recursively reach the bottom element but do not remove it.
- Push back all elements.
Size():
- Returns the current size.
OutputFront: 1
Size: 3
Front: 2
Size: 2
Time Complexity
- enqueue(x): O(1) - Just a single s.push(x).
- dequeue(): O(n) - In the worst case, it pops all n elements recursively until it reaches the bottom, then pushes them back while unwinding recursion.
- front(): O(n) - Similar to dequeue(), it traverses all elements recursively to reach the bottom and then restores them.
- size(): O(1) - Direct call to s.size().
Auxiliary Space: O(n) due to the recursion call stack holding up to n elements temporarily.