Implement Queue Using a Linked List

Learn Implement Queue Using a Linked List with practical examples, explanations, best practices, and real-world coding techniques.

Implementing a Queue with a Linked List

A queue can be implemented using a linked list by maintaining references to the front and rear nodes. The queue follows the FIFO (First In, First Out) principle, so new elements are added at the rear and elements are removed from the front.

Unlike an array-based queue, a linked-list implementation does not require elements to be shifted when the front element is removed. By keeping both front and rear references, enqueue and dequeue operations can be performed efficiently.

Front                                  Rear
  ↓                                      ↓
[10 | •] → [20 | •] → [30 | null]
  ↑                                      ↑
Remove                                   Add

Why Use a Linked List for a Queue?

A linked list is useful for queue implementation because inserting a node at the rear and removing a node from the front can both be performed without moving the remaining elements.

The implementation keeps track of two references:

  • Front: Points to the node that will be removed next.
  • Rear: Points to the most recently added node.

When the queue is empty, both references are null.

Empty queue:

front = null
rear  = null

The Node Structure

Each element in the linked-list queue is represented by a node. A node contains the value and a reference to the next node in the sequence.

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

The next property connects one node to the next node in the queue. The last node always points to null.

Structure of a Linked-List Queue

Suppose the queue contains 10, 20, and 30. The linked-list representation looks like this:

front
  ↓
[10 | •] → [20 | •] → [30 | null]
                              ↑
                             rear

The front node contains the first element to be processed, while the rear node represents the location where the next element will be attached.

Queue Class

A queue class can maintain the front node, rear node, and optionally the number of elements. Keeping a count makes the size operation constant time.

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
    this.count = 0;
  }
}

Enqueue in a Linked-List Queue

The enqueue operation adds a new node to the rear of the queue. There are two situations to handle: adding to an empty queue and adding to a non-empty queue.

Adding to an Empty Queue

When the queue is empty, there is no existing rear node. The newly created node therefore becomes both the front and rear.

Before:

front = null
rear  = null

enqueue(10)

After:

front → [10 | null] ← rear

Adding to a Non-Empty Queue

When the queue already contains elements, the new node is connected to the current rear node. The rear reference is then moved to the new node.

Before:

front → [10 | •] → [20 | null] ← rear

enqueue(30)

After:

front → [10 | •] → [20 | •] → [30 | null]
                                      ↑
                                     rear

Enqueue Implementation

enqueue(value) {
  const node = new Node(value);

  if (this.rear === null) {
    this.front = node;
    this.rear = node;
  } else {
    this.rear.next = node;
    this.rear = node;
  }

  this.count++;
}

Because the rear node is already known, no traversal is required to find the insertion point. Therefore, enqueue can be performed in O(1) time.

Dequeue in a Linked-List Queue

The dequeue operation removes the node at the front of the queue. Since the front node is directly available through the front reference, the operation does not require traversal.

Before:

front → [10 | •] → [20 | •] → [30 | null]
                                      ↑
                                     rear

dequeue()

After:

front → [20 | •] → [30 | null]
                            ↑
                           rear

Removed: 10

The front reference is simply moved to the next node.

dequeue() {
  if (this.front === null) {
    return null;
  }

  const value = this.front.value;
  this.front = this.front.next;
  this.count--;

  return value;
}

Handling the Last Element

A special case occurs when the queue contains only one node. After removing that node, both front and rear must become null.

Before:

front → [10 | null] ← rear

dequeue()

After:

front = null
rear  = null

If the rear reference is not updated when the final element is removed, it may continue pointing to a node that is no longer part of the queue. This can cause incorrect behavior during a later enqueue.

Complete Dequeue Implementation

A robust dequeue operation handles both the normal case and the case where the removed node was the final node in the queue.

dequeue() {
  if (this.front === null) {
    return null;
  }

  const value = this.front.value;
  this.front = this.front.next;
  this.count--;

  if (this.front === null) {
    this.rear = null;
  }

  return value;
}

Peek Operation

The peek operation returns the value at the front without removing the node. It can directly access the front reference.

peek() {
  return this.front === null
    ? null
    : this.front.value;
}

