Introducing linked list

Linked lists are a fundamental data structure in computer science. They are often used to represent a sequence of elements, where each element is linked to the next one. In this post, we'll introduce linked lists and explore some algorithm questions related to them.

cat that is stepping on a chain

What is a Linked List?

A linked list is a data structure that comprises a sequence of nodes, where each node contains a data element and a reference (or "link") to the next node in the sequence. The first node in the sequence is known as the "head" of the linked list, and the last node is known as the "tail". Here's a simple example of a linked list:

Head -> Node1 -> Node2 -> Node3 -> Tail

In this example, each node contains a data element (e.g., a number or a string) and a reference to the next node in the sequence. The "head" node contains the first element in the list, and the "tail" node is the last element in the list.

class Node {
  val: number
  next: ListNode | null
  constructor(val?: number, next?: ListNode | null) {
    this.val = val === undefined ? 0 : val
    this.next = next === undefined ? null : next
  }
}

One of the main benefits of linked lists is that they can be easily modified by changing the links between nodes. For example, we can insert a new node into a linked list by creating a new node, setting its reference to the next node, and updating the reference of the previous node to point to the new node. Similarly, we can delete a node from a linked list by updating the reference of the previous node to point to the next node (and optionally freeing the memory used by the deleted node).

Algorithm Questions

Now that we have introduced linked lists, let's explore some algorithm questions related to them. In particular, we'll examine three questions: reversing a linked list, finding the middle node of a linked list, and detecting a cycle in a linked list.

Reversing a Linked List

One common question regarding linked lists is how to reverse a linked list. In other words, given a linked list, how can we reverse the order of its nodes? Here's an example:

Input: Head -> Node1 -> Node2 -> Node3 -> Tail
Output: Tail -> Node3 -> Node2 -> Node1 -> Head

There are several ways to solve this problem, but one common approach is to use a "three-pointer" technique. Here's how it works:

Initialise three pointers: prev (initialised to null), curr (initialised to the head of the linked list), and next (initialised to the second node in the linked list).

While curr is not null, do the following:

  1. Set next to the next node in the linked list.
  2. Set the next reference of curr to prev.
  3. Set prev to curr.
  4. Set curr to next.

Set the head of the linked list to prev.

Here's the JavaScript code that implements this algorithm:

function reverseLinkedList(head) {
  let prev = null
  let curr = head
  while (curr !== null) {
    let next = curr.next
    curr.next = prev
    prev = curr
    curr = next
  }
  return prev
}

This algorithm has a time complexity of O(n), where n is the length of the linked list (since we need to visit each node once), and a space complexity of O(1) (since we only need to use a constant amount of extra space).

Finding the Middle Node

Given a linked list, we can find its middle node by using the "slow-fast" pointer technique. The idea is to use two pointers: a "slow" pointer that moves one node at a time, and a "fast" pointer that moves two nodes at a time. When the fast pointer reaches the end of the list (i.e., its next node is null), the slow pointer will be pointing to the middle node (or the node just before the middle node if the length of the list is even). Here's an example:

Input: Head -> Node1 -> Node2 -> Node3 -> Node4 -> Tail
Output: Node3

Here's the JavaScript code that implements this algorithm:

function findMiddleNode(head) {
  let slow = head
  let fast = head
  while (fast !== null && fast.next !== null) {
    slow = slow.next
    fast = fast.next.next
  }
  return slow
}

This algorithm has a time complexity of O(n), where n is the length of the linked list (since we need to visit each node once), and a space complexity of O(1) (since we only need to use a constant amount of extra space).

Detecting a Cycle

Finally, another common question involving linked lists is how to detect whether a linked list contains a cycle (i.e., a loop where a node points to a previous node in the list). Here's an example:

Input: Head -> Node1 -> Node2 -> Node3 -> Node4 -> Node2
Output: true

To detect a cycle in a linked list, we can use the "slow-fast" pointer technique again. We start by initialising two pointers: a "slow" pointer that moves one node at a time, and a "fast" pointer that moves two nodes at a time. If there is a cycle in the list, the fast pointer will eventually "catch up" to the slow pointer (since it is moving twice as fast). If there is no cycle, the fast pointer will reach the end of the list (i.e., its next node is null) and we can return false.

Here's the JavaScript code that implements this algorithm:

function hasCycle(head) {
  let slow = head
  let fast = head
  while (fast !== null && fast.next !== null) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) {
      return true
    }
  }
  return false
}

This algorithm has a time complexity of O(n), where n is the length of the linked list (since we need to visit each node once), and a space complexity of O(1) (since we only need to use a constant amount of extra space).