Step-by-Step Guide to Implementing Stack and Queue in JavaScript

Step-by-Step Guide to Implementing Stack and Queue in JavaScript

Introduction

Data structures are techniques for storing data for efficient data access and manipulation in a system. This is a build-up to the previous elaborate article on implementing a linked list in JavaScript.

Stacks and queues are abstract data structures constructed with other fundamental data structures, such as arrays or linked lists. Usually by constraining the way elements are added, removed, or accessed to create specialized behaviours through limiting the functions of the enabling data structure.

Stack

A stack is a linear data structure that operates on a Last-In-First-Out principle. This means the most recently added elements are processed first. To visualize a stack, a classic example is a stack of plates: the last plate placed on the stack is the first to be removed, while the plate at the bottom of the stack remains the last to be taken out.

Another instance is the “call stack” in programming. During function execution, each function call is added or pushed onto the stack of functions to be executed. If there are nested functions or recursive calls, these are also pushed onto the stack in the order they are encountered. The functions are then executed in reverse order, starting from the top of the stack, with each completed function being removed (or "popped") from the stack until the program completes execution. This way, elements are removed and inserted from only one side of the data structure, which is also known as the top.

A stack data structure is useful for redoing/undoing functionalities in an application.

Primary Operations on a Stack:

  1. Push - adds an element to the top of the stack → Big 0(1)

  2. Pop - removes an element from the top of the stack and returns it → Big 0(1)

  3. Peek - gets the top element from the stack → Big 0(1)

  4. Size - returns the size of the stack → Big 0(1)

  5. isEmpty - checks if the stack is empty → Big 0(1)

Implementing the Operations of a Stack with a Doubly Linked List:

// Implementing the operations of a stack with a doubly linked list:
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.previous = null;
  }
}

class Stack {
  constructor() {
    this.head = null; // pointer to the first node added (bottom of the stack)
    this.tail = null; // pointer to the last node (top of the stack)
    this.size = 0; // used to track the size of the stack
  }

  // ISEMPTY: checks if the stack is empty or otherwise
  isEmpty() {
    return this.size === 0;
  }

  // PUSH: adds a new node to the top of the stack
  push(value) {
    const newNode = new Node(value);
    if (this.isEmpty()) {
      // if the stack is empty, both head and tail point to the new node
      this.head = newNode;
      this.tail = this.head;
    } else {
        // if not, the new node becomes the tail, and the previous and next
        // pointers are updated accordingly
      newNode.previous = this.tail;
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.size++;
  }

    // POP: removes the node at the top of the stack,
    // i.e the last node added
  pop() {
    if (this.isEmpty()) {
      return "An empty stack";
    }

    let removedNode;
    if (this.size === 1) {
        // if the stack has one node, both head and tail are reset to null
      removedNode = this.head;
      this.tail = this.head = null;
    } else {
        // otherwise, tail moves to the previous node, and the old tail's references are cleaned up
      removedNode = this.tail; // save a copy of the node to remove
      this.tail = this.tail.previous;
      this.tail.next = null;
    }
    this.size--;
    return removedNode;
  }

    // PEEK: returns the last node added to the stack,
    // i.e the node at the top of the stack without removing it
    peek() {
     return this.tail;
    }
}

// TEST
const newStack = new Stack();
newStack.push(40);
newStack.push(20);
newStack.push(30);
newStack.push(60);

console.log(newStack);
// Returns:
// head: Node { value: 40, next: Node, previous: null }
// size: 4
// tail: Node { value: 60, next: null, previous: Node }

newStack.pop();

console.log("newStack.peek()", newStack.peek());
// Returns:
// next: null
// previous: Node {value: 20, next: Node, previous: Node}
// value: 30

console.log(newStack);
// Returns:
// head: Node {value: 40, next: Node, previous: null}
// size: 3
// tail: Node {value: 30, next: null, previous: Node}

Implementing the Operations of a Stack with an Array:

class Stack {
  constructor() {
    this.stack = [];
  }

  push(value) {
    this.stack.push(value);
    return this.stack;
  }

  pop() {
    return this.stack.pop();
  }

  peek() {
    return this.stack[this.stack.length - 1];
  }

  isEmpty() {
    return this.stack.length < 1;
  }

