Singly Linked List Explained

Learn Singly Linked List Explained with practical examples, explanations, best practices, and real-world coding techniques.

What Is a Singly Linked List?

A singly linked list is a linear data structure made up of nodes. Each node stores a value and a reference to the next node in the list. The final node points to null.

head
 ↓
[10 | next] → [20 | next] → [30 | null]

The list can be traversed in only one direction: from the head toward the end. This is why it is called singly linked.

Structure of a Singly Linked List

Every node in a singly linked list normally contains two parts:

  • Data: the value stored by the node.
  • Next: a reference to the next node.
+-------------+-------------+
|    value    |    next     |
+-------------+-------------+
      10            ↓
                  Node 2

The next reference creates the connection between nodes. Unlike an array, the nodes do not need to be stored next to each other in memory.

Example of a Singly Linked List

Suppose a list contains four values:

10 → 20 → 30 → 40 → null

The first node stores 10 and points to the node containing 20. The second points to 30, the third points to 40, and the final node points to null.

Head and Tail

A singly linked list commonly maintains a reference to the head, which identifies the first node. Some implementations also maintain a tail reference to the final node.

head                         tail
 ↓                             ↓
[10] → [20] → [30] → [40] → null

Keeping a tail reference can make appending a new node at the end an O(1) operation. Without a tail reference, the list usually needs to be traversed to find the final node.

Creating a Singly Linked List Node

In JavaScript, a node can be represented with a class containing a value and a next reference.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

const first = new Node(10);
const second = new Node(20);

first.next = second;

console.log(first.value);
// 10

console.log(first.next.value);
// 20

Assigning first.next = second creates the link between the two nodes.

Building a Singly Linked List

Multiple nodes can be connected by assigning each node's next reference to the following node.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

const first = new Node(10);
const second = new Node(20);
const third = new Node(30);

first.next = second;
second.next = third;

const head = first;

The resulting structure is:

head
 ↓
[10] → [20] → [30] → null

Traversing a Singly Linked List

Traversal starts at the head and follows the next reference until null is reached.

function traverse(head) {
  let current = head;

  while (current !== null) {
    console.log(current.value);
    current = current.next;
  }
}

traverse(head);

// 10
// 20
// 30

The current variable moves through the list without changing the head. This is an important pattern when implementing linked-list algorithms.

Accessing an Element by Position

A singly linked list does not provide direct O(1) access by index. To reach a particular position, the list must be traversed from the head.

function getAt(head, index) {
  let current = head;
  let position = 0;

  while (current !== null) {
    if (position === index) {
      return current.value;
    }

    current = current.next;
    position++;
  }

  return undefined;
}

Accessing the first node is O(1), but accessing a node at a general position is O(n) in the worst case.

Inserting at the Beginning

Inserting at the beginning is one of the main advantages of a singly linked list. The new node points to the current head, and the head is then updated.

Before:

head
 ↓
10 → 20 → 30

Insert 5:

5 → 10 → 20 → 30
↑
head
function insertAtBeginning(head, value) {
  const newNode = new Node(value);

  newNode.next = head;

  return newNode;
}

The insertion itself takes O(1) time.

Inserting at the End

Without a tail reference, inserting at the end requires traversing to the final node first.

function append(head, value) {
  const newNode = new Node(value);

  if (head === null) {
    return newNode;
  }

  let current = head;

  while (current.next !== null) {
    current = current.next;
  }

  current.next = newNode;

  return head;
}

This implementation takes O(n) time because it must find the final node. If the linked list maintains a tail reference, appending can be performed in O(1).

Inserting After a Known Node

If the node where the insertion should occur is already known, a new node can be inserted without traversing the list.

Before:

10 → 20 → 40

Insert 30 after 20:

10 → 20 → 30 → 40
function insertAfter(node, value) {
  if (node === null) {
    return;
  }

  const newNode = new Node(value);

  newNode.next = node.next;
  node.next = newNode;
}

The pointer changes are O(1). Finding the node by value or position may still take O(n).

Deleting the First Node

To remove the first node, move the head to the next node.

Before:

head
 ↓
10 → 20 → 30

After deleting 10:

head
 ↓
20 → 30
function deleteFirst(head) {
  if (head === null) {
    return null;
  }

  return head.next;
}

Deleting the first node takes O(1) time.

Deleting a Node After a Known Node

If the predecessor of the node to remove is already known, the predecessor can point directly to the node after the removed node.

