Getting Started with Linked Lists: A Practical Guide with Examples

Elif İrem Kara Kadyrov
10 min readJan 17, 2023

--

Linked lists might not sound like the most exciting topic in the world, but they are a crucial part of the programmer’s toolkit.

In an interview, you can expect to be tested on your understanding of linked lists and how to use them effectively. Whether you’re asked to implement a linked list from scratch or explain how you would use one to solve a particular problem... So don’t sleep on linked lists — they may not be the flashiest topic, but they are an essential part of the coding journey.

What is a Linked List?

A linked list is a linear data structure that stores a sequence of elements. Each element (will be referred to as a node), consists of a value and a reference to the next item in the list. The items in a linked list are connected using pointers.

The first element in a linked list is called the head, and the last element is called the tail. The head is the starting point for traversing the list, and the tail is the endpoint.

Implementation of a Linked List

In JavaScript, a linked list can be implemented using objects and pointers. Each object represents a node in the list, and it has two properties: a “value” property to store the node’s value and a “next” property to store a reference to the next node in the list.

Here is a simple example of a linked list with a method that adds an element to the tail in JavaScript:

class LinkedList {
constructor() {
this.head = null;
this.tail = null;
}

addToTail(value) {
const newNode = { value, next: null };
if (!this.head) {
this.head = newNode;
} else {
this.tail.next = newNode;// adding new node always to the end.
}
this.tail = newNode;
}
}

This code block can be used like:

const list = new LinkedList();
list.addToTail(1);
list.addToTail(2);
list.addToTail(3);

This particular code will create a linked list with three nodes. The head of the list object will point to the first node in the list (with a value of 1), and the tail will point to the last node in the list (with a value of 3). The output we can observe in each step will be:

LinkedList { head: null, tail: null }
LinkedList {
head: { value: 1, next: null },
tail: { value: 1, next: null }
}
LinkedList {
head: { value: 1, next: { value: 2, next: null } },
tail: { value: 2, next: null }
}
LinkedList {
head: { value: 1, next: { value: 2, next: [Object] } },
tail: { value: 3, next: null }
}

Problems from LeetCode

I have selected a group of problems that involve the use and understanding of linked lists. Before attempting to solve these problems, let’s take some time to understand the questions and what is being asked of us. Once we have a good grasp of the problems, we can then develop solutions using linked lists.

206. Reverse Linked List

This is a common question that developers might encounter while preparing for an interview.

The question: Given a singly linked list(head), reverse the list, and return the reversed list.

Input: head = [1,2,3,4,5]
Output: [5,4,3,2,1]

Input: head = [1,2]
Output: [2,1]

Input: head = []
Output: []

In-place reversal approach: This method involves changing the next pointers of each node in the list to point to the previous node, effectively reversing the direction of the list.

function reverseLinkedList(head) {
let previous = null;
let current = head;

while (current) {
let next = current.next; // Save the next node
current.next = previous; // Reverse the current node's pointer
previous = current; // Move the previous node to the current node
current = next; // Move to the next node
}
return previous; // the head of the reversed list is the previous node
}

Explanation: Consider the following linked list:

A -> B -> C -> D -> E

The in-place reversal method works by iterating through the list and reversing the next pointer of each node so that it points to the previous node.

Initial State

current = A previous = null
A -> B -> C -> D -> E

First Iteration

current = B previous = A
A <- B -> C -> D -> E

Second Iteration

current = C previous = B
A <- B <- C -> D -> E

Third Iteration

current = D previous = C
A <- B <- C <- D -> E

Forth Iteration

current = E previous = D
A <- B <- C <- D <- E

Last Iteration

current = null previous = E
A <- B <- C <- D <- E

This method changes the direction of pointers in a linked list, making the first element the last and the last element the first by iterating through the list. It has O(n) time complexity and O(1) space complexity.

Array approach: You can also convert the linked list into an array, reverse the array, and then rebuild the linked list using the reversed array.

function reverseLinkedList(head) {
let current = head;
let arr = [];
while (current) {
arr.push(current);
current = current.next;
}
arr.reverse();
for (let i = 0; i < arr.length - 1; i++) {
arr[i].next = arr[i + 1];
}
arr[arr.length - 1].next = null;
return arr[0];
}

There is a trade-off in this approach in terms of space complexity which is O(n) which could be an issue if the linked list is extensive. The time complexity O(n).

Recursion Approach: The way it works is by reversing the order of the elements in smaller parts of the list, and then making changes to the current element so it points to the previous element.

function reverseLinkedList(current, previous = null) {
if (current == null) {
return previous;
}
let next = current.next;
current.next = previous;
return reverseLinkedList(next, current);
}

Like the array method, this also has an O(n) space and O(n) time complexity. The space complexity of this approach is O(n) because, for each recursive call, a new function call is added to the call stack and these function calls take up a certain amount of memory. Since the list has n nodes, the maximum depth of the call stack will be n.

19. Remove Nth Node From End of List

The question: Given the head of a linked list, remove the nth node from the end of the list and return its head.

The length approach: First, we can find the length of the linked list, and then use that information to determine which node to remove.

