Oscar Luna

Posted on

# Javascript Data Structures and Algorithms (Linked Lists, part 1: Singly-Linked Lists)

Hello! This is the third installment in a series of posts that will introduce you to the Data structures and algorithms you can expect to be asked about at a technical interview. If you want a more thorough introduction, check out parts 1 and 2 where I discuss arrays in the menu above. Let's get right into it.

Linked Lists are a sorted collection of vertices or nodes that contain data and refs that link to each other or in one direction, aptly called pointers. The nodes in a linked list must contain a head, or a first value, a tail, which is the last value in a node, and a pointer, which connects a node's tail to the next node in the linked list. The last node in a linked list, however, will point to `null` to indicate the end of the linked list.

Creating a linked list
It's safe to say that it is unlikely that you'll be asked to create a linked list from scratch at an interview, but I found creating one to be an excellent learning tool. We can create a linked list class that we can instantiate as needed. Since linked lists are sorted, nodes are inserted front to back; the first value inserted in an empty linked list becomes the head and will also be the tail. We can define a linked list's values in a class constructor:

``````class LinkedList {
constructor(value) {
value: value,
next: null
};
this.length = 1;
}
}
// we can now instantiate this linked list as needed
``````

Prepend
Prepending, or inserting a value at the beginning of a linked list, is more optimal in a linked list than it would be in an array, given that adding to a linked list does not require traversal of any kind. There is also no shifting involved as it would be in an array. Prepending a value in a linked list is done in constant time, O(1), as opposed to linear time in an array, O(n). The same goes for appending, or adding a value at the end of a linked list. For an array, both cases require traversing to either end of an array.

To prepend a new node to a linked list we simply need to assign a new node as the head of the given linked list.

``````class MyArray {
constructor(value) {
this.value = value
}
//initialize a method to prepend a node
//our method will receive the node's value
prepend(value) {

//we can either initialize an object or instantiate a node class,
//like we created in the last example
const newNode = new Node(value)

//if there is no head, we can simply I sort the new node as the head of the linked list
if(this.head === null) {
return this;

//have the new node point to the head of the linked list

//reassign the head property to equate to the new node

//increase the size of the linked list
this.length++;

//return the linked list
return this;
}

}

``````

Append
Appending nodes to a linked list is very similar to prepending. Like appending it is done in 0(1) time, since no traversing or shifting of elements is required. We simply reassign the inserted node as the new tail in the linked list and have the former tail point to the inserted node.

``````...
append(value) {
const newNode = new Node(value);
//make the old tail point to the new tail
this.tail.next = newNode;
//make the inserted node the new tail
this.tail = newNode;
//since we need to allocate memory in a linked list, increase the length of it
this.length++;
return this;
}

``````

Insert
Prepending and appending may be simple, but things get a little complicated when we need to insert a node at any other position in a linked list. We can identify the head or the tail of a linked list in O(1) time, but accessing values between the head and the tail requires traversal, an operation that runs in O(n) time. As a reminder, this means that it's time complexity grows relative to the size of the data structure. The bigger the list, the more nodes we need to traverse. After traversal we need to insert our new node and shift the pointers of the adjacent nodes.

``````...
insert(index, value){
//check our parameters
if(index >= this.length) {
return this.append(value);
}

const newNode = new Node(value);
//define the index before our insertion point
const leader = this.traverseToIndex(index-1);
//define the index after the insertion point
const holdingPointer = leader.next;
//make the leader point to the inserted node
//make the inserted node point to the holding node
newNode.next = holdingPointer;
this.length++;
return this;
}
...
``````

Deletion
The last operation I'll go over is deleting a node in a linked list. Unlike deletion in an array, we don't need to traverse through every node to delete a head or tail node, so long as we have a defined value for the node. This makes deletion of a head or tail node in Singly-Linked Lists run in O(1) time. Here's an example of deletion methods, where I've included a third method that deletes any node in a linked list that isn't the head or the tail:

``````//remove the head node of a linked list
return }
return this
}

//removing the tail node of a linked list
removeTail(index) {
//verify parameters
return;
}
//define node before target node
let prevNode = this.head;
//define node after the target node
let tail = this.head.next;
while(tail.next != null) {
let targetNode = this.prevNode.next;
prevNode= tail;
tail = tail.next
return this;
}
}

//removing a node from the linked list that isn't the head or tail
remove(index) {
// Check Parameters
const leader = this.traverseToIndex(index-1);
const unwantedNode = leader.next;
The example above shows us three cases for deleting from a linked list. Deleting a head or tail node is our best case here, having a time complexity of O(1). At worst case, which is deleting any other node, has a time complexity of O(n), where `n` is the number of nodes that must be traversed in the linked list.