Queue Operations: Enqueue and Dequeue

Learn Queue Operations: Enqueue and Dequeue with practical examples, explanations, best practices, and real-world coding techniques.

Understanding Queue Operations

A queue is a linear data structure that follows the FIFO (First In, First Out) principle. Queue operations are designed around two ends: the front, where elements are removed or inspected, and the rear, where new elements are added.

Understanding these operations is important because queues are used in task scheduling, request processing, breadth-first search, message processing, print queues, and many other systems where items need to be handled in an ordered sequence.

Front                                      Rear
  ↓                                          ↓
[ 10 ] → [ 20 ] → [ 30 ] → [ 40 ]
  ↑                                          ↑
Dequeue                                   Enqueue

Core Operations of a Queue

The main queue operations provide ways to insert, remove, inspect, and manage elements without breaking FIFO ordering.

Operation Purpose Typical Complexity
Enqueue Adds an element to the rear O(1)
Dequeue Removes the front element O(1)
Peek Reads the front element O(1)
IsEmpty Checks whether the queue has elements O(1)
Size Returns the number of elements O(1)

1. Enqueue Operation

Enqueue adds a new element to the rear of the queue. Existing elements are not reordered, so the element that was already waiting remains ahead of the newly added element.

Queue before:
Front → [10] [20] [30] ← Rear

Enqueue(40)

Queue after:
Front → [10] [20] [30] [40] ← Rear

If the queue is empty, the first inserted element becomes both the front and rear element.

Empty queue:
[ ]

Enqueue(10)

Front → [10] ← Rear

2. Dequeue Operation

Dequeue removes the element at the front of the queue. This operation is what maintains the FIFO behavior of the data structure.

Queue:
Front → [10] [20] [30] ← Rear

Dequeue()

Removed: 10

Queue:
Front → [20] [30] ← Rear

Attempting to dequeue from an empty queue is commonly called an underflow condition. An implementation can handle it by returning a special value, throwing an exception, or using another error-handling strategy.

3. Peek Operation

The peek operation returns the front element without removing it. It is useful when an application needs to know what will be processed next.

Queue:
Front → [25] [40] [55] ← Rear

Peek()

Result: 25

Queue remains:
Front → [25] [40] [55] ← Rear

Unlike dequeue, peek does not change the number or order of elements in the queue.

4. IsEmpty Operation

IsEmpty checks whether a queue currently contains any elements. This operation is especially useful before performing dequeue or peek operations.

Queue:
[10] [20]

isEmpty() → false

After removing all elements:

[ ]

isEmpty() → true

A good queue implementation should define clearly what happens when operations such as dequeue or peek are requested while the queue is empty.

5. Size Operation

The size operation returns the number of elements currently present in the queue. Maintaining a separate counter can make this operation constant time.

Queue:
[10] [20] [30] [40]

size() → 4

If an implementation calculates the size by traversing every stored element, the operation may instead take O(n). Therefore, complexity depends on the underlying implementation.

6. Front Operation

Some queue implementations expose a front operation that returns the element currently at the front. Conceptually, it is similar to peek because it observes the next element without removing it.

Queue:
Front → [100] [200] [300] ← Rear

front() → 100

7. Rear Operation

The rear operation returns the element currently at the back of the queue. It allows an implementation or application to inspect the most recently added element without removing it.

Queue:
Front → [100] [200] [300] ← Rear

rear() → 300

Queue Operation Sequence

Queue operations become easier to understand when several operations are performed consecutively. Consider the following sequence:

enqueue(10)
enqueue(20)
enqueue(30)

Queue:
Front → [10] [20] [30] ← Rear

dequeue()
Removed: 10

Queue:
Front → [20] [30] ← Rear

enqueue(40)

Queue:
Front → [20] [30] [40] ← Rear

peek()
Result: 20

Notice that adding 40 does not make it the next element to be processed. The existing element 20 remains at the front because it entered the queue earlier.

Implementing Queue Operations with an Array

A simple JavaScript array can store queue elements. However, using shift() for every dequeue can be inefficient for large queues because the remaining elements may need to be reindexed.

A more efficient approach is to maintain a front index. New elements are added with push(), while dequeue advances the front index instead of physically removing the first array element.

const queue = [];
let front = 0;

function enqueue(value) {
  queue.push(value);
}

function dequeue() {
  if (front >= queue.length) {
    return null;
  }

  return queue[front++];
}

function peek() {
  return front < queue.length
    ? queue[front]
    : null;
}

function isEmpty() {
  return front >= queue.length;
}

function size() {
  return queue.length - front;
}

enqueue(10);
enqueue(20);
enqueue(30);

