Implementing A Linked List In Javascript

Implementing A Linked List In Javascript

Introduction

A linked list is a foundational data structure that can be used to implement other data structures like a queue and a stack. All three data structures are linear, meaning their elements are arranged sequentially, with a next and/or previous element attached to them.

Key Features of a Linked List:

  1. Node Structure: A linked list is a series of connected nodes, where each node contains a:

    • Value: the data or information stored in the node; and a

    • Pointer: a reference to the next node in the sequence

  2. Memory Layout: A linked list stores elements in incontiguous memory spaces, using pointers to maintain the sequence.

While an array stores its elements in a contiguous (non-breaking) memory layout, a linked list instead stores its elements in an incontiguous memory space, that is, elements are scattered in memory, while maintaining a reference to each element with additional memory spaces.

In a contiguous memory space, such as an array, elements are stored sequentially, making it possible to access them directly by index. But with an incontiguous data structure, elements are not located next to one another, they can be placed anywhere in the memory where there is space.

Building a linked list prioritizes speed for insertion and deletion at the cost of additional memory for pointers. In contrast, an array optimizes memory usage but incurs higher costs for these operations.

The mode of memory allocation impacts the time complexity for different operations. A linked list is more efficient for insertion/deletion, which is costly in an array. However, for search operations, an array is more efficient if the index is known. Linked lists are dynamic, flexible structures that excel where frequent insertions and deletions are needed. Arrays are more memory-efficient but better suited for scenarios requiring fast random access.

Now that we understand the concept and advantages of linked lists, let's move on to implementing one step by step. We'll start by creating a node, which is the building block of a linked list. Then, we’ll implement the linked list that will contain the nodes, along with methods to update it – adding, removing or searching for a node in it.

Follow the next steps sequentially to implement a linked list:

STEP 1: Create a Node Class

Creating a class from which we can instantiate each node

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

You can create an instance of the Node class like this:

const node_instance = new Node(54)

If you print out the node_instance with console.log(node_instance), you will get:

Node {value: 54, next: null}

STEP 2: Create a Linked List Class

Since we have the building block for our linked list, which is the node class, the next step is to create the linked list that will contain all of the nodes.

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    // returns true or false if the list is empty, which is when the size of the list is 0  
    }
}

In a linked list, the first node is called the head, and it is an important element for different operations. The size property, together with its methods, getSize and isEmpty, is to manually calculate how many nodes are in the list when we add or delete a node. So in every new instance of the list, the head and size properties are initialized, although empty.

Create a new instance of the node list class like this:

const linkedListInstance = new LinkedList

You can print it out with console.log(linkedList) to see what it currently looks like:

LinkedList {head: null, size: 0}

We can check for the size of the node and whether or not it is empty by running the following command:

console.log(linkedListInstance.isEmpty())
console.log(linkedListInstance.getSize())

Now we can start implementing basic operations on our list. We will implement a prepend, append, delete, search, insert, methods on the LinkedList class.

STEP 3: A Prepend Method For Adding To A LinkedList

Understanding the concept and nature of a linked list as a data structure is pivotal to thinking through the necessary operations that can be performed on a linked list.

Prepending has a time complexity of O(1).

//PREPENDING A NODE TO A LINKED LIST**
        //To prepend a node, we would need to:
    prepend(value){
        // 1. get a node instance of the value to be added
        const node = new Node(value)

            // 2. add it to the list. Remember we need to maintain a pointer to the head, which is the first node. In prepending, we are adding the node to the beginning of the list, so we would need to keep updating the head whenever we prepend a node. We also need to consider whether or not the list is empty. If it is empty, we only need to update the head node, and increase the size. If not, we need to update the next property of the new node such that it points to the previous node, which is the current head, then update the head to point to the newly added node.

        if(this.isEmpty()){
            this.head = node
        }else{
            //update the current's node next, such that it points to the head in an existing linked list
            node.next = this.head
            //then update the pointer to the head to point to the new node, such that the latest node becomes the head node
            this.head = node
            //the order of this operation is very important given that the head is the only entry point or reference we have to the list so if we update the head without having a reference to its content, we would lose the entire list.
        }

        // 3. increase the size count of the linked list in any case of an empty or otherwise list
        this.size++
    }