function removeNthFromEnd(head, n) {
let length = 0;
let current = head;
// Find the length of the linked list
while (current) {
length++;
current = current.next;
}
// Find the node to remove
current = head;
let previous = null;
for (let i = 0; i < length - n; i++) {
//the index of the node (length-n)
previous = current;
current = current.next;
}
// remove the nth node from the end
if(previous) previous.next = current.next;
else head = head.next;
return head;
}

This code first uses a while loop to find the length of the linked list. Then it uses another loop to traverse the list again, this time stopping at the nth node from the end, where it updates the next pointer of the previous node to skip the current node, effectively removing it from the list.

This approach has a time complexity of O(n) and a space complexity of O(1) and is easier to understand.

The sliding-window approach: This approach is similar to the two-pointers approach, but it uses a window of size n to keep track of the nth node from the end of the list.

The first node in the window is the nth node from the end of the list, and the second node is the head of the list. The code uses two pointers which are initially set to a dummy node.

function removeNthFromEnd(head, n) {
let dummy = new ListNode(0);
dummy.next = head;
let first = dummy;
let second = dummy;

// Iterate first pointer n steps ahead
for (let i = 0; i < n; i++) {
first = first.next;
}

// Iterate both pointers until the first pointer reaches to the end
while (first.next != null) {
first = first.next;
second = second.next;
}

// remove the nth node from the end
second.next = second.next.next;

//return the new head
return dummy.next;
}

The dummy node is added at the start of the list to handle the case where the head of the list is removed. The first pointer is placed at the nth node from the end which is the start of the window in the beginning. After that, both pointers are moved together until the first reaches the end of the list. The second pointer will refer to the end of the window (the node that comes before the nth node from the end).

This approach has O(n) time complexity, because we have to iterate through the list once, and the space complexity is O(1).

160. The intersection of Two Linked Lists

The question: Given the heads of two singly linked-lists headA and headB, return the node at which the two lists intersect. If there is no intersection, return null.

Output: Intersected at ‘c1’.

The approach: We can use two pointers to keep track of the node changes.

Assume we have two pointers that are initially set to the heads of the two linked lists.

function getIntersectionNode(headA, headB) {
let p1 = headA, p2 = headB;
while (p1 !== p2) {
p1 = p1 ? p1.next : headB;
p2 = p2 ? p2.next : headA;
}
return p1;
}

The while loop runs as long as p1 and p2 are not equal. If they are identical, they have reached the intersection node and the function returns the node.

On each iteration of the while loop, if p1 is not null, it is set to the next node in the list. Otherwise, it means that it has reached the end of the first list, so it is set to the head of the second list (headB). This code is used to ensure that both pointers traverse the same number of nodes. When one pointer reaches the end of its list, it is assigned the head of the other list so it can continue traversing.

21. Merge Two Sorted Lists

The question: You are given the heads of two sorted linked lists head1 and head2. Merge the two lists into one sorted list. Return the head of the merged linked list.

The approach: It can be easier to use a recursive function for this question.

var mergeTwoLists = function(head1, head2) {
if (!head1) {
return head2;
} else if (!head2) {
return head1;
}
if(head1.val < head2.val) {
head1.next = mergeTwoLists(head1.next, head2);
return head1;
} else {
head2.next = mergeTwoLists(head1, head2.next);
return head2;
}
};

If there is no element in one of the two lists, we should return the none-null sorted list. This is the base case for the recursion.

If none of the heads are null, we will compare their values. If the value of head1 is less than head2, the next node of head1 is the result of merging the next node of both lists. It then returns head1. This logic works for both lists.

This recursive approach continues until the end of both lists is reached and the final result is a fully merged and sorted linked list.

The time complexity of this solution is O(n log(n)), n is the total number of nodes, and space complexity is O(log(n)).

141. Linked List Cycle

The question: Given the head of a linked list, determine if the linked list has a cycle in it.

The two-pointers approach: We can have two-pointers that checks one and two step forward of the list. If the two-pointers ever point to the same node, it means there is a cycle in the list.

function hasCycle(head) {
let first = head;
let second = head;

while (second && second.next) {
first = first.next;
second = second.next.next;
if (first === second) {
return true;
}
}

return false;
}

The time complexity of this algorithm is O(n), where n is the number of nodes in the linked list. The space complexity is O(1) because it is constant and does not grow with input.

The set approach: This method consists of keeping track of the visited nodes as traversing. If we can detect we have visited a node before, we can say that there is a cycle.

function hasCycle(head) {
let set = new Set();
let current = head;

while (current) {
if (set.has(current)) {
return true;
}
set.add(current);
current = current.next;
}

return false;
}

If the current node is already in the set, it means there is a cycle in the list. If the current node is not in the set, add it to the set and move on to the next node. If we are at the end of the list without a cycle, return false.

This approach also has O(n) time complexity, but the space complexity is O(n) because it uses a set to store the visited nodes. Depending on the constraints(containing smaller-sized lists), both approaches could be acceptable.

Conclusion

We can say that there is no ultimate solution to any problem. There will be different approaches that come to mind. As long as you are aware of the constraints, trade-offs between the methods, and how your solution can be improved, you are doing great!

--

--