A queue follows the First-In-First-Out (FIFO) principle, whereas a stack follows the Last-In-First-Out (LIFO) principle. The challenge here is to use the properties of stacks to mimic the behavior of a queue.
Problem Understanding
The challenge in implementing a queue using stacks lies in maintaining the FIFO order while using the Last-In-Last-Out (LIFO) behavior of stacks. To achieve this, we can use two stacks: one for enqueuing elements and another for dequeueing elements.
Design a queue using two stacks.
- Implement the following operations:
enqueue(x): Add an elementxto the end of the queue.dequeue(): Remove and return the element from the front of the queue.peek(): Return the element at the front of the queue without removing it.empty(): Return whether the queue is empty.
Approaches
1️⃣ Using Two Stacks
Intuition:
The core idea is to use two stacks (stack1 and stack2) to simulate the behavior of a queue. The two stacks help us reverse the order of elements twice, which effectively preserves the FIFO order.
- Push Operation (
enqueue):- Always push the new element onto
stack1.
- Always push the new element onto
- Pop Operation (
dequeue):- If
stack2is empty, transfer all elements fromstack1tostack2(this reverses the order). - Pop the top element from
stack2.
- If
- Peek Operation (
peek):- Similar to the pop operation, but instead of popping the element, just return the top of
stack2.
- Similar to the pop operation, but instead of popping the element, just return the top of
- Empty Operation (
empty):- The queue is empty if both
stack1andstack2are empty.
- The queue is empty if both
Approach:
To implement the queue using two stacks, we maintain the following invariants:
stack1: Used to store the elements in the order they were enqueued.stack2: Used to reverse the order of elements fromstack1to maintain the FIFO order.
When an element is enqueued, it is pushed onto stack1. When an element is dequeued, if stack2 is empty, all elements from stack1 are popped and pushed onto stack2, and then the top element of stack2 is popped. This operation reverses the order of elements twice, mimicking the behavior of a queue.
Steps for Operations:
- Enqueue (
enqueue(x)):- Push
xontostack1.
- Push
- Dequeue (
dequeue()):- If
stack2is empty:- Transfer all elements from
stack1tostack2by popping fromstack1and pushing ontostack2.
- Transfer all elements from
- Pop the top element from
stack2.
- If
- Peek (
peek()):- If
stack2is empty:- Transfer all elements from
stack1tostack2.
- Transfer all elements from
- Return the top element of
stack2.
- If
- Empty (
empty()):- Return
trueif bothstack1andstack2are empty; otherwise, returnfalse.
- Return
Dry Run:
Let's consider an example to understand how these operations work:
Example:
Suppose we perform the following sequence of operations:
enqueue(1)enqueue(2)enqueue(3)dequeue()enqueue(4)dequeue()peek()
Initial State:
stack1 = []
stack2 = []enqueue(1):
stack1 = [1]
stack2 = []enqueue(2):
stack1 = [1, 2]
stack2 = []enqueue(3):
stack1 = [1, 2, 3]
stack2 = []dequeue():
stack1 = [1, 2, 3]
stack2 = []
Since stack2 is empty, so move elements from stack1 to stack2.
stack1 = []
stack2 = [3, 2, 1]
Now, pop from stack2: result is 1
stack1 = []
stact2 = [3, 2]enqueue(4):
stack1 = [4]
stack2 = [3, 2]dequeue():
stack1 = [4]
stack2 = [3, 2]
Since the stack2 is not empty, pop from stack2: result is 2.
stack1 = [4]
stack2 = [3]peek():
stack1 = [4]
stack2 = [3]
Since stack is not empty, top of stack2 is 3Solution in C++:
Let's implement a queue using two stacks in C++. We will use one stack (inStack) for enqueuing elements and another stack (outStack) for dequeueing elements.
#include <iostream>
#include <stack>
class MyQueue {
private:
std::stack<int> inStack; // Stack for enqueueing new elements
std::stack<int> outStack; // Stack used for dequeuing elements (reversed order)
public:
// Function to enqueue (add) an element to the back of the queue
void enqueue(int x) {
// Step 1: Simply push the element into inStack
inStack.push(x);
}
// Function to dequeue (remove) an element from the front of the queue
int dequeue() {
// Step 1: If outStack is empty, we need to move elements from inStack
if (outStack.empty()) {
// Step 1.1: If inStack is also empty, the queue is empty
if (inStack.empty()) {
return -1; // Indicating underflow or empty queue
}
// Step 1.2: Transfer all elements from inStack to outStack
while (!inStack.empty()) {
outStack.push(inStack.top()); // Reverse the order
inStack.pop(); // Remove from inStack
}
}
// Step 2: Now outStack has the oldest elements at the top (FIFO)
int front = outStack.top(); // Get the front element of the queue
outStack.pop(); // Remove the front element
return front; // Return the dequeued value
}
// Function to get the front element of the queue without removing it
int peek() {
// Step 1: If outStack is empty, transfer elements from inStack
if (outStack.empty()) {
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
// Step 2: Return the front element (top of outStack)
return outStack.top();
}
// Function to check if the queue is empty
bool empty() {
// Queue is empty only when both stacks are empty
return inStack.empty() && outStack.empty();
}
};
int main() {
MyQueue q;
// Enqueue operations
q.enqueue(1); // Queue: [1]
q.enqueue(2); // Queue: [1, 2]
q.enqueue(3); // Queue: [1, 2, 3]
// Dequeue operations
std::cout << q.dequeue() << std::endl; // Output: 1 (front of queue)
std::cout << q.dequeue() << std::endl; // Output: 2
std::cout << q.dequeue() << std::endl; // Output: 3
return 0;
}
/* Output:
1
2
3
*/
Complexity Analysis:
- Time Complexity:
- Enqueue Operation:
- Time Complexity:
O(1)
- Time Complexity:
- Dequeue Operation:
- Amortized Time Complexity:
O(1) - Worst Case Time Complexity:
O(n)(whenoutStackis empty and all elements need to be transferred frominStacktooutStack)
- Amortized Time Complexity:
- Enqueue Operation:
- Space Complexity:
O(n): The space used by the two stack is proportional to the number of elementsnin the queue.
2️⃣ Other Approach
#include <bits/stdc++.h>
using namespace std;
// Queue implementation using two stacks
class StackQueue {
private:
stack<int> st1, st2; // Two stacks used to simulate a queue
public:
// Constructor to initialize an empty StackQueue
StackQueue() {
// Nothing to initialize explicitly
}
// Method to push an element to the back of the queue
void push(int x) {
// Step 1: Move all elements from st1 to st2
while (!st1.empty()) {
st2.push(st1.top()); // Move top of st1 to st2
st1.pop(); // Remove from st1
}
// Step 2: Push the new element to st1 (now it's empty)
st1.push(x); // This becomes the bottom-most element of the queue
// Step 3: Move everything back from st2 to st1
while (!st2.empty()) {
st1.push(st2.top()); // Restore original order
st2.pop();
}
}
// Method to remove and return the front element of the queue
int pop() {
// Step 1: Check for empty queue
if (st1.empty()) {
cout << "Stack is empty";
return -1; // Return -1 if queue is empty
}
// Step 2: Front of queue is at the top of st1
int topElement = st1.top(); // Get the front
st1.pop(); // Remove the front element
// Step 3: Return popped element
return topElement;
}
// Method to get the front element of the queue without removing it
int peek() {
// Step 1: Check for empty queue
if (st1.empty()) {
cout << "Stack is empty";
return -1;
}
// Step 2: Return the top element (which is front of queue)
return st1.top();
}
// Method to check if the queue is empty
bool isEmpty() {
return st1.empty(); // If st1 is empty, the queue is empty
}
};
int main() {
StackQueue q;
// Simulated list of commands and their inputs
vector<string> commands = {"StackQueue", "push", "push", "pop", "peek", "isEmpty"};
vector<vector<int>> inputs = {{}, {4}, {8}, {}, {}, {}};
// Simulate each command one by one
for (int i = 0; i < commands.size(); ++i) {
if (commands[i] == "push") {
q.push(inputs[i][0]); // Push the provided input
cout << "null "; // Output 'null' to match typical API return
} else if (commands[i] == "pop") {
cout << q.pop() << " "; // Output popped value
} else if (commands[i] == "peek") {
cout << q.peek() << " "; // Output front value
} else if (commands[i] == "isEmpty") {
cout << (q.isEmpty() ? "true" : "false") << " "; // Output empty status
} else if (commands[i] == "StackQueue") {
cout << "null "; // Constructor call simulated as 'null'
}
}
return 0;
}
Complexity Analysis:
- Time Complexity:
O(N)where N is the number of elements. - Space Complexity:
O(2N)because, in the worst case, both the input and output stacks can each hold up to N elements, totaling 2N elements. Here N is the size of the stack.