At this point, our code looks like this, without the comments:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }
}

To visualize our linked list values cleanly, we will implement a print() function so we can see the values of each node.

Print Method:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }
    **//PRINTING A NODE TO A LINKED LIST**
        //To view the values of the list, we would have to iterate over each node to print out its value. 
    print() {
        //An early return to guard against an empty list
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        //Where the list is not empty
        let current = this.head // getting a reference to the head node from which we can reach other nodes exisitng in the .next property.

        // We're checking to see that the current's item next is not null, where it is null, it means that current item is the last item on the list.

        while (current !== null) {
        //if the current item is not null, we print it's value, and update the current item to the next node for the iteration to continue, or stop where the list is empty
            console.log(current.value)
            current = current.next
        }
    }
}

Now we can check to see that our prepend and print methods are working as expected:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    **//PREPENDING A NODE TO A LINKED LIST**
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    **//PREPENDING A NODE TO A LINKED LIST**
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.prepend(40)

linkedListInstance.prepend(50)

linkedListInstance.prepend(30)

console.log(linkedListInstance.print())
// Returns 
// 30
// 50
// 40

STEP 4: Append Method For Adding To The End of A LinkedList

With prepending, we add to the beginning of the list, but in appending, we add to the end of the list. As you can imagine, we need to traverse the entire list until we get to the end, which is when the node is pointing to null, and then we add the node.

Appending has a time complexity of O(n) in a singly linked list, but O(1) in a doubly linked list.

Like we did with the head pointer, we would have updated a tail pointer if we maintained a pointer to the tail node, which is the last node. More on the tail node below.

// APPENDING A NODE TO A LINKED LIST**
        //To append a node, we would need to:
append(value) {
    // 1. create a node instance with the given value
    const node = new Node(value)
    // 2. get a reference to the list's head
        let current = this.head

    // 3. Add the node to the list. But to do this, we need to consider the edge case of an empty list or otherwise:

        if (this.isEmpty()) {
        // 3a. where the list is empty, update the head with the node
            this.head = node
        } else {
        // 3.b otherwise, loop through until you get to the last item
            while (current.next !== null) {
            //keep updating the current with it's next, where the next is not null
                current = current.next
            }
            // where the next node becomes null, update the last node's next from null to the new node
            current.next = node // this will run when the code exits the while block, meaning it has now hit a null value, then we update the current item's next from null to the new node.
        }
        this.size++ //increase the size of the list where a new node was added to an empty or otherwise list
}

Now we can check to see if our append method is working as expected:


//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
        } else {
            while (current.next !== null) {
                current = current.next
            }
            current.next = node
        }
        this.size++
    }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.append(40)

linkedListInstance.append(50)

linkedListInstance.append(30)

console.log(linkedListInstance.print())
// Returns 
// 40
// 50
// 30

STEP 5: Remove Method For Removing A Node From A LinkedList

We can remove a node from a linked list either by it’s value, that is checking each node in the list until the first node containing a value that matches the list, or by its index, that is imitating the indexing system of an array to index each node.

Removing has a time complexity of O(n).

Here is a brief illustration of what removing a node looks like in a linked list:

We will implement both methods of removing a node from a linked list:

  1. Removing by index
// Delete Method For Removing A Node From A LinkedList By Value
        //To delete a node, we would need to:
    removeByValue(value) {
        // 1. check against the list is empty
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        // 2. Specifically check if the value to be removed matches the value in the head node so we can handle the head pointer
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
            // the order of the operation really matters. Can you think through the possible error where the steps are swapped?
        } else {
            let current = this.head; // use this to begin interation
            let previous = null; // this will be the node before the particular node we want to remove

            while (value !== current.value) {
                // while value does not yet match any node, we keep advancing through the list
                previous = current
                current = current.next
                if (current === null) { // this 'if' statement is necessary where we have gone over the list wihout finding the value, and the current is now pointing at null
                    return 'Value not found in the list' 
                }
            }
            // this lines below will run when the while loop is exited, that is when the value === current.value
            removedNode = current // use this to save a reference to the node to be removed
            previous.next = removedNode.next // use this to update the pointer of the node before the node to be removed from pointing to the node to be removed to point to the next node after the node to be removed.
        }
        this.size-- // to keep our linked list's size in sync 
        return removedNode
    }

