Linked Lists are a fundamental data structure used to store information linearly, this information is not stored in contiguous places in memory rather linked lists use a pointer in each node that links to the next node that is stored.
A node in a linked list could be represented as follows:
As already stated each of these nodes contains data that is stored in this.value and has a direct reference to the next node in line through this.next, the first node of the linked list is usually referred to as the Head and the last node is called Tail, since the Tail is always the last node the value of its this.next property will always be null for singly linked lists.
So if we try to represent that in code we get something close to:
Let's start by creating a linked list API, we already know how to represent a node and we know the basics, we know how we will represent the head and the tail, so we can start to define how we will append new nodes to the linked list, for this we need to consider that:
If not head is defined the new node should be defined as the head and also the tail.
If the head is defined we add the new node to the current tail's next property and we define the new node as the tail.
import Node from'./LinkedListNode';classLinkedList{constructor(){this.head =null;this.tail =null;}append(value){// We create a new Nodeconst node =newNode(value);if(!this.head){// If not head is define we define it alongside with the tailthis.head = node;// We define the tailthis.tail = node;returnthis;}// If the head is defined we attach the new node to the// tail's next propertythis.tail.next = node;// We make the new node the tailthis.tail = node;returnthis;}}
This is excellent, you can see how the nodes connect with each other, they are just objects connected to each other through their next property.
1.1. Time complexity for Appending nodes to a linked list
Appending an element to the end of the linked list requires us to modify the tail's next property and reassign the tail with the value of the new node.
this is true for any node we want to append which makes this a constant O(1) operation.
2. Prepending nodes to a linked list
Prepending a node is simpler since we already have the head of the list stored, the only thing we need to do is assign the new node as the head of the list and define its next property with a reference to the previous head Node.
It does not matter how many nodes the linked list has, it will always be the same process and complexity for prepending hence the time complexity of prepending is constant O(1).
3. Accessing and Searching nodes
The only way to access and search an element in a given linked list is through the iteration of the next property of all the nodes that come before the node we are looking for, it is important to note that if the element we are searching or trying to access is not found this would still require us to go through all the nodes in the list.
i.e let's find the node 3 in the linked list below:
3.1. Time complexity of accessing and searching nodes
Knowing this we can stablish that accessing and searching an element would be O(n) where n = number of nodes in the list, even though we don't always search the whole list the big O notation analizes algorithms by their trend and worst case scenario and so we arrive at this conclusion.
4. Removing nodes from a linked list
Great, now as you can imagine, removing elements from a linked list is pretty straight forward:
Check if the node we want to remove is currently the head of our linked list, if so, we just remove the reference to such node by making this.head be the next node in line (since now there is no reference to the node with value 1 it will be garbage collected and removed):
If the node to remove is not the head, we iterate over the nodes until the node to remove is found, if the node is not found we don't do anything.
Once the node to remove is found we get the node previous to that one, we then modify this previous node's next property so that it points to the node that comes after the node to remove, in this way the reference to the node to be removed is lost and it can be garbage collected hence the node is removed from the linked list.
let's see how this would look in the code:
classLinkedList{...remove(value){if(!this.head || value ===undefined){returnnull;}let nodeToRemove =null;// Check if the node to remove is the head nodeif(this.head.value === value){// We save the node just to return it later nodeToRemove =this.head;// If the node is the head we remove the node by assigning// the second node as the head.this.head =this.head.next;}else{// currentNode will be used to iterate over the nodeslet currentNode =this.head;// We iterate over the nodes until there are no more nodes left to search// or until we find the node to removewhile(currentNode.next !==null){if(currentNode.next.value === value){// We save the node just to return it later nodeToRemove = currentNode.next;// If we find the node we remove it as explained on point 4. currentNode.next = currentNode.next.next;}else{// If the node has not been found we continue searching currentNode = currentNode.next;}}}return nodeToRemove;}}
Let's say we want to remove the node that contains the value 2, we would ideally do this by calling the method remove like:
linkedList.remove(2)
Which would modify the reference from the node with value 1 to be now the reference of the node with value 3, in this way node 2 is left out:
4.1. Time complexity for Deleting a node (From the beginning / Head node)
Deleting a node from the beginning of the list as previously seen just requires us to change the this.head value to be this.head.next in this way we remove the reference to the first node, since this operation is constant no matter the size of the list it is considered O(1).
4.2. Time complexity for Deleting the Tail or any node that's not the head
Doing this will require us to iterate over the list until we find the element to delete (Same that we need to search an node), then we just remove the node as usual so the time complexity would be O(n) where n = number of nodes in the list.
Space Complexity of linked lists
The space required for a linked list is directly correlated with the number of nodes that it holds, this means that the more nodes we have the more space we use and this grows linearly per node which makes linked lists O(n) for Space complexity.
Use cases and why to learn about them
Most of the cases where linked lists shine come in situations where we need to insert or delete multiple nodes, in these cases linked lists perform at a constant time which makes them ideal, also since linked list's space grows linearly we can also leverage their dynamic memory allocation in ocassions where we lack memory.
Another important point is that there are other structures that are and can be built with linked lists as a base, one good example are queues (That we will analyze later in another article)
Hope this article helped you understand linked lists a little.
Big O notation allows us to evaluate the performance of algorithms so that we can determine their efficiency and make decisions based on this determinations, let's try to understand how this notation works and how we can apply it in our lives as software developers.Read more
Tracking information in your application can be very challenging, especially when dealing when page unloads and users leaving before tracking requests finished, this article tries to teach you about some possible solutions to this challenges and more.Read more