Before:

10 → 20 → 30 → 40

Remove 30:

10 → 20 ─────→ 40
function deleteAfter(node) {
  if (
    node === null ||
    node.next === null
  ) {
    return;
  }

  node.next = node.next.next;
}

The reference update is O(1), although finding the predecessor may require O(n) traversal.

Searching a Singly Linked List

Searching requires checking nodes one by one until the target value is found or the end of the list is reached.

function search(head, target) {
  let current = head;

  while (current !== null) {
    if (current.value === target) {
      return true;
    }

    current = current.next;
  }

  return false;
}

The worst-case time complexity is O(n).

Finding the Length

The length can be calculated by traversing all nodes and counting them.

function getLength(head) {
  let length = 0;
  let current = head;

  while (current !== null) {
    length++;
    current = current.next;
  }

  return length;
}

This operation takes O(n) time unless the linked-list implementation maintains a separate size counter.

Reversing a Singly Linked List

Reversing a singly linked list changes the direction of every next reference.

Before:

10 → 20 → 30 → null

After:

30 → 20 → 10 → null

An iterative solution commonly uses three references: previous, current, and next.

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

  while (current !== null) {
    const next = current.next;

    current.next = previous;
    previous = current;
    current = next;
  }

  return previous;
}

The algorithm runs in O(n) time and uses O(1) auxiliary space.

Finding the Middle Node

The slow and fast pointer technique can find the middle node in one traversal. The slow pointer moves one node at a time, while the fast pointer moves two nodes at a time.

10 → 20 → 30 → 40 → 50
     ↑              ↑
   slow           fast

When fast reaches the end,
slow is near the middle.
function findMiddle(head) {
  let slow = head;
  let fast = head;

  while (
    fast !== null &&
    fast.next !== null
  ) {
    slow = slow.next;
    fast = fast.next.next;
  }

  return slow;
}

This approach takes O(n) time and O(1) auxiliary space.

Detecting a Cycle

A normal singly linked list ends at null. A cycle occurs when a node eventually points back to an earlier node.

10 → 20 → 30 → 40
          ↑         ↓
          └─────────┘

Floyd's cycle detection algorithm, also known as the tortoise and hare algorithm, uses slow and fast pointers.

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;
}

If the two pointers meet, a cycle exists. The algorithm runs in O(n) time and uses O(1) auxiliary space.

Singly Linked List Complexity

Operation Time Condition
Access first node O(1) Head is available
Access by index O(n) Sequential traversal
Search O(n) Value may be anywhere
Insert at head O(1) Head reference available
Insert after known node O(1) Target node already known
Append without tail O(n) Must find final node
Append with tail O(1) Tail reference maintained
Delete head O(1) Head reference available
Reverse O(n) Every node is processed

Singly Linked List vs Array

The main trade-off is between random access and efficient local structural updates.

Feature Array Singly Linked List
Index access O(1) O(n)
Search O(n) for unsorted values O(n)
Insert at beginning O(n) O(1)
Delete at beginning O(n) in a typical array O(1)
Memory overhead Lower per element Extra reference per node

Advantages of a Singly Linked List

  • Insertion at the head can be performed in O(1).
  • Deletion at the head can be performed in O(1).
  • Nodes do not require contiguous storage.
  • The structure can grow and shrink dynamically.
  • It is a useful foundation for stacks, queues, and other data structures.
  • Local insertion and deletion can be efficient when the relevant node is already known.

Limitations of a Singly Linked List

  • No direct random access by index.
  • Searching generally takes O(n).
  • Backward traversal is not supported.
  • Each node requires additional memory for the next reference.
  • Appending without a tail reference requires traversal.
  • Reference manipulation can introduce bugs such as lost nodes or accidental cycles.

Common Applications

Singly linked lists are often used as building blocks rather than as the primary collection for every application.

  • Implementing stacks.
  • Implementing queues.
  • Hash-table chaining.
  • Graph adjacency structures.
  • Maintaining sequences where frequent local insertion and deletion are required.
  • Educational implementations of pointer-based data structures.

Complete Singly Linked List Implementation