Removing by value

// Delete Method For Removing A Node From A LinkedList By Value**
        //To delete a node, we would need to:
    removeByValue(value) {
        // 1. check against the list is empty
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        // 2. Specifically check if the value to be removed matches the value in the head node so we can handle the head pointer
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
            // the order of the operation really matters. Can you think through the possible error where the steps are swapped?
        } else {
            let current = this.head; // use this to begin interation
            let previous = null; // this will be the node before the particular node we want to remove

            while (value !== current.value) {
                // while value does not yet match any node, we keep advancing through the list
                previous = current
                current = current.next
                if (current === null) { // this 'if' statement is necessary where we have gone over the list wihout finding the value, and the current is now pointing at null
                    return 'Value not found in the list' 
                }
            }
            // this lines below will run when the while loop is exited, that is when the value === current.value
            removedNode = current // use this to save a reference to the node to be removed
            previous.next = removedNode.next // use this to update the pointer of the node before the node to be removed from pointing to the node to be removed to point to the next node after the node to be removed.
        }
        this.size-- // to keep our linked list's size in sync 
        return removedNode
    }

Now we can check to see if our delete methods are working as expected:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
        } else {
            while (current.next !== null) {
                current = current.next
            }
            current.next = node
        }
        this.size++
    }

    **// Delete Method For Removing A Node From A LinkedList by Index**
    removeByIndex(index) {
        if (index < 0 || index >= this.size) {
            return 'Invalid index provided'
        }
        let removedNode;
        if (index === 0) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let counter = 0;
            let current = this.head;
            let previous = null;

            while (counter < index) {
                previous = current
                current = current.next
                counter++
            }
            removedNode = current
            previous.next = removedNode.next
        }
        this.size--
        return removedNode
    }

    **// Delete Method For Removing A Node From A LinkedList By Valu**
    removeByValue(value) {
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let current = this.head;
            let previous = null;
            while (value !== current.value) {
                previous = current
                current = current.next
                if (current === null) {
                    return 'Value not found in the list' 
                }
            }
            removedNode = current
            previous.next = removedNode.next
    }
        this.size--
        return removedNode
    }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.prepend(40)

linkedListInstance.prepend(90)

linkedListInstance.prepend(30)

linkedListInstance.prepend(50)

console.log(linkedListInstance.print())
// Returns 
// 50
// 30
// 90
// 40

console.log(linkedListInstance.removeByValue(30))
console.log(linkedListInstance.removeByValue(2)) // Returns: Value not found in the list

console.log(linkedListInstance.removeByIndex(0))

console.log(linkedListInstance.print())
// Returns
// 90
// 40

STEP 6: Search Method In A LinkedList

We can search for a node within a linked list by its value, by iterating over each node until the first node that matches the provided value.

Searching has a time complexity of O(n).

// SEARCHING FOR A NODE IN A LINKED LIST**
        //To search for a node, we need to:
searchByValue(value) {
        // 1. Check against an empty list
        if (this.isEmpty()) {
            return 'Can not search athrough an empty list'
        }

        let current = this.head;
        let index = 0;

        // 2. Iterate through the list while the value does not match the value of the current node 
        while (current.value !== value) { 
            current = current.next
            index++

            // 3. Check against a null value, which indicates the end of the list, implying the value was not found
            if (current.next === null) {
                return 'Cannot find value in the list' 
            }
        }

        return index;
}