console.log(peek());     // 10
console.log(dequeue());  // 10
console.log(size());     // 2
console.log(isEmpty());  // false

Why Avoid Repeated Array Shift?

The following implementation is easy to write:

const queue = [];

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

const value = queue.shift();

console.log(value); // 10

For small examples this is perfectly understandable. However, repeatedly calling shift() on a large array can require work proportional to the number of remaining elements. If a queue is heavily used, maintaining a front index or using a dedicated queue implementation can avoid this repeated shifting.

Queue Using a Linked List

A linked list can also implement a queue efficiently when the implementation maintains references to both the front and rear nodes. Enqueue adds a node at the rear, while dequeue removes the node at the front.

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

With front and rear references, both enqueue and dequeue can be implemented in O(1) time.

Handling an Empty Queue

Empty-queue handling is an important part of queue operations. A dequeue or peek operation should not attempt to access an element that does not exist.

Typical empty queue:

Front → [ ]

dequeue() → no element available
peek()    → no element available

The exact response depends on the programming language and implementation. Some APIs return null or another empty value, while others throw an exception.

Queue Operations and Complexity

When a queue is implemented with appropriate data structures and maintained front and rear references, its primary operations can be very efficient.

Operation Typical Complexity Changes Queue?
Enqueue O(1) Yes
Dequeue O(1) Yes
Peek O(1) No
IsEmpty O(1) No
Size O(1)* No

*Size is O(1) when the implementation maintains a count. If size is calculated by traversing all elements, it can be O(n).

Common Mistakes with Queue Operations

  • Removing elements from the rear instead of the front.
  • Adding new elements to the front instead of the rear.
  • Forgetting to handle dequeue on an empty queue.
  • Confusing queue FIFO behavior with stack LIFO behavior.
  • Using an inefficient array operation for large queues without considering its complexity.
  • Failing to update the rear reference when the last element is removed.
  • Failing to keep the front and rear states consistent when the queue becomes empty.

Queue Operations in Real Applications

These operations become especially useful when a system has work arriving at different times but needs to process that work in a predictable order.

For example, consider a background job system. New jobs are added using enqueue. A worker takes the next available job using dequeue. The system can use peek to inspect the next job, size to monitor how much work is waiting, and isEmpty to determine whether there is currently work available.

const jobs = [];
let front = 0;

function addJob(job) {
  jobs.push(job);
}

function processNextJob() {
  if (front >= jobs.length) {
    return null;
  }

  return jobs[front++];
}

addJob("Send email");
addJob("Generate report");
addJob("Create backup");

console.log(processNextJob()); // Send email
console.log(processNextJob()); // Generate report
console.log(processNextJob()); // Create backup

Key Takeaways

  • Enqueue adds an element to the rear.
  • Dequeue removes an element from the front.
  • Peek reads the front element without removing it.
  • IsEmpty checks whether the queue contains elements.
  • Size reports the number of stored elements when supported by the implementation.
  • Front identifies the next element to be processed.
  • Rear identifies the most recently added element.
  • Efficient queue implementations can provide O(1) enqueue and dequeue operations.
  • For JavaScript arrays, a front index can avoid the repeated shifting associated with shift().

Code Examples

const queue = [];
let front = 0;

function enqueue(value) {
  queue.push(value);
}

function dequeue() {
  if (front >= queue.length) {
    return null;
  }

  return queue[front++];
}

function peek() {
  return front < queue.length
    ? queue[front]
    : null;
}

function isEmpty() {
  return front >= queue.length;
}

function size() {
  return queue.length - front;
}

enqueue(10);
enqueue(20);
enqueue(30);

console.log("Peek:", peek());
console.log("Dequeue:", dequeue());
console.log("Size:", size());
console.log("Empty:", isEmpty());
    
const queue = [];
let front = 0;

function enqueue(value) {
  queue.push(value);
}

function peek() {
  return front < queue.length
    ? queue[front]
    : null;
}

function rear() {
  return front < queue.length
    ? queue[queue.length - 1]
    : null;
}

enqueue(100);
enqueue(200);
enqueue(300);

console.log("Front:", peek()); // 100
console.log("Rear:", rear());  // 300
    
const jobs = [];
let front = 0;

function addJob(job) {
  jobs.push(job);
}

function processNextJob() {
  if (front >= jobs.length) {
    return null;
  }

  return jobs[front++];
}

addJob("Send email");
addJob("Generate report");
addJob("Create backup");

let job;

while ((job = processNextJob()) !== null) {
  console.log("Processing:", job);
}
    

Pro Tip

For high-volume queues, focus on the cost of removing the front element. In JavaScript, a front index or linked-list implementation can avoid the repeated element shifting that can occur with array.shift().

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