This is the first day of my participation in Gwen Challenge
Dudu dudu, let’s ride with the blogger! 😊 attached at the end of the article related topics
Not only the graphic explanation, the notes are also very clear oh! :thumbsup:
[TOC]
What is a linked list
A linked list is a collection of nodes, each of which has a pointer to its next node. For example, like the little train pictured above, each carriage is connected by a rope. Each carriage is a node, and the connection between the carriages is the pointer :heart:
So once you know what a linked list is, a lot of people think, well, what’s the difference between this and an array? Isn’t array manipulation more convenient?
Arrays are fixed in size, and inserting or removing items from the beginning or middle of an array is expensive because it requires moving elements (the same is true behind the scenes, despite all the API we’ve learned).
1.1 Advantages of linked lists
One advantage of linked lists over traditional arrays is that adding or removing elements does not require moving other elements ==. In this way, the time complexity of add and remove operations == is O(1). The following figure is a schematic diagram of inserting nodes into a one-way linked list. We only need to change the next pointer of the previous node and change the next pointer of the new node to join the list
1.2 Disadvantages of linked lists
In contrast to an array, when accessing an element, an array can be accessed directly through the index, while in a linked list, accessing an element requires iterating through the list from the starting point until the desired element is found. So the time complexity of access falls between O(1) and O(n)
1.3 Time complexity comparison of each operation between unidirectional linked list and array
List operation | Maximum time complexity | An array of operating | Maximum time complexity |
---|---|---|---|
Search (access) | O(n) |
Search (access) | O(1) |
Insert | O(1) |
Insert | O(n) |
Remove (remove) | O(1) |
Remove (remove) | O(n) |
Append (add) | O(1) |
Append (add) | O(1) |
1.4 Classification of linked lists
There are three types: unidirectional, bidirectional and circular lists
1.4.1 Unidirectional linked lists
1.4.2 Bidirectional linked lists
1.4.3 Circular linked lists
2. Use JS to implement linked lists
Now that we know what a linked list is, we can use JS to implement related operations on a linked list
2.1 Unidirectional linked lists
2.1.1 Creating a one-way linked list
Here is the basic skeleton of a LinkedList
class LinkedList {
constructor() {
// List length size
this.size = 0;
this.head = null;
}
// Add the list entry
append(element){}// Insert the linked list entry
appendAt(position, element){}// Deletes the specified list item
remove(element){}// Delete the list entry at the specified location
removeAt(position){}// Find the specified element index
indexOf(element){}// Flip the linked list
reserve(){}// Get the node
getNode(index){}}/ / test
let l1 = new LinkedList();
Copy the code
Linked list data structures also require a Node helper class. The Node class represents the items to be added to the list. It contains an Element attribute, which is the value to add to the list, and a next attribute, which is a pointer to the next node item in the list.
/ / the node class
class Node {
constructor(element) {
this.element = element;
// Add a pointer
this.next = null}}Copy the code
Another important point is that we also need to store a reference to the first node. To do this, we can store the reference in a variable called head, and we’ll implement the method for filling in the LinkedList class.
append(element)
: Adds a new item to the end of the listappendAt(position, element)
: Inserts a new item to a specific location in the listremove(element)
: Removes an item from the listremoveAt(position)
: Removes an item from a specific position in the listgetNode(index)
: Gets a node at a locationreserve()
: reverses the linked list
2.1.2 Obtaining nodes in the Linked List
I wrote this first because there are many methods that use this function later! In fact, you can also not separate encapsulation into a function, save personal habits
Pass in the position of the node that you want to find, through the for loop, keep pointing current to the next bit until index is reached, break out of the for loop, and return the current node :orange:
getNode(index) {
// bounds determine whether the parameter passed is within the length range of the list
if (index < 0 || index >= this.size) {
throw new Error('out')}let current = this.head;
// Continuously move current to the next digit until index-1 is reached
// This is a loop loop
for (let i = 0; i < index; i++) {
current = current.next;
}
// Finally return to the node
return current
}
Copy the code
2.1.3 Append elements to the end of the list
There are two scenarios:
- The list is empty and the first element is added
- The list is not empty. Append elements to it
Get the last node of the list by using the getNode method in the previous part. Let the next pointer of the last node point to the newly created node node, so that the list is connected in series. Finally, let the length of the list add itself.
append(element) {
// Add attributes via instantiation
let node = new Node(element);
// Add operations
if (this.head === null) {
this.head = node;
} else {
let current = this.getNode(this.size - 1);
// Next of the last node points to the newly created node
current.next = node
}
// Add the linked entry, the size increases
this.size++;
}
Copy the code
Note that node creation is done by using the new operator. Node is constructed as an object with its own value, Element, and a pointer to next. The next pointer to the newly created node defaults to null
2.1.4 Insert an element anywhere in the list
Because you insert elements by location, you first determine whether the location is out of bounds. If the boundaries are crossed, you can simply throw an error. Again, there are two scenarios:
In the first scenario, we need to add an element at the beginning of the list, which is the first position, make the newly created node next pointer to the head node, which is this.head, and set the newly created node as the head node :tada:
The second scenario: non-starting insertion. First, we need to find the previous node pre, and make the newly created node point to the next node pre. Next, and make the next pointer of the Pre node point to the newly created node :apple:
// Insert the linked list entry
appendAt(position, element) {
// boundary judgment
if (position < 0 || position > this.size) {
throw new Error('position out range')}// instantiate create node
let node = new Node(element);
// Start insert
if (position === 0) {
node.next = this.head;
this.head = node;
} else {
// Find the previous node where you want to insert
let pre = this.getNode(position - 1);
// Make the newly created node point to the next node from the current previous node
node.next = pre.next;
// Make the previous node point to the newly created nodepre.next = node; }}Copy the code
2.1.5 Removing an element from a linked list (by specific location)
There are also two scenarios for removing elements: removing the first element, and removing any element other than the first. Again, we need to do the boundary judgment first, throwing errors outside the length of the list. :banana:
The first scenario is very simple, since the first node is removed, you just need to have the head point to the second element in the list
Now, suppose we want to remove the last item in the list or one of the middle items. First we need to get the previous node of the deleted node and make the next pointer of this node point to the next node of the deleted node. The discarded elements are then discarded in computer memory, waiting to be scavenged by the garbage collector. :pear:
// Delete the list entry at the specified location
removeAt(position) {
// If the length of the list is exceeded, an error is thrown
if (position < 0 || position >= this.size) {
throw new Error('position out range')}let current = this.head
if (position === 0) {
// If the first node is deleted, make head point to the next node
this.head = current.next;
} else {
// Get the previous node of the deleted location
let pre = this.getNode(position - 1);
// current indicates the deleted node
current = pre.next;
// Make the previous node point to the next node after the deleted node
pre.next = current.next;
}
// The length is reduced
this.size--;
}
Copy the code
2.1.6 Finding the position of an element in the linked list
We need a variable to help us loop through the list, current in the code, whose initial value is head. The for loop iterates through the list to determine whether the value of the current position is equal to the search value, and if so, returns its position; If not, keep going. If the end of the for loop does not pop up, the end of the list is reached, that is, the element does not exist in the list, and -1 is returned. :heart_decoration:
// Find the specified element index
indexOf(element) {
// The current list head node
let current = this.head;
// Iterate through the list
for (let i = 0; i < this.size; i++) {
if (current.element === element) {
return i
}
// Move down in sequence
current = current.next;
}
return -1;
}
Copy the code
2.1.7 Removing an element based on its value
Now that we have indexOf, we can pass in the element’s value, find its location, and then call removeAt and pass in the location to remove the element :deciduous_tree:
remove(element) {
// Get the position of the element to be deleted in the list
let index = this.indexOf(element)
if(index! = -1 ) {
// Remove the node at the specified location using the following removeAt method
this.removeAt(index)
}else {
// Delete elements that do not exist, throw an error
throw new Error('element no found')}}Copy the code
2.1.8 Reverse the linked list
Reverse lists are common in LeetCode and can be found in various interview questions. So, in passing, I’ll write the following
First, we define two Pointers: prev and current, with current before prev after. Set the next pointer of current to prev each time to achieve a local inversion. After the partial inversion is completed, prev and Current move forward one position at the same time, and cycle the process until current reaches the end of the list. The animation is as follows
reserve() {
let prev = null;
let current = this.head;
while (current) {
// Save the current next bit
let next = current.next;
// Make the current node point to the previous node prev
current.next = prev;
// After the swap, let the prev move to the next bit, which is current
prev = current;
current = next;
}
// Return the flipped list
this.head = prev
}
Copy the code
2.2 Bidirectional linked lists
The difference between a bidirectional list and a unidirectional list is that a node in a unidirectional list has only a pointer to the next node in the chain, while in a bidirectional list, there are two Pointers, one pointing to the previous element and one to the next element, as shown below:
2.2.1 Creating a bidirectional linked list
There is an extra pointer to the previous element as opposed to a one-way list, so there are some changes to be made in the code
// a list node
class Node {
constructor(element) {
this.element = element
this.next = null
this.prev = null/ / new}}// Two-way linked list
class doubleLinkedList {
constructor () {
this.size = 0
this.head = null
this.tail = null/ / new
}
// Here is how to write next
}
Copy the code
Advantages of bidirectional lists: Access to the next or == before == element of a particular node. In a one-way list, if you iterate over the list and miss the element you’re looking for, you need to go back to the beginning of the list and start over:
Note: There is a tail attribute in the doubleLinedList class that holds a reference to the last item in the list.
2.2.2 Obtaining nodes in the Linked List
The method of obtaining linked list by position is the same as that in one-way linked list. Remember to jump back and have a look! :heart:
// Get a node at a location, same as a one-way list
getNode(index) {
if (index < 0 || index >= this.size) {
throw new Error('out')}let current = this.head
for(let i = 0; i < index; i++) { current = current.next }return current
}
Copy the code
2.2.3 Append nodes to the end of the list
Believe that you see here, should know what to do first! There are two cases
Case one: The linked list is empty
Second case: Linked lists are not empty
In the first case, we simply point head and tail to the newly created node
In the second case, which is a bit different from the one-way list, we need to set the precursor pointer.
Make the next pointer of the last node point to the new node, and make the prev pointer of the new node point to the previous node (i.e. the last node before joining). Finally, remember to move tail to the last node and increment the size! Here’s the schematic, clear and clear :thumbsup:
append(element) {
// Create a node
let node = new Node(element)
// Save the current pointer to the last element
let tail = this.tail
// If the list is empty, the new node will be the first
if(this.head === null) {
// Head and tail are nodes
this.head = node
this.tail = node
}else {
// Make the next pointer to the last element point to node
tail.next = node
// Make node's precursor pointer to tail the last node
node.prev = tail
// Finally update tail to point to the last element node
this.tail = node
}
this.size++
}
Copy the code
2.2.4 Inserting a node into a Linked list
Inserting a new node into a two-way list is very similar to a one-way list. The difference is that one-way lists only need to control one NEXT pointer, whereas bidirectional lists control both next and prev Pointers
Let’s start with the first scenario: insert a new node at the first position in the list. If the list is empty, all you need to do is point head and tail to the new node. If not null, the current variable will hold a reference to the first element in the list, so simply make the next pointer to current, the prev pointer to current, and the head pointer to the first node. ==
Next comes the second scenario: tail insertion, which is somewhat similar to the previous one, but will not be repeated here
The final scenario is also a little more complicated: insert in the middle of the list
Using the getNode method we wrote earlier, we get the previous node, preNode, and the next node, current. We insert new elements between the current and preNode elements. First, the next pointer to node points to current, then the prev pointer to node points to preNode, and then the remaining two Pointers to node respectively.
// Insert the node
insert(position, element) {
// The input position needs to be within the range
if (position < 0 || position > this.size) {
throw new Error('position out range')}// Create a node
let node = new Node(element)
// Save the first node as a variable
let current = this.head
// In the first case, insert in the most header
if (position === 0) {
// The list is empty
if (!this.head) {
this.head = node
this.tail = node
} else {
// The linked list is not empty
// Make node's next pointer point to the first node
node.next = current
// The first node has a precursor pointer to node
current.prev = node
// head points to the new first node
this.head = node
}
this.size++
} else if (position === this.size) {
// Insert at the end
// the current variable holds the last node
current = this.tail
// The next pointer to the last node points to the new node
current.next = node
// The new node has a precursor pointer to the previous node
node.prev = current
// tail moves to node
this.tail = node
this.size++
} else {
// Insert in the middle
// Get the previous node of the insertion position
let preNode = this.getNode(position - 1)
// The current variable holds the next node of the preNode
current = preNode.next
// 1. Make the next pointer to node current
node.next = current
// 2. Set the prev pointer of node to the previous preNode
node.prev = preNode
// 3. Make the next pointer to preNode point to the new node
preNode.next = node
// 4. Set the current prev pointer to the new node
current.prev = node
this.size++
}
}
Copy the code
Note: In our encapsulated getNode method, which is iterated from scratch anyway, we can actually optimize the process by iterating from the end when the position we are looking for is greater than half size, which improves performance.
2.2.5 Remove an element from a specific position in a linked list
In fact, the operation of bidirectional linked lists is similar to that of unidirectional linked lists, but there is only one more precursor pointer to operate one more pointer. For this method of deleting elements in specific positions, the most important thing we need to know is to remove the deleted node from the linked list, and then connect the linked list intact
Again, we need to break it down into 3 cases
Case 1: Delete the first node
The current variable holds a reference to the first node in the list that we want to remove. The first thing to change is the head reference, changing it from current to current-.next. We also need to update the current. Next pointer to the last element, so we need to determine whether the length of the list is 1. If it is 1, the list will be empty after the first node is deleted, so we need to set tail to NULL. The demo diagram is as follows:
The second case: delete the last node
Since we have the tail reference to the last node, we don’t need to get the last node via getNode, so we can also assign the tail reference to the current variable. Next, we need to update the reference to tail to the next-to-last element in the list and point the next pointer to null, as shown below:
Third case: delete intermediate nodes
First, we need to find the node to be deleted through the getNode method, save it in the current variable, and use preNode to indicate the previous node to be deleted. To remove it, we can skip it in the linked list by updating the references to prev.next and current.next-prev, so that the next pointer to prev points to current.next and the prev pointer to current.next points to prev, as shown below:
removeAt(position) {
// The input position needs to be within the range
if (position < 0 || position > this.size) {
throw new Error('position out range')}let current = this.head
// Delete the first node
if (position === 0) {
// make head point to the next digit
this.head = current.next
// If the length of the list is 1, the end of the list should be null
if(this.size === 1) {
this.tail = null
}else {
// The first node (current.next) after the node is deleted is null
this.head.prev = null
}
this.size--
}else if (position === this.size -1) {
// Delete the last node
// Let current save the node we want to delete
current = this.tail
// move tail to the first digit of the deleted node
this.tail = current.prev
// Set tail's next pointer to null so that the last node is discarded
this.tail.next = null
this.size--
}else {
// The deleted node
let current = this.getNode(position)
// Delete the previous node of the node
let preNode = current.prev
// Delete the previous node next points to its next node
preNode.next = current.next
// The prev of the latter node points to the preNode
current.next.prev = preNode
this.size--
}
}
Copy the code
2.2.6 Find the position of an element in the linked list
This is done in the same way as a one-way list, with no changes :hamburger:
indexOf(element) {
// The current list head node
let current = this.head;
// Iterate through the list
for (let i = 0; i < this.size; i++) {
if (current.element === element) {
return i
}
// Move down in sequence
current = current.next;
}
return -1;
}
Copy the code
2.2.7 Remove nodes based on element values
RemoveAt removeAt removeAt removeAt removeAt removeAt removeAt removeAt removeAt removeAt removeAt
// Delete elements based on their value
remove(element) {
let index = this.indexOf(element)
if(index ! = -1) {
// Remove the node at the specified location using the following removeAt method
this.removeAt(index)
} else {
// Delete elements that do not exist, throw an error
throw new Error('element no found')}}Copy the code
2.2.8 Printing bidirectional linked lists
By walking through the list, concatenating the values of each node, it looks much clearer
Example: NULL<->1<->2<->3<->NULL
print() {
let str = 'NULL<->';
let current = this.head
while(current ! = =null) {
str += current.element + '< - >';
current = current.next;
}
return str += 'NULL';
}
Copy the code
2.3 Unidirectional circular linked lists
A circular list is similar to a one-way list in that the node type is the same. The only difference is that when creating a circular list, the next pointer of the last node points to the first node
// Assume lastItem is the last node
lastItem.next = this.head
Copy the code
2.3.1 Creating a one-way circular linked list
The general structure is the same as that of a one-way list
class Node {
constructor(element) {
this.element = element
this.next = null}}class CircleLinkedList {
constructor() {
this.size = 0
this.head = null
}
// Add nodes at the end
append(element) {}
// Insert node at any position
insert(position,element) {}
// Delete nodes by location
removeAt(position){}
// Find the corresponding index of the element, the same as the one-way list
indexOf(element) {}
// Remove nodes based on element values, as in a one-way list
remove(element) {}
// Get node, same as unidirectional list
getNode(index){}}Copy the code
2.3.2 Append nodes to the end of the list
If the list is empty, use head to point to a new node
In the second case, the list is not empty. Use getNode to get the last node of the list and make the next pointer point to the new node node
Note: the next pointer of the last node points to the head of the first node after executing the if judgment
append(element) {
// Create a node
let node = new Node(element)
// The linked list is empty
if (this.head === null) {
this.head = node
} else {
// Get the last node
let current = this.getNode(this.size - 1)
current.next = node
}
In either case, keep it end to end
node.next = this.head
this.size++
}
Copy the code
2.3.3 Inserting a Node into a Linked List
Similarly, there are two scenarios ==
- When the insertion position is the first, unlike the operation of a one-way list, extra will be required to link the last node of the list
next
The pointer points to the new nodenode
- When you insert somewhere else, you can use the ternary operator to determine what the new node is
next
Student: Pointing rightnull
If it isnull
New nodeInsert it at the last node, the node’snext
Pointer to the first node, otherwise insert normally
insert(position, element) {
// boundary judgment
if (position < 0 || position > this.size) {
throw new Error('position out range')}// instantiate create node
let node = new Node(element);
if (position === 0) {
// node points to the previous first node
node.next = this.head;
// The head node is changed to the new node node
this.head = node;
let current = this.getNode(this.size - 1)
current.next = this.head
this.size++
} else {
// Find the previous node where you want to insert
let prev = this.getNode(position - 1);
// If prev.next is null point to head otherwise point to next
node.next = prev.next == null ? this.head : prev.next
// Make the previous node point to the newly created node
prev.next = node;
this.size++
}
}
Copy the code
2.3.4 Deleting a node at a specific location from the linked list
Different from one-way linked lists, == when deleting the first node ==, the next pointing of the last node needs to be changed to point to the new first node. When deleting other nodes, it needs to determine whether the next pointing of the previous node of the following deleted node is null to proceed with the next operation
// Delete nodes by location
removeAt(position) {
// If the length of the list is exceeded, an error is thrown
if (position < 0 || position >= this.size) {
throw new Error('position out range')}let current = this.head
if (position === 0) {
// If the first node is deleted, make head point to the next node
let last = this.getNode(this.size - 1);
this.head = current.next;
last.next = this.head
} else {
// Get the previous node of the deleted location
let pre = this.getNode(position - 1);
// current indicates the deleted node
current = pre.next;
// Make the previous node point to the next node after the deleted node
pre.next = current.next == null ? this.head : current.next;
}
// The length is reduced
this.size--;
}
Copy the code
2.4 Bidirectional circular linked lists
Bidirectional lists differ from unidirectional lists in that they have an additional prev pointer from the first node to the last node
Bidirectional circular list and the front of the difference is not very big, this article will not expand to write, I believe that see the front of the detailed solution, must be able to complete their own
3. Related topics
A few topics on linked lists in Leetcode, with ac code attached
3.1 Reverse a linked list(Direct link oh!)
var reverseList = function(head) {
let prev = null;
let current = head;
while (current) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev
};
Copy the code
3.2 Merge K ascending linked lists
var mergeKLists = function (lists) {
if(! lists || ! lists.length)return null;
let end = lists[0]
let i = 1;
while(i ! = lists.length) { end = mergeTwoLists(lists[i++], end) }return end
};
function mergeTwoLists(l1, l2) {
// If one of them is empty, the other list is returned
if(! l1)return l2
if(! l2)return l1
// Compare the list values in turn
if (l1.val > l2.val) {
// If the value of l2 is large, the next of L2 is recursively determined again
l2.next = mergeTwoLists(l1, l2.next)
return l2
} else {
l1.next = mergeTwoLists(l1.next, l2)
return l1
}
};
Copy the code
3.3 Delete the penultimate node of the linked list
var removeNthFromEnd = function (head, n) {
let node = head;
let count = 0 ;
// Calculate the list length
while(node) {
node = node.next;
count++;
}
count = count - n - 1
if(count == -1) {
return head.next
}else {
node = head;
while(count > 0) {
node = node.next;
count--;
}
}
node.next = node.next.next
return head
};
Copy the code
4. Summary
That’s all for this JavaScript implementation list, and I hope you learned a lot from it.
And that’s the end of today’s sharing!
References: javascript data structures and algorithms