Now we can check to see if our search method is working as expected:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
        } else {
            while (current.next !== null) {
                current = current.next
            }
            current.next = node
        }
        this.size++
    }

    // Delete Method For Removing A Node From A LinkedList by Index
    removeByIndex(index) {
        if (index < 0 || index >= this.size) {
            return 'Invalid index provided'
        }
        let removedNode;
        if (index === 0) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let counter = 0;
            let current = this.head;
            let previous = null;

            while (counter < index) {
                previous = current
                current = current.next
                counter++
            }
            removedNode = current
            previous.next = removedNode.next
        }
        this.size--
        return removedNode
    }

    // Delete Method For Removing A Node From A LinkedList By Value
    removeByValue(value) {
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let current = this.head;
            let previous = null;
            while (value !== current.value) {
                previous = current
                current = current.next
                if (current === null) {
                    return 'Value not found in the list' 
                }
            }
            removedNode = current
            previous.next = removedNode.next
        }
        this.size--
        return removedNode
    }

    // Search Method
        searchByValue(value) {
                if (this.isEmpty()) {
                    return 'Can not search athrough an empty list'
                }

                let current = this.head
                let index = 0

                while (current.value !== value) { 
                    current = current.next
                    index++

                    if (current.next === null) {
                        return 'Cannot find value in the list' 
                    }
                }
                return index
        }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.prepend(40)

linkedListInstance.prepend(90)

linkedListInstance.prepend(30)

linkedListInstance.prepend(50)

console.log(linkedListInstance.print())
// Returns 
// 50
// 30
// 90
// 40

console.log(linkedListInstance.searchByValue(0))
// Returns:
//  Cannot find value in the list

console.log(linkedListInstance.searchByValue(90))
// Returns:
// 2

STEP 8: Inserting in a Linked List

Inserting a node into a linked list follows similar patterns of iterating over the list until we reach the index to insert the new node.

Inserting has a time complexity of O(n).

Here is a brief illustration of what inserting looks like in a linked list:

// INSERTING A NODE IN A LINKED LIST
        //To insert a node, we need to:
    insert(value, index) {
        // 1. Check against invalid index and values
        if (index < 0 || index > this.size || index === undefined || value == undefined) {
            return 'Invalid index/value provided'
        }
        // 2. Iterate over the list and insert at the right index
        if (index === 0) {
            this.prepend(value) // where the index is 0, we're basically prepending
        } else {
            const node = new Node(value) // making a node from the value supplied
            let current = this.head 
            let counter = 0 // to keep track of the index iterated over in the linked list

            while (counter !== index) { // advancing through the list where we have not gotten to the required index
                current = current.next
                counter++
            }

            node.next = current.next // Updating the new node's next
            current.next = node // Updating node before the new node, such that it's next points to the new node
            this.size++
        }
        return 'Node inserted successfully !'
    }

Now we can check to see if our insert method is working as expected:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
        } else {
            while (current.next !== null) {
                current = current.next
            }
            current.next = node
        }
        this.size++
    }

    // Delete Method For Removing A Node From A LinkedList by Index
    removeByIndex(index) {
        if (index < 0 || index >= this.size) {
            return 'Invalid index provided'
        }
        let removedNode;
        if (index === 0) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let counter = 0;
            let current = this.head;
            let previous = null;

            while (counter < index) {
                previous = current
                current = current.next
                counter++
            }
            removedNode = current
            previous.next = removedNode.next
        }

        this.size--

        return removedNode
    }

    // Delete Method For Removing A Node From A LinkedList By Value
    removeByValue(value) {
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let current = this.head;
            let previous = null;
            while (value !== current.value) {
                previous = current
                current = current.next
                if (current === null) {
                    return 'Value not found in the list' 
                }
            }
            removedNode = current
            previous.next = removedNode.next
        }

        this.size--
        return removedNode
    }

    // Search Method
        searchByValue(value) {
                if (this.isEmpty()) {
                    return 'Can not search athrough an empty list'
                }

                let current = this.head
                let index = 0

                while (current.value !== value) { 
                    current = current.next
                    index++

                    if (current.next === null) {
                        return 'Cannot find value in the list' 
                    }
                }
                return index
        }

        insert(value, index) {
            if (index < 0 || index > this.size || index === undefined || value == undefined) {
                return 'Invalid index/value provided'
            }

            if (index === 0) {
                this.prepend(value)
            } else {
                const node = new Node(value)
                let current = this.head 
                let counter = 0 list

                while (counter !== index) {
                    current = current.next
                    counter++
                }

                node.next = current.next
                current.next = node
                this.size++
            }
            return 'Node inserted successfully !'
    }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.prepend(30)
