Implement a Queue Using Two Stacks
This challenge involves creating a queue data structure using only the fundamental operations of two stacks. Queues follow a First-In, First-Out (FIFO) principle, while stacks operate on a Last-In, First-Out (LIFO) basis. Successfully implementing a queue with two stacks demonstrates a deep understanding of these abstract data types and how to manipulate them.
Problem Description
You are tasked with implementing a Queue class in JavaScript. This class should mimic the behavior of a standard queue, supporting the following operations:
enqueue(value): Adds an element to the back of the queue.dequeue(): Removes and returns the element from the front of the queue.peek(): Returns the element at the front of the queue without removing it.isEmpty(): Returnstrueif the queue is empty,falseotherwise.
The critical constraint is that you must implement these operations using only two stacks. You cannot use any built-in queue implementations or other array methods that directly simulate queue behavior (like shift()). You can use basic array operations like push() and pop() to simulate stack behavior.
Key Requirements:
- The
Queueclass should be initialized with two internal arrays, each representing a stack. - The
enqueueoperation should efficiently add elements to the conceptual "back" of the queue. - The
dequeueoperation should efficiently remove and return elements from the conceptual "front" of the queue. - The
peekoperation should return the front element without modifying the queue. - The
isEmptyoperation should accurately report the queue's state.
Expected Behavior:
When elements are enqueued and then dequeued, they should appear in the same order they were added (FIFO).
Edge Cases:
- Dequeuing from an empty queue should return
undefinedornull. - Peeking at an empty queue should return
undefinedornull. - Handling multiple enqueues followed by multiple dequeues.
- Alternating enqueues and dequeues.
Examples
Example 1:
Input:
const queue = new Queue();
queue.enqueue(10);
queue.enqueue(20);
console.log(queue.dequeue()); // Output: 10
queue.enqueue(30);
console.log(queue.dequeue()); // Output: 20
console.log(queue.peek()); // Output: 30
console.log(queue.dequeue()); // Output: 30
console.log(queue.isEmpty()); // Output: true
Explanation:
enqueue(10): Queue state conceptually: [10]enqueue(20): Queue state conceptually: [10, 20]dequeue(): Removes and returns 10. Queue state conceptually: [20]enqueue(30): Queue state conceptually: [20, 30]dequeue(): Removes and returns 20. Queue state conceptually: [30]peek(): Returns 30 without removing. Queue state conceptually: [30]dequeue(): Removes and returns 30. Queue state conceptually: []isEmpty(): Returns true as the queue is empty.
Example 2:
Input:
const queue = new Queue();
console.log(queue.isEmpty()); // Output: true
console.log(queue.dequeue()); // Output: undefined
console.log(queue.peek()); // Output: undefined
Explanation:
The queue is initialized empty, so isEmpty returns true, and dequeue and peek on an empty queue return undefined.
Constraints
- You must use exactly two arrays to represent the internal stacks.
- The
enqueue,dequeue,peek, andisEmptyoperations should have an average time complexity of O(1). Some operations might involve transferring elements between stacks, leading to amortized O(1) performance fordequeueandpeek. - Do not use any external libraries or built-in queue data structures.
Notes
Consider how you can use one stack to store incoming elements and the other to prepare elements for dequeueing. The core idea is to reverse the order of elements when necessary. Think about when you need to transfer elements from one stack to the other to maintain the FIFO order.
Think about the efficiency of your dequeue and peek operations. If the stack that holds the front elements is empty, you'll need to transfer elements from the other stack. This transfer operation is key to achieving the amortized O(1) complexity.