Since the front node is directly available, peek takes O(1) time.

Checking Whether the Queue Is Empty

A linked-list queue is empty when its front reference is null. Because a queue maintains both front and rear references, a consistent implementation should have both references set to null when the queue contains no elements.

isEmpty() {
  return this.front === null;
}

Getting the Queue Size

If the implementation maintains a count property, the number of elements can be returned directly.

size() {
  return this.count;
}

Without a stored count, calculating the size would require traversing the linked list, which takes O(n) time. Maintaining a count makes the operation O(1).

Inspecting the Rear Element

Because the queue maintains a rear reference, the last element can also be inspected directly. This is useful when the implementation provides a rear or back operation.

rearValue() {
  return this.rear === null
    ? null
    : this.rear.value;
}

Complete Linked-List Queue

Combining the node structure with front and rear references produces a complete linked-list queue implementation.

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

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
    this.count = 0;
  }

  enqueue(value) {
    const node = new Node(value);

    if (this.rear === null) {
      this.front = node;
      this.rear = node;
    } else {
      this.rear.next = node;
      this.rear = node;
    }

    this.count++;
  }

  dequeue() {
    if (this.front === null) {
      return null;
    }

    const value = this.front.value;
    this.front = this.front.next;
    this.count--;

    if (this.front === null) {
      this.rear = null;
    }

    return value;
  }

  peek() {
    return this.front === null
      ? null
      : this.front.value;
  }

  isEmpty() {
    return this.front === null;
  }

  size() {
    return this.count;
  }

  rearValue() {
    return this.rear === null
      ? null
      : this.rear.value;
  }
}

const queue = new Queue();

queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);

console.log(queue.peek());      // 10
console.log(queue.rearValue()); // 30
console.log(queue.dequeue());   // 10
console.log(queue.dequeue());   // 20
console.log(queue.size());      // 1
console.log(queue.peek());      // 30

Step-by-Step Example

Consider adding three values to an empty linked-list queue.

Step 1: Enqueue 10

front
  ↓
[10 | null]
  ↑
rear

Step 2: Enqueue 20

front
  ↓
[10 | •] → [20 | null]
                   ↑
                  rear

Step 3: Enqueue 30

front
  ↓
[10 | •] → [20 | •] → [30 | null]
                              ↑
                             rear

Step 4: Dequeue

Removed: 10

front
  ↓
[20 | •] → [30 | null]
                   ↑
                  rear

The first node is removed simply by moving the front reference from the old node to its next node. No other nodes need to be moved.

Linked List vs Array Queue

Both arrays and linked lists can implement queues efficiently when the implementation is designed correctly. The main differences are in storage behavior, memory management, and how the queue reuses space.

Feature Array Queue Linked-List Queue
Enqueue O(1) amortized with append O(1) with rear reference
Dequeue O(1) with front index O(1)
Memory layout Array storage Separate linked nodes
Capacity May require resizing or fixed capacity Grows node by node
Memory overhead Generally lower per element Extra reference per node
Storage reuse Requires indexing, compaction, or circular design Removed nodes no longer belong to the queue

Complexity Analysis

With both front and rear references, the main queue operations can be performed without traversing the linked list.

Operation Time Complexity Reason
Enqueue O(1) Rear node is directly available
Dequeue O(1) Front node is directly available
Peek O(1) Reads the front node
IsEmpty O(1) Checks the front reference
Size O(1)* Uses a maintained count
Search O(n) Requires traversal

*Size is O(1) when the implementation maintains a count. Traversing the linked list to calculate the size would take O(n).

Memory Considerations

A linked-list queue grows dynamically as nodes are created. It does not require a contiguous block of storage for all queue elements, but every node needs additional memory for its next reference.

This means the choice between an array and linked list is not only about operation complexity. Memory overhead, allocation behavior, expected queue size, and the surrounding application can also influence the implementation choice.

Common Mistakes

  • Traversing the entire linked list during every enqueue instead of maintaining a rear reference.
  • Forgetting to update the rear when the last node is dequeued.
  • Not handling dequeue on an empty queue.
  • Updating the rear but forgetting to connect the old rear to the new node.
  • Calculating size by traversal when a maintained count would be more efficient.
  • Confusing the front node with the rear node.

