A list (or Linked List) is a linear data structure where each node is "linked" to the next.
-
Singly: every item has a pointer to the next node
-
Doubly: every node has a reference to the next and previous object
-
Circular: the last element points to the first one.
Each element or node is connected to the next one by a reference. When a node only has one connection it’s called singly linked list:
Usually, a Linked List is referenced by the first element in called head (or root node). For instance, if you want to get the cat
element from the example above, then the only way to get there is using the next
field on the head node. You would get art
first, then use the next field recursively until you eventually get the cat
element.
When each node has a connection to the next
item and also the previous
one, then we have a doubly linked list.
With a doubly list you can not only move forward but also backward. If you keep the reference to the last element (cat
) you can step back and reach the middle part.
If we implement the code for the Node
elements, it would be something like this:
link:../../../src/data-structures/linked-lists/node.js[role=include]
Arrays allow you to access data anywhere in the collection using an index. However, Linked List visits nodes in sequential order. In the worst case scenario, it takes O(n) to get an element from a Linked List. You might be wondering: Isn’t always an array more efficient with O(1) access time? It depends.
We also have to understand the space complexity to see the trade-offs between arrays and linked lists. An array pre-allocates contiguous blocks of memory. When it is getting full, it has to create a bigger array (usually 2x) and copy all the elements. It takes O(n) to copy all the items over. On the other hand, LinkedList’s nodes only reserve precisely the amount of memory it needs. They don’t have to be next to each other, nor large chunks of memory have to be booked beforehand like arrays. Linked List is more on a "grow as you go" basis.
Another difference is that adding/deleting at the beginning on an array takes O(n); however, the linked list is a constant operation O(1) as we will implement later.
A drawback of a linked list is that if you want to insert/delete an element at the end of the list, you would have to navigate the whole collection to find the last one O(n). However, this can be solved by keeping track of the last element in the list. We are going to implement that!
We are going to implement a doubly linked list. First, let’s start with the constructor.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
// ... methods go here ...
}
In our constructor, we keep a reference of the first
and also last
node for performance reasons.
Finding an element by value there’s no other way than iterating through the whole list.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
If we find the element, we will return the index otherwise undefined
. The runtime for locating an item by value is O(n).
For finding elements by value or position we are using the following helper function:
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
-
We initialize two variables
current
to the first node andposition
to keep track of the index. -
While
current
node is not null we keep going. -
On each loop we move to the next node and increment the index.
-
We invoke the callback passing the current position and node. If the callback returns something, then we stop and return that value.
-
Return whatever result we got from the callback. E.g., we can return the index or the node itself or any other calculation.
We are going to use this find
method again to implement searching by index.
Searching by index is very similar, we iterate through the list until we find the element that matches the position.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
If there’s no match, we return undefined
then. The runtime is O(n). As you might notice the search by index and by position methods looks pretty similar. If you want to take a look at the whole implementation click here.
Similar to the array, with a linked list you can add elements at the beginning, end or anywhere in the middle of the list. So, let’s implement each case.
We are going to use the Node
class to create a new element and stick it at the beginning of the list as shown below.
To insert at the beginning, we create a new node with the next reference to the current first node. Then we make first the new node. In code, it would look something like this:
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
As you can see, we create a new node and make it the first one.
Appending an element at the end of the list can be done very effectively if we have a pointer to the last
item in the list. Otherwise, you would have to iterate through the whole list.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
If there’s no element in the list yet, the first and last node would be the same. If there’s something, then, we go to the last
item and add the reference next
to the new node. That’s it! We got a constant time for inserting at the beginning and the end of the list: O(1).
For inserting an element at the middle of the list, you would need to specify the position (index) in the collection. Then, you create the new node and update the references to it.
-
New node’s
next
. -
New node’s
previous
. -
New node’s previous
next
. -
New node’s next
previous
.
Let’s do an example, with the following doubly linked list:
art <-> dog <-> cat
We want to insert the new
node in the 2nd position. For that we first create the "new" node and update the references around it.
Take a look into the implementation of LinkedList.add:
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
-
If the new item goes to position 0, then we reuse the
addFirst
method, and we are done! -
However, If we are adding to the last position, then we reuse the
addLast
method, and done! -
Adding
newNode
to the middle: First, create thenew
node only if the position exists. Take a look at Searching by index to seeget
implementation. -
Set newNode
previous
reference. -
Set newNode
next
link. -
No other node in the list is pointing to
newNode
, so we have to make the prior element point tonewNode
. -
Make the next element point to
newNode
.
Take notice that we reused, addFirst
and addLast
methods. For all the other cases the insertion is in the middle. We use current.previous.next
and current.next
to update the surrounding elements and make them point to the new node. Inserting on the middle takes O(n) because we have to iterate through the list using the get
method.
Deleting is an interesting one. We don’t delete an element; we remove all references to that node. Let’s go case by case to explore what happens.
Deleting the first element (or head) is a matter of removing all references to it.
For instance, to remove the head (“art”) node, we change the variable first
to point to the second node “dog”. We also remove the variable previous
from the "dog" node, so it doesn’t point to the “art” node. The garbage collector will get rid of the “art” node when it seems nothing is using it anymore.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
As you can see, when we want to remove the first node we make the 2nd element the first one.
Removing the last element from the list would require to iterate from the head until we find the last one, that’s O(n). But, If we have a reference to the last element, which we do, We can do it in O(1) instead!
For instance, if we want to remove the last node “cat”. We use the last pointer to avoid iterating through the whole list. We check last.previous
to get the “dog” node and make it the new last
and remove its next reference to “cat”. Since nothing is pointing to “cat” then is out of the list and eventually is deleted from memory by the garbage collector.
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
The code is very similar to removeFirst
, but instead of first we update last
reference, and instead of nullifying previous
we nullify its next
reference.
To remove a node from the middle, we make the surrounding nodes to bypass the one we want to delete.
In the illustration, we are removing the middle node “dog” by making art’s next
variable to point to cat and cat’s previous
to be “art” totally bypassing “dog”.
Let’s implement it:
link:../../../src/data-structures/linked-lists/linked-list.js[role=include]
Notice that we are using the get
method to get the node at the current position. That method loops through the list until it found the node at the specified location. This iteration has a runtime of O(n).
So far, we have seen two liner data structures with different use cases. Here’s a summary:
Data Structure |
Searching By |
Inserting at the |
Deleting from |
Space |
|||||
Index/Key |
Value |
beginning |
middle |
end |
beginning |
middle |
end |
||
Array |
O(1) |
O(n) |
O(n) |
O(n) |
O(1) |
O(n) |
O(n) |
O(1) |
O(n) |
Linked List (singly) |
O(n) |
O(n) |
O(1) |
O(n) |
O(1) |
O(1) |
O(n) |
O(n) |
O(n) |
Linked List (doubly) |
O(n) |
O(n) |
O(1) |
O(n) |
O(1) |
O(1) |
O(n) |
O(1) |
O(n) |
If you compare the singly linked list vs. doubly linked list, you will notice that the main difference is deleting elements from the end. For a singly list is O(n), while for a doubly list is O(1).
Comparing an array with a doubly linked list, both have different use cases:
Use arrays when:
-
You want to access random elements by numeric key or index in constant time O(1).
-
You need two-dimensional and multi-dimensional arrays.
Use a doubly linked list when:
-
You want to access elements in a sequential manner only like part02-linear-data-structures.asc or part02-linear-data-structures.asc.
-
You want to insert elements at the start and end of the list. The linked list has O(1) while array has O(n).
-
You want to save some memory when dealing with possibly large data sets. Arrays pre-allocate a large chunk of contiguous memory on initialization. Lists are more “grow as you go”.
For the next two linear data structures part02-linear-data-structures.asc and part02-linear-data-structures.asc, we are going to use a doubly linked list to implement them. We could use an array as well, but since inserting/deleting from the start perform better on linked-list, we are going use that.