The following implementation provides common operations for a singly linked list.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class SinglyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  prepend(value) {
    const newNode = new Node(value);

    newNode.next = this.head;
    this.head = newNode;

    if (this.tail === null) {
      this.tail = newNode;
    }

    this.size++;
  }

  append(value) {
    const newNode = new Node(value);

    if (this.head === null) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }

    this.size++;
  }

  contains(value) {
    let current = this.head;

    while (current !== null) {
      if (current.value === value) {
        return true;
      }

      current = current.next;
    }

    return false;
  }

  deleteFirst() {
    if (this.head === null) {
      return undefined;
    }

    const value = this.head.value;

    this.head = this.head.next;
    this.size--;

    if (this.head === null) {
      this.tail = null;
    }

    return value;
  }

  print() {
    const values = [];
    let current = this.head;

    while (current !== null) {
      values.push(current.value);
      current = current.next;
    }

    console.log(
      values.length > 0
        ? values.join(" → ")
        : "Empty list"
    );
  }
}

const list = new SinglyLinkedList();

list.append(20);
list.append(30);
list.prepend(10);

list.print();
// 10 → 20 → 30

console.log(list.contains(20));
// true

console.log(list.deleteFirst());
// 10

list.print();
// 20 → 30

Common Singly Linked List Mistakes

Overwriting the Next Reference

During insertion or reversal, changing current.next before saving the original next node can make the rest of the list unreachable.

Forgetting the Empty List

Always consider the case where the head is null, especially during insertion, deletion, and traversal.

Not Updating the Tail

If an implementation maintains a tail reference, deleting the final node or adding the first node must keep the tail consistent with the list.

Confusing O(1) Insertion with O(1) Search

Inserting after a known node is O(1), but finding that node may require O(n) traversal. The complete operation must account for both steps.

Creating an Accidental Cycle

Incorrectly assigning a next reference can cause a node to point back to an earlier node. Traversal may then never reach null.

Singly Linked List Interview Patterns

Singly linked lists appear frequently in coding interviews because many problems can be solved by carefully manipulating node references.

  • Reverse a linked list.
  • Find the middle node using slow and fast pointers.
  • Detect a cycle using Floyd's algorithm.
  • Find the start of a cycle.
  • Merge two sorted linked lists.
  • Remove the nth node from the end.
  • Check whether a linked list is a palindrome.
  • Find the intersection of two linked lists.

Key Takeaways

  • A singly linked list consists of nodes connected through next references.
  • Each node stores a value and a reference to the next node.
  • The head identifies the first node.
  • The final node normally points to null.
  • Traversal proceeds in one direction from head to tail.
  • Random access by index takes O(n).
  • Insertion and deletion at the head take O(1).
  • Insertion after a known node takes O(1), excluding the cost of finding that node.
  • A tail reference can make appending O(1).
  • Slow and fast pointers are useful for middle-node and cycle problems.
  • Careful reference management is essential when modifying a linked list.

Code Examples

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class SinglyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  prepend(value) {
    const newNode = new Node(value);

    newNode.next = this.head;
    this.head = newNode;

    if (this.tail === null) {
      this.tail = newNode;
    }

    this.size++;
  }

  append(value) {
    const newNode = new Node(value);

    if (this.head === null) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }

    this.size++;
  }

  print() {
    const values = [];
    let current = this.head;

    while (current !== null) {
      values.push(current.value);
      current = current.next;
    }

    console.log(values.join(" → "));
  }
}

const list = new SinglyLinkedList();

list.append(20);
list.append(30);
list.prepend(10);

list.print();
// 10 → 20 → 30
    
function reverse(head) {
  let previous = null;
  let current = head;

  while (current !== null) {
    const next = current.next;

    current.next = previous;

    previous = current;
    current = next;
  }

  return previous;
}

const head = {
  value: 10,
  next: {
    value: 20,
    next: {
      value: 30,
      next: null
    }
  }
};

const reversed = reverse(head);

let current = reversed;

while (current !== null) {
  console.log(current.value);
  current = current.next;
}

// 30
// 20
// 10
    
function findMiddle(head) {
  let slow = head;
  let fast = head;

  while (
    fast !== null &&
    fast.next !== null
  ) {
    slow = slow.next;
    fast = fast.next.next;
  }

  return slow;
}

const head = {
  value: 10,
  next: {
    value: 20,
    next: {
      value: 30,
      next: {
        value: 40,
        next: {
          value: 50,
          next: null
        }
      }
    }
  }
};

const middle = findMiddle(head);

console.log(middle.value);
// 30
    

Pro Tip

For singly linked-list problems, keep track of the references before changing them. The most useful pattern is previous → current → next, especially when reversing or deleting nodes.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.