linkedListInstance.prepend(50)

console.log(linkedListInstance.print())
// Returns 
// 50
// 30

console.log(linkedListInstance.insert(20, 1))
// Returns
// Node inserted successfully !
console.log(linkedListInstance.print())
// Returns
// 50
// 20
// 30
console.log(linkedListInstance.insert())
// Returns 
// Invalid index/value provided

STEP 8: Reversing a Linked List

To reverse a linked list, we want to flip it such that each node’s next points to its previous node. It involves 5 steps:

  1. Update the next variable with the current node’s next

  2. Set the current node’s next to it’s previous

  3. Update the previous to the current node

  4. Update the current node to the next variable

  5. Update the head to point to the last node reversed, which is now the first node

Reversing has a time complexity of O(n).

// REVERSING A LINKED LIST
reverse() {
        let current = this.head
        let prev = null

        while (current) {
            let next = current.next
            current.next = prev
            prev = current
            current = next
        }
        this.head = prev
}

Now we can check to see if our reverse method is working as expected:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head= node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
        } else {
            while (current.next !== null) {
                current = current.next
            }
            current.next = node
        }
        this.size++
    }

    // Delete Method For Removing A Node From A LinkedList by Index
    removeByIndex(index) {
        if (index < 0 || index >= this.size) {
            return 'Invalid index provided'
        }
        let removedNode;
        if (index === 0) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let counter = 0;
            let current = this.head;
            let previous = null;

            while (counter < index) {
                previous = current
                current = current.next
                counter++
            }
            removedNode = current
            previous.next = removedNode.next
        }

        this.size--

        return removedNode
    }

    // Delete Method For Removing A Node From A LinkedList By Value
    removeByValue(value) {
        if (this.isEmpty()) {
            return 'Cannot remove from an empty list'
        }
        let removedNode;
        if (value === this.head.value) {
            removedNode = this.head
            this.head = this.head.next
        } else {
            let current = this.head;
            let previous = null;
            while (value !== current.value) {
                previous = current
                current = current.next
                if (current === null) {
                    return 'Value not found in the list' 
                }
            }
            removedNode = current
            previous.next = removedNode.next
        }

        this.size--
        return removedNode
    }

    // Search Method
        searchByValue(value) {
                if (this.isEmpty()) {
                    return 'Can not search athrough an empty list'
                }

                let current = this.head
                let index = 0

                while (current.value !== value) { 
                    current = current.next
                    index++

                    if (current.next === null) {
                        return 'Cannot find value in the list' 
                    }
                }
                return index
        }

        // Inserting into a Linked List
        insert(value, index) {
            if (index < 0 || index > this.size || index === undefined || value == undefined) {
                return 'Invalid index/value provided'
            }

            if (index === 0) {
                this.prepend(value)
            } else {
                const node = new Node(value)
                let current = this.head 
                let counter = 0 list

                while (counter !== index) {
                    current = current.next
                    counter++
                }

                node.next = current.next
                current.next = node
                this.size++
            }
            return 'Node inserted successfully !'
    }

    // REVERSING A LINKED LIST
    reverse() {
            let current = this.head
            let prev = null

            while (current) {
                let next = current.next
                current.next = prev
                prev = current
                current = next
            }
            this.head = prev
    }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

//Tests:

const linkedListInstance = new LinkedList

console.log(linkedListInstance.print()) // Returns 'Nothing to print, the list is empty'

linkedListInstance.prepend(30)
linkedListInstance.prepend(50)

console.log(linkedListInstance.print())
// Returns 
// 50
// 30

console.log(linkedListInstance.insert(20, 1))
// Returns
// Node inserted successfully !

console.log(linkedListInstance.print())
// Returns
// 50
// 20
// 30

console.log(linkedListInstance.reverse())
// Returns 
// 30
// 20
// 50

STEP 9: Linked List with a Tail

In the same way we maintained a pointer to the head of the list, we can keep a pointer to the last node, which may reduce the time complexity of particular operations. For example, while for other operations like searching, having a pointer to the tail node might not make a difference, in appending a node to a list, having a pointer to the tail node will save us the operation of traversing the entire list to get to the last node, thereby making it a constant time operation — O(1).

Here’s an implementation of a linked list with a pointer to the tail node including operations where having a pointer to the tail will count:

//Node Class
class Node {
    constructor(value) {
        this.value = value
        this.next = null
    }
}

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.tail = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head = node
            this.tail =node
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
            this.tail = node
        } else {
            this.tail.next = node
            this.tail = node
        }
        this.size++
    }

    removeFromBeginning(){
        if(this.isEmpty()){
        return 'An Empty List'
        }

        const nodeToRemove = this.head
        this.head = this.head.next
        this.size--

        return nodeToRemove
    }
    removeFromEnd(){
        if(this.isEmpty()){
            return 'An Empty List'
        }
        let nodeToRemove = this.tail
        if(this.size === 1){
            this.head = null
            this.tail = null
        }else{
            let prev = this.head

            while(prev.next !== this.tail){
                prev = prev.next
            }
            prev.next = null
            this.tail = prev
        }
        this.size--
        return nodeToRemove        
    }

    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head

        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

STEP 10: Doubly Linked List

Where nodes in a singly linked list only maintain a pointer to the next node in the list, in a doubly linked list, a node maintains a pointer to its next and previous nodes in the list.

Below is an implementation of the linked list with a pointer to the previous node:

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

//Linked List Class
class LinkedList{
    constructor(){
        this.head = null
        this.size = 0
    }

    getSize(){
        return this.size
    }

    isEmpty(){
        return this.size === 0    
    }

    //PREPENDING A NODE TO A LINKED LIST
    prepend(value){
        const node = new Node(value)

        if(this.isEmpty()){
            this.head = node
            this.tail = node 
        }else{
            node.next = this.head
            this.head = node
        }

        this.size++
    }

    // APPENDING A NODE TO A LINKED LIST
    append(value) {
        const node = new Node(value)
        let current = this.head

        if (this.isEmpty()) {
            this.head = node
            this.tail = node
        } else {
            this.tail.next = node
            node.previous = this.tail
            this.tail = node
        }
        this.size++
    }

    removeFromBeginning(){
        if(this.isEmpty()){
        return 'An Empty List'
        }

        const nodeToRemove = this.head
        this.head = this.head.next
        this.size--

        return nodeToRemove
    }
    removeFromEnd(){
        if(this.isEmpty()){
            return 'An Empty List'
        }
        let nodeToRemove = this.tail
        if(this.size === 1){
            this.head = null
            this.tail = null
        }else{
            let prev = this.tail.previous

            prev.next = null
            this.tail = prev
        }
        this.size--
        return nodeToRemove        
    }

    // REVERSING A LINKED LIST
    reverse() {
        let current = this.head
        let temp = null

        while (current) {
            temp = current.next
            current.next = current.previous
            current.previous = temp
            current = temp
        }
            temp = this.head
            this.head = this.tail
            this.tail = temp
    }
    // PRINTING THE VALUES OF THE LINKED LIST
    print() {
        if (this.isEmpty()) {
            alert('Nothing to print, the list is empty')
            return ('Nothing to print, the list is empty')
        }

        let current = this.head
        while (current !== null) {
            console.log(current.value)
            current = current.next
        }
    }
}

CONCLUSION:

Since each concept is a buildup, it pays more dividends to understand them before moving on to the next, especially to understand the concept of a linked list. If it helps, feel free to explore more resources to understand a linked list, before coming back to this how to Implement it article

Free tip, I found it instrumental to consult multiple resources, which consolidate what I am learning from different perspectives, in different words or forms, like an audiovisual in addition to a written piece.