How do you implement a data structure like a stack or queue in JavaScript?
Stacks and queues are fundamental linear data structures that store data sequentially. A stack operates on a Last-In, First-Out (LIFO) principle, while a queue operates on a First-In, First-Out (FIFO) principle. JavaScript's built-in `Array` object can be effectively used to implement both, providing convenient methods like `push()`, `pop()`, and `shift()`.
Implementing a Stack (LIFO)
A stack is a linear data structure that follows the LIFO (Last-In, First-Out) principle. This means the last element added to the stack is the first one to be removed. Think of a stack of plates: you add new plates to the top, and you remove plates from the top.
class Stack {
constructor() {
this.items = [];
}
// Add an element to the top of the stack
push(element) {
this.items.push(element);
}
// Remove and return the top element from the stack
pop() {
if (this.isEmpty()) {
return "Underflow"; // Or throw an error
}
return this.items.pop();
}
// Return the top element of the stack without removing it
peek() {
if (this.isEmpty()) {
return null; // Or undefined
}
return this.items[this.items.length - 1];
}
// Check if the stack is empty
isEmpty() {
return this.items.length === 0;
}
// Return the number of elements in the stack
size() {
return this.items.length;
}
// Clear the stack
clear() {
this.items = [];
}
// Print the stack elements
printStack() {
return this.items.toString();
}
}
In this implementation, push() adds an element to the end of the array (conceptually the top of the stack), and pop() removes an element from the end. peek() allows inspecting the top element without removal. isEmpty() and size() provide utility for checking the stack's state.
const myStack = new Stack();
console.log(myStack.isEmpty()); // true
myStack.push(10);
myStack.push(20);
myStack.push(30);
console.log(myStack.printStack()); // 10,20,30
console.log(myStack.peek()); // 30
console.log(myStack.size()); // 3
console.log(myStack.pop()); // 30
console.log(myStack.printStack()); // 10,20
console.log(myStack.isEmpty()); // false
Implementing a Queue (FIFO)
A queue is a linear data structure that follows the FIFO (First-In, First-Out) principle. This means the first element added to the queue is the first one to be removed. Think of a line at a supermarket: the first person in line is the first person to be served.
class Queue {
constructor() {
this.items = [];
}
// Add an element to the end of the queue
enqueue(element) {
this.items.push(element);
}
// Remove and return the front element from the queue
dequeue() {
if (this.isEmpty()) {
return "Underflow"; // Or throw an error
}
return this.items.shift(); // shift() removes from the beginning
}
// Return the front element of the queue without removing it
peek() {
if (this.isEmpty()) {
return null;
}
return this.items[0];
}
// Check if the queue is empty
isEmpty() {
return this.items.length === 0;
}
// Return the number of elements in the queue
size() {
return this.items.length;
}
// Clear the queue
clear() {
this.items = [];
}
// Print the queue elements
printQueue() {
return this.items.toString();
}
}
For a queue, enqueue() adds an element to the end of the array. The crucial part is dequeue(), which uses shift() to remove the element from the beginning of the array, ensuring the FIFO order. peek() returns the element at the front of the queue without removing it.
const myQueue = new Queue();
console.log(myQueue.isEmpty()); // true
myQueue.enqueue('Alice');
myQueue.enqueue('Bob');
myQueue.enqueue('Charlie');
console.log(myQueue.printQueue()); // Alice,Bob,Charlie
console.log(myQueue.peek()); // Alice
console.log(myQueue.size()); // 3
console.log(myQueue.dequeue()); // Alice
console.log(myQueue.printQueue()); // Bob,Charlie
console.log(myQueue.isEmpty()); // false
Performance Considerations and Alternatives
While JavaScript arrays are convenient for implementing both stacks and queues, it's worth noting that Array.prototype.shift() has a time complexity of O(n) because it re-indexes all subsequent elements after removing the first. For high-performance queue operations with a very large number of elements, a linked list-based implementation or a ring buffer might be more efficient, offering O(1) for both enqueue and dequeue. However, for most typical applications, the Array-based approach is simple, easy to understand, and performs well enough.