  getSize() {
    return this.stack.length;
  }
}

const stack = new Stack();

// PUSH
stack.push(40);
stack.push(20);
stack.push(30);
stack.push(60);

// POP
console.log("stack.pop: ", stack.pop());

// PEEK
console.log("stack.peek: ", stack.peek());

// ISEMPTY
console.log("stack.isEmpty: ", stack.isEmpty());

// SIZE
console.log("stack.getSize: ", stack.getSize());


console.log(stack)

Queue

In contrast to a stack, a queue operates on a First-In-First-Out principle. Here, the first item to be added to the data structure is the first to be removed and processed. An example is a queue of people, where the first person on the line is the first to be attended to. Another example is a printing queue. Multiple print jobs sent to a printer are processed in the order they were sent and received, hence first-in-first-out. Here, an item is inserted from the rear, but removed/accessed from the front.

Queues are useful in scheduling systems, where tasks are processed in the order they arrive.

There are three types of a queue: circular queue, priority queue, and double-ended queue.

  • Circular Queue: A queue where the last element is connected to the first, making it circular.

  • Priority Queue: A queue where each element has a priority. Elements with higher priority are dequeued first.

  • Deque (Double-Ended Queue): Allows insertion and removal of elements from both ends (front and rear).

Primary Operations on a Queue:

  1. Enqueue - inserting an item to the rear of the queue → Big 0(1)

  2. Dequeue - removing an item from the front of the queue → Big 0(1)

  3. Peek - returns the first front element, without removing it from the queue → Big 0(1)

  4. isEmpty - checks if the queue is empty or otherwise → Big 0(1)

  5. size - tracks the length of the queue → Big 0(1)

Implementing Queue with a Linked List:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.previous = null;
  }
}

class Queue {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  isEmpty() {
      // returns a boolean indicating whether the queue is empty (i.e., size === 0).
    return this.size === 0;
  }

  enqueue(value) {
      // Adds a new node to the end (tail) of the queue.
    const newNode = new Node(value);
    if (this.isEmpty()) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.size++; // increases the size after adding an element
  }

  dequeue() {
      // removes the node from the front (head) of the queue
    if (this.isEmpty()) {
      return "Empty queue";
    }
    let dequeuedNode;

    if (this.size === 1) {
      dequeuedNode = this.head;
      this.head = null;
      this.tail = null;
    } else {
      dequeuedNode = this.head;
      this.head = this.head.next;
    }

    this.size--;
    return dequeuedNode.value; // returns the value of the dequeued node
  }

  peek() {
      // returns the value of the node at the front of the queue (head)
        // if the queue is empty, it returns null.
    return this.head ? this.head.value : null;
  }
}

const queue = new Queue();

queue.enqueue(45); // Adds 45 to the queue
queue.enqueue(30); // Adds 30 to the queue
queue.enqueue(45); // Adds 45 to the queue
console.log(queue);

queue.dequeue(); // Removes the first element

console.log(queue.peek());
// Returns 30, the new front element after running queue.dequeue()

console.log(queue.isEmpty()); // Returns false, as there are still items in the queue

console.log(queue.size);  // Returns 2, as there are two elements left

Implementing Queue with an Array:

class Queue{
  constructor(){
    this.queue =[]
  }

  enqueue(value){
     // adds a value to the end of the queue by using the push() method.
     // This operation returns the updated queue.
   this.queue.push(value)
    return this.queue
  }

  dequeue(value){
   // removes and returns the first value from the queue using the shift() method,
   // which removes the first element
   return this.queue.shift()
  }

  peek(){
      // returns the value of the front of the queue without removing it
      // (i.e., the first item in the array).
    return this.queue[0]
  }

  isEmpty(){
    return this.queue.length <1
  }
  getSize(){
    return this.queue.length
  }
  }

const queueArr = new Queue()
console.log(queueArr.enqueue(32))
console.log(queueArr.enqueue(442))
console.log(queueArr.enqueue(100))
console.log(queueArr.enqueue(99))

console.log(queueArr.dequeue())

console.log(queueArr)
console.log(queueArr.peek())

console.log(queueArr.isEmpty())
console.log(queueArr.getSize())

Conclusion

In this article, we explored stacks and queues, their practical applications, and implementations using two fundamental data structures: array and linked list.

As abstract data structures, built on other foundational data structures, they allow efficient data management by specifying how elements are added, removed, and/or accessed.

A stack allows us to process the most recent data, which is useful in cases like undoing or redoing a recent action. On the flip, a queue processes the first element added first, and this is useful in cases where the order of arrival is important, such as job scheduling systems, thereby ensuring the oldest tasks are executed first.