Practical Example: Processing Jobs

A linked-list queue can represent jobs waiting for a worker. Each new job is attached to the rear, while the worker removes jobs from the front.

const queue = new Queue();

queue.enqueue("Send email");
queue.enqueue("Generate invoice");
queue.enqueue("Create backup");

while (!queue.isEmpty()) {
  console.log("Processing:", queue.dequeue());
}

// Processing: Send email
// Processing: Generate invoice
// Processing: Create backup

This pattern is useful whenever work arrives continuously but should be processed in the order in which it was received.

When to Use a Linked-List Queue

A linked-list implementation can be useful when the queue needs dynamic growth and efficient insertion and removal without maintaining a large contiguous array.

  • The queue size can change significantly during execution.
  • Constant-time enqueue and dequeue are required.
  • There is no need for direct indexed access to queue elements.
  • The application can tolerate the memory overhead of node references.
  • The implementation benefits from explicit front and rear node management.

Key Takeaways

  • A linked-list queue uses nodes connected through next references.
  • The front reference identifies the next node to be removed.
  • The rear reference identifies where the next node should be added.
  • Enqueue adds a node at the rear in O(1) time when a rear reference is maintained.
  • Dequeue removes a node from the front in O(1) time.
  • When the last node is removed, both front and rear should become null.
  • A maintained count allows the size operation to run in O(1) time.
  • Linked-list queues grow dynamically but require additional memory for node references.

Code Examples

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

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
    this.count = 0;
  }

  enqueue(value) {
    const node = new Node(value);

    if (this.rear === null) {
      this.front = node;
      this.rear = node;
    } else {
      this.rear.next = node;
      this.rear = node;
    }

    this.count++;
  }

  dequeue() {
    if (this.front === null) {
      return null;
    }

    const value = this.front.value;
    this.front = this.front.next;
    this.count--;

    if (this.front === null) {
      this.rear = null;
    }

    return value;
  }

  peek() {
    return this.front === null
      ? null
      : this.front.value;
  }

  isEmpty() {
    return this.front === null;
  }

  size() {
    return this.count;
  }
}

const queue = new Queue();

queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);

console.log(queue.peek());    // 10
console.log(queue.dequeue()); // 10
console.log(queue.dequeue()); // 20
console.log(queue.size());    // 1
    
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
  }

  enqueue(value) {
    const node = new Node(value);

    if (this.rear === null) {
      this.front = node;
      this.rear = node;
      return;
    }

    this.rear.next = node;
    this.rear = node;
  }

  dequeue() {
    if (this.front === null) {
      return null;
    }

    const value = this.front.value;
    this.front = this.front.next;

    if (this.front === null) {
      this.rear = null;
    }

    return value;
  }

  peek() {
    return this.front?.value ?? null;
  }

  rearValue() {
    return this.rear?.value ?? null;
  }
}

const queue = new Queue();

queue.enqueue("A");
queue.enqueue("B");
queue.enqueue("C");

console.log("Front:", queue.peek());
console.log("Rear:", queue.rearValue());
console.log("Removed:", queue.dequeue());
    
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
  }

  enqueue(value) {
    const node = new Node(value);

    if (this.rear === null) {
      this.front = node;
      this.rear = node;
    } else {
      this.rear.next = node;
      this.rear = node;
    }
  }

  dequeue() {
    if (this.front === null) {
      return null;
    }

    const value = this.front.value;
    this.front = this.front.next;

    if (this.front === null) {
      this.rear = null;
    }

    return value;
  }

  isEmpty() {
    return this.front === null;
  }
}

const jobs = new Queue();

jobs.enqueue("Send email");
jobs.enqueue("Generate report");
jobs.enqueue("Create backup");

while (!jobs.isEmpty()) {
  console.log("Processing:", jobs.dequeue());
}
    

Pro Tip

The most important detail in a linked-list queue is maintaining both front and rear references. Without a rear reference, enqueue may require traversing the entire list, turning an otherwise O(1) operation into O(n).

🎨 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.