Data structures are a fundamental aspect of computer science and programming, irrespective of the language you use. Having a thorough knowledge of them can help you efficiently organize, manage, store, and modify data. Identifying the proper data structure for your use case can improve performance by a huge margin.

However, JavaScript only comes with primitive data structures such as arrays and objects by default. But with the introduction of ECMAScript 6 (ES6) classes, you can now create custom data structures such as stacks and queues with the help of primitive data structures.

Stack Data Structure

The stack data structure permits you to push new data on top of the existing data in a LIFO (last-in, first-out) manner. This linear data structure is easy to visualize using a simple example. Consider a stack of plates kept on a table. You can add or remove a plate from the top of the stack only.

Here's how you can implement the stack data structure using JavaScript arrays and ES6 classes:

        class Stack {
 constructor() {
 this.data = [];
 this.top = -1;
 }
}

Let's explore and build some of the operations that you can perform on a stack.

Push Operation

The push operation is used to insert new data into the stack. You need to pass the data as a parameter while calling the push method. Before inserting the data, the top pointer of the stack is incremented by one, and the new data is inserted at the top position.

        push(data) {
 this.top++;
 this.data[this.top] = data;
 return this.data;
}

Pop Operation

The pop operation is used to remove the uppermost data element of the stack. While performing this operation, the top pointer is reduced by 1.

        pop() {
 if (this.top < 0) return undefined;
 const poppedTop = this.data[this.top];
 this.top--;
 return poppedTop;
}

Peek Operation

The peek operation is used to return the value present at the top of the stack. The time complexity for retrieving this data is O(1).

Learn More: What Is Big-O Notation?

        peek() {
 return this.top >= 0 ? this.data[this.top] : undefined;
}

Linked List Data Structure

A linked list is a linear data structure consisting of numerous nodes connected to each other with the help of pointers. Each node in the list contains the data and a pointer variable that points to the next node in the list.

Learn More: An Introduction to Pointers for Programmers

Unlike a stack, linked list implementations in JavaScript require two classes. The first class is the Node class for creating a node, and the second class is the LinkedList class to perform all operations on the linked list. The head pointer points to the first node of the linked list, and the tail pointer points to the last node of the linked list.

        class Node {
 constructor(data, next = null) {
 this.data = data;
 this.next = next;
 }
}

class LinkedList {
 constructor() {
 this.head = null;
 this.tail = null;
 this.size = 0;
 }
}

Here are some primary operations that you can perform on a linked list:

Append Operation

The append operation is used to add a new node to the end of the linked list. You have to pass the data as a parameter for inserting a new node. Firstly, create a new node object using the new keyword in JavaScript.

If the linked list is empty, both the head and the tail pointer will point to the new node. Otherwise, only the tail pointer will point to the new node.

        append(data) {
 const newNode = new Node(data);
 if (!this.head) {
 this.head = newNode;
 this.tail = newNode;
 } else {
 this.tail.next = newNode;
 this.tail = newNode;
 }
 this.size++;
 return this;
}

Insert Operation

To insert a new node at a particular index, you can make use of the insert operation. This method takes two parameters: the data to insert and the index at which it is to be inserted. In the worst case, this method has a time complexity of O(N) as it may have to traverse through the entire list.

        insert(data, index) {
 if (index < 0 || index > this.size) return undefined;
 if (index === 0) {
 this.head = new Node(data, this.head);
 !this.tail ? (this.tail = this.head) : null;
 this.size++;
 return this;
 }
 if (index === this.size) return this.append(data);

 let count = 0;
 let beforeNode = this.head;
 while (count !== index) {
 beforeNode = beforeNode.next;
 count++;
 }

 const newNode = new Node(data);
 let afterNode = beforeNode.next;

 newNode.next = afterNode;
 beforeNode.next = newNode;
 this.size++;
 return this;
}

Delete Operation

The delete operation traverses through the linked list to get the reference to the node that is to be deleted and removes the link of the previous node. Similar to the insert operation, the delete operation also has a time complexity of O(N) in the worst case.

        deleteNode(index) {
 if (index === 0) {
 const removedHead = this.head;
 this.head = this.head.next;
 this.size--;
 this.size === 0 ? (this.tail = null) : null;
 return removedHead;
 }
 if (index === this.size - 1) {
 if (!this.head) return undefined;
 let currentNode = this.head;
 let newTail = currentNode;

 while (currentNode.next) {
 newTail = currentNode;
 currentNode = currentNode.next;
 }
 this.tail = newTail;
 this.tail.next = null;
 this.size--;
 this.size === 0 ? ([this.head, this.tail] = [null, null]) : null;
 return currentNode;
 }
 if (index < 0 || index > this.size - 1) return undefined;

 let count = 0;
 let beforeNode = this.head;
 while (count !== index - 1) {
 beforeNode = beforeNode.next;
 count++;
 }
 const removedNode = beforeNode.next;
 let afterNode = removedNode.next;

 beforeNode.next = afterNode;
 removedNode.next = null;
 this.size--;
 return removedNode;
}

Queue Data Structure

The queue data structure is similar to a bunch of people standing in a queue. The person who enters the queue first is served before others. Similarly, this linear data structure follows the FIFO (first in, first out) approach to insert and remove data. This data structure can be recreated in JavaScript using a linked list in this manner:

        class Queue {
 constructor() {
 this.front = null;
 this.rear = null;
 this.size = 0;
 }
}

Here's how you can insert and remove data from a queue in JavaScript:

Enqueue Operation

The enqueue operation inserts new data into the queue. While calling this method, if the queue data structure is empty, both the front and rear pointers point to the newly inserted node in the queue. If the queue is not empty, the new node is added to the end of the list and the rear pointer points to this node.

        enqueue(data) {
 const newNode = new Node(data);
 if (!this.front) {
 this.front = newNode;
 this.rear = newNode;
 } else {
 this.rear.next = newNode;
 this.rear = newNode;
 }
 this.size++;
 return this;
}

Dequeue Operation

The dequeue operation removes the first element in the queue. During the dequeue operation, the head pointer is moved ahead to the second node in the list. This second node now becomes the head of the queue.

        dequeue() {
 if (!this.front) return undefined;
 if (this.front === this.rear) this.rear = null;
 const dequeuedNode = this.front;
 this.front = this.front.next;
 this.size--;
 return dequeuedNode;
}

The Next Step After Data Structures

Data structures can be a tricky concept to grasp, especially if you're new to programming. But just like any other skill, practice can help you truly understand and appreciate the efficiency it provides for storing and managing data in your applications.

Algorithms are just as useful as data structures and could become the next logical step in your programming journey. So, why not start with a sorting algorithm such as bubble sort?