Linked Lists: A Comprehensive Guide and Detailed Implementation with JavaScript
Fernando Nunes
Software Engineer | Full Stack Developer | Angular | Nodejs | Nestjs | React | AWS | Azure
Abstract
This article provides an in-depth tutorial on linked lists, a fundamental data structure in computer science. Linked lists allow for dynamic memory allocation and offer efficient insertion and deletion operations. This guide explores the conceptual understanding, operations, and detailed implementation of linked lists, complete with a Big O analysis for each operation. Detailed explanations of each line of code within the LinkedList class methods are provided, along with practical examples to illustrate their usage. By the end of this guide, readers will have a thorough understanding of linked lists and their implementation in JavaScript, making it easier to apply this knowledge in various software development scenarios.
Introduction
Linked lists are a versatile data structure that allows for efficient dynamic memory management. Unlike arrays, linked lists do not require contiguous memory allocation, making them ideal for situations where the size of the data set is unknown or changes dynamically. This article aims to provide a comprehensive overview of linked lists, starting from their structure to detailed explanations of each method used in their implementation.
Linked Lists vs. Arrays
Arrays are a commonly used data structure in programming, characterized by their contiguous memory allocation and ability to access elements via indices. However, arrays come with limitations, such as fixed size and costly insertion and deletion operations when elements need to be added or removed from the middle.
Linked lists, on the other hand, consist of nodes where each node contains a value and a pointer to the next node in the sequence. This non-contiguous memory allocation allows linked lists to grow and shrink dynamically, offering flexibility that arrays lack. However, accessing elements in a linked list requires traversal from the head, making certain operations less efficient compared to arrays.
Key Differences:
Structure of a Linked List
A linked list is composed of nodes, where each node has two main components:
In a linked list, two pointers are typically maintained:
Node Class
Before we explore the methods of the LinkedList class, it's essential to understand the Node class.
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
Explanation:
Example:
const node = new Node(4);
console.log(node); // Node { value: 4, next: null }
Here, a node is created with the value 4, and since it doesn’t point to any other node, next is null.
Detailed Explanation of Linked List Methods
Each method in the LinkedList class is designed to perform a specific operation on the linked list. Below, we will delve into the purpose of each line of code within these methods, providing examples and explaining why certain approaches are used.
LinkedList Constructor
class LinkedList {
constructor(value) {
const newNode = new Node(value);
this.head = newNode;
this.tail = newNode;
this.length = 1;
}
}
Explanation:
Example:
const list = new LinkedList(4);
console.log(list);?
// LinkedList { head: Node { value: 4, next: null }, tail: Node { value: 4, next: null }, length: 1 }
Here, a linked list is created with a single node containing the value 4. The head and tail both point to this node, and the list length is 1.
Push Method
push(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
Explanation:
Example:
list.push(5);
console.log(list);
// LinkedList { head: Node { value: 4, next: Node { value: 5, next: null } }, tail: Node { value: 5, next: null }, length: 2 }
After pushing the value 5 to the list, the new node is added to the end, the tail now points to this new node, and the list length is updated to 2.
Why Use It?: The push method is essential for adding elements to the end of the list efficiently in O(1) time.
Pop Method
pop() {
if (!this.head) return undefined;
let temp = this.head;
let pre = this.head;
while (temp.next) {
pre = temp;
temp = temp.next;
}
this.tail = pre;
this.tail.next = null;
this.length--;
if (this.length === 0) {
this.head = null;
this.tail = null;
}
return temp;
}
Explanation:
Example:
list.pop();
console.log(list);
// LinkedList { head: Node { value: 4, next: null }, tail: Node { value: 4, next: null }, length: 1 }
After popping, the last node (with value 5) is removed, the tail now points to the node with value 4, and the list length is 1.
Why Use It?: The pop method efficiently removes the last element in O(n) time (due to the need to traverse the list) and is useful for stack implementations.
Unshift Method
unshift(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head = newNode;
}
this.length++;
return this;
}
Explanation:
Example:
list.unshift(3);
console.log(list);
// LinkedList { head: Node { value: 3, next: Node { value: 4, next: null } }, tail: Node { value: 4, next: null }, length: 2 }
After unshifting, a new node with value 3 is added to the start of the list, and the list length is updated to 2.
Why Use It?: The unshift method efficiently adds elements to the beginning of the list in O(1) time, useful for queue implementations.
领英推荐
Shift Method
shift() {
if (!this.head) return undefined;
let temp = this.head;
this.head = this.head.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
return temp;
}
Explanation:
Example:
list.shift();
console.log(list);
// LinkedList { head: Node { value: 4, next: null }, tail: Node { value: 4, next: null }, length: 1 }
After shifting, the first node (with value 3) is removed, and the head now points to the node with value 4.
Why Use It?: The shift method removes the first element in O(1) time, which is essential for implementing queues.
Get Method
get(index) {
if (index < 0 || index >= this.length) return undefined;
let temp = this.head;
for (let i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
Explanation:
Example:
const node = list.get(1);
console.log(node); // Node { value: 4, next: null }
In this example, the get(1) method retrieves the node at index 1. If the list has the structure Node { value: 3, next: Node { value: 4, next: null } }, it will return the node with value 4.
Why Use It?: The get method is essential for accessing elements at specific indices within the list. Though it operates in O(n) time due to the need to traverse the list, it is a straightforward and efficient way to retrieve a node by its position.
Set Method
set(index, value) {
let temp = this.get(index);
if (temp) {
temp.value = value;
return true;
}
return false;
}
Explanation:
Example:
list.set(1, 10);
console.log(list.get(1)); // Node { value: 10, next: null }
In this example, the set(1, 10) method sets the value of the node at index 1 to 10. The node's value is then updated accordingly.
Why Use It?: The set method is useful for updating the value of a node at a specific index, without altering the list's structure. It allows for efficient in-place modifications to the list.
Insert Method
insert(index, value) {
if (index < 0 || index > this.length) return false;
if (index === 0) return this.unshift(value);
if (index === this.length) return this.push(value);
const newNode = new Node(value);
const temp = this.get(index - 1);
newNode.next = temp.next;
temp.next = newNode;
this.length++;
return true;
}
Explanation:
Example:
list.insert(1, 7);
console.log(list.get(1)); // Node { value: 7, next: Node { value: 4, next: null } }
In this example, the insert(1, 7) method inserts a node with the value 7 at index 1, shifting the original node at index 1 to index 2.
Why Use It?: The insert method allows for flexible insertion of elements at any position within the list. It is especially useful when you need to insert an element in the middle of the list, maintaining the order of the existing elements.
Remove Method
remove(index) {
if (index < 0 || index >= this.length) return undefined;
if (index === 0) return this.shift();
if (index === this.length - 1) return this.pop();
const before = this.get(index - 1);
const temp = before.next;
before.next = temp.next;
temp.next = null;
this.length--;
return temp;
}
Explanation:
node to be removed by setting the next pointer of the before node to the node after temp.
Example:
list.remove(1);
console.log(list.get(1)); // Node { value: 4, next: null }
In this example, the remove(1) method removes the node at index 1. The list is updated, and the removed node is returned.
Why Use It?: The remove method is crucial for dynamically managing the contents of the list, allowing nodes to be removed from any position. It ensures that the list structure remains intact after the removal.
Reverse Method
reverse() {
let temp = this.head;
this.head = this.tail;
this.tail = temp;
let next = null;
let prev = null;
for (let i = 0; i < this.length; i++) {
next = temp.next;
temp.next = prev;
prev = temp;
temp = next;
}
return this;
}
Explanation:
Example:
list.reverse();
console.log(list);
// LinkedList { head: Node { value: 4, next: Node { value: 3, next: null } }, tail: Node { value: 3, next: null }, length: 2 }
In this example, the reverse() method reverses the order of the nodes in the list.
Why Use It?: The reverse method is a common operation in algorithms that require reversing the order of elements. It is also a popular interview question due to the complexity involved in manipulating pointers during the reversal process.
Complete LinkedList Class
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor(value) {
const newNode = new Node(value);
this.head = newNode;
this.tail = newNode;
this.length = 1;
}
push(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
pop() {
if (!this.head) return undefined;
let temp = this.head;
let pre = this.head;
while (temp.next) {
pre = temp;
temp = temp.next;
}
this.tail = pre;
this.tail.next = null;
this.length--;
if (this.length === 0) {
this.head = null;
this.tail = null;
}
return temp;
}
unshift(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head = newNode;
}
this.length++;
return this;
}
shift() {
if (!this.head) return undefined;
let temp = this.head;
this.head = this.head.next;
this.length--;
if (this.length === 0) {
this.tail = null;
}
return temp;
}
get(index) {
if (index < 0 || index >= this.length) return undefined;
let temp = this.head;
for (let i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
set(index, value) {
let temp = this.get(index);
if (temp) {
temp.value = value;
return true;
}
return false;
}
insert(index, value) {
if (index < 0 || index > this.length) return false;
if (index === 0) return this.unshift(value);
if (index === this.length) return this.push(value);
const newNode = new Node(value);
const temp = this.get(index - 1);
newNode.next = temp.next;
temp.next = newNode;
this.length++;
return true;
}
remove(index) {
if (index < 0 || index >= this.length) return undefined;
if (index === 0) return this.shift();
if (index === this.length - 1) return this.pop();
const before = this.get(index - 1);
const temp = before.next;
before.next = temp.next;
temp.next = null;
this.length--;
return temp;
}
reverse() {
let temp = this.head;
this.head = this.tail;
this.tail = temp;
let next = null;
let prev = null;
for (let i = 0; i < this.length; i++) {
next = temp.next;
temp.next = prev;
prev = temp;
temp = next;
}
return this;
}
}
This complete class, LinkedList, encapsulates all the fundamental operations needed for managing a singly linked list, making it a versatile and essential data structure in various programming scenarios.
Conclusion
Linked lists are a fundamental data structure that provides efficient dynamic memory management. Each method in the LinkedList class serves a specific purpose, whether it’s adding, removing, accessing, or modifying nodes. The detailed explanations and examples provided here illustrate the practical use of each method and reinforce the importance of understanding linked lists as a core component of data structures and algorithms.
References
Mobile Engineer | React Native Developer | React | TypeScript | JavaScript | Mobile Developer | Node
1 个月Thanks for sharing!
Senior iOS Engineer | Mobile Developer | Swift | Objective-C
1 个月Great explanation Fernando!
Full Stack Developer | Software Engineer | NodeJs | Ruby on Rails | ReactJS | GCP | AWS
1 个月Awesome Fernando Nunes
Full Stack Software Engineer | .NET | C# | TDD | React | Azure | SQL | Docker
1 个月Nice explanation
Senior Mobile Developer | Android Software Engineer | Jetpack Compose | GraphQL | Kotlin | Java
1 个月Thanks for sharing