Implement Queue Using an Array

Learn Implement Queue Using an Array with practical examples, explanations, best practices, and real-world coding techniques.

Implementing a Queue with an Array

A queue can be implemented using an array by maintaining the order of elements and controlling where new elements are inserted and where existing elements are removed. The queue follows the FIFO (First In, First Out) principle, so elements are added at the rear and removed from the front.

Arrays are convenient for demonstrating queue implementation because they provide indexed storage and are available in almost every programming language. However, the way the front of the queue is managed has an important effect on performance.

Front                         Rear
  ↓                             ↓
[ 10 ] [ 20 ] [ 30 ] [ 40 ]
  ↑                             ↑
Remove                        Add

Basic Array-Based Queue

The simplest approach is to store all queue elements in an array. A new element can be added with an array append operation, while the first element can be removed using an operation such as shift() in JavaScript.

const queue = [];

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

console.log(queue);
// [10, 20, 30]

const removed = queue.shift();

console.log(removed);
// 10

console.log(queue);
// [20, 30]

This implementation is easy to understand and works correctly for small queues. However, repeatedly removing the first element from a regular array can become inefficient because the remaining elements may need to be shifted or reindexed.

Why Front Indexing Matters

A more efficient array-based queue keeps track of the position of the front element instead of physically removing the first array element after every dequeue operation.

Consider the following queue:

queue = [10, 20, 30, 40]
front = 0

Front points to:
queue[0] → 10

After removing 10, the implementation can simply increment front.

queue = [10, 20, 30, 40]
front = 1

Front now points to:
queue[1] → 20

The value 10 remains physically stored in the array, but it is no longer considered part of the active queue. The active queue is represented by the elements from the front index onward.

Array Queue Structure

An efficient array-based queue commonly maintains two pieces of information:

  • Queue array: Stores the elements.
  • Front index: Identifies the next element that should be removed.

The rear can be represented by the current array length when elements are always appended using push().

Array:  [10] [20] [30] [40] [50]
          ↑               ↑
        front            rear

front = 0

After dequeue:

Array:  [10] [20] [30] [40] [50]
               ↑           ↑
             front        rear

front = 1

Enqueue in an Array Queue

The enqueue operation adds a new value to the rear of the queue. With a JavaScript array, push() can be used for this purpose.

const queue = [];
let front = 0;

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

console.log(queue);
// [10, 20, 30]

Appending an element does not affect the front index. The newly inserted value simply becomes the newest element at the rear.

Dequeue Using a Front Index

Instead of calling shift(), dequeue can read the element at the current front index and then increment that index.

const queue = [10, 20, 30];
let front = 0;

const removed = queue[front];
front++;

console.log(removed); // 10
console.log(queue[front]); // 20

The active queue is now represented by the values starting at front. No elements need to be shifted toward the beginning of the array.

Complete Array Queue Implementation

The following implementation provides the common queue operations using an array and a front index.

class Queue {
  constructor() {
    this.items = [];
    this.front = 0;
  }

  enqueue(value) {
    this.items.push(value);
  }

  dequeue() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items[this.front++];
  }

  peek() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items[this.front];
  }

  isEmpty() {
    return this.front >= this.items.length;
  }

  size() {
    return this.items.length - this.front;
  }
}

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

How the Implementation Works

Suppose three elements are inserted:

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

items = [10, 20, 30]
front = 0

The first dequeue reads items[0] and advances the front index.

dequeue()

removed = items[0]
front = 1

Active queue:
[20, 30]

A second dequeue reads items[1] and advances the front again.

dequeue()

removed = items[1]
front = 2

Active queue:
[30]

The underlying array still contains the earlier values, but the front index determines which portion is currently active.

Checking Whether the Queue Is Empty

With front indexing, the queue is empty when the front index reaches or passes the array length.

isEmpty() {
  return this.front >= this.items.length;
}

For example, if the array contains three inserted values and the front index is 3, there are no active elements left in the queue.

items = [10, 20, 30]
front = 3

isEmpty() → true

Calculating Queue Size

The number of active elements can be calculated using the difference between the array length and the front index.

size = items.length - front

If the array contains five values and the front index is 2, the active queue contains three elements.

items.length = 5
front = 2

size = 5 - 2
     = 3

Handling an Empty Queue

Dequeue and peek operations should check whether the queue is empty before accessing an element. Returning null is one possible design, although other applications may prefer an exception or another explicit result.

dequeue() {
  if (this.isEmpty()) {
    return null;
  }

  return this.items[this.front++];
}

This prevents the implementation from returning an invalid queue element when no active values remain.

The Memory Trade-Off

Front indexing improves dequeue performance, but it introduces another consideration: values that have already been removed remain in the underlying array.

After many dequeues:

items = [10, 20, 30, 40, 50, 60, 70]
                         ↑
                       front = 5

Active queue:
[60, 70]

Unused prefix:
[10, 20, 30, 40, 50]

If a queue is long-lived and processes a large number of elements, retaining this unused prefix can consume unnecessary memory. A production implementation can periodically compact the array or use a different queue structure.

Compacting the Array

One approach is to periodically remove the inactive prefix when it becomes large enough. This should not be performed after every dequeue because doing so would reintroduce repeated shifting costs.

compact() {
  if (this.front > 0 && this.front >= this.items.length / 2) {
    this.items = this.items.slice(this.front);
    this.front = 0;
  }
}

This strategy allows the implementation to reclaim unused space occasionally while keeping normal dequeue operations efficient. The exact compaction strategy depends on the workload and memory requirements.

Circular Array Queue

Another important array-based approach is a circular queue. Instead of allowing the front of the queue to move permanently toward the end of the array, the implementation reuses positions that become available after dequeue operations.

Initial:
[10] [20] [30] [40] [50]

After removing 10 and 20:
[  ] [  ] [30] [40] [50]

New elements can wrap around:
[60] [70] [30] [40] [50]
 ↑
rear wraps to beginning

Circular queues are particularly useful when a fixed amount of storage is available and memory positions should be reused. The circular queue is covered in more detail as a separate queue type.

Array Queue Complexity

The complexity of an array-based queue depends on how its operations are implemented. Using a front index avoids the repeated shifting associated with removing the first array element.

Operation Front Index Approach Notes
Enqueue O(1) amortized Appends to the array
Dequeue O(1) Advances the front index
Peek O(1) Reads the front position
IsEmpty O(1) Compares front with length
Size O(1) Uses length minus front

The exact enqueue complexity can vary because dynamic arrays may occasionally resize their internal storage. Over a sequence of appends, the usual amortized complexity is O(1).

Array Queue vs Shift-Based Queue

Both approaches are valid for demonstrating queue behavior, but they make different performance trade-offs.

Approach Dequeue Strategy Main Consideration
shift() Physically removes first array element Can require O(n) work
Front index Moves an index forward Uses additional inactive array space
Circular array Reuses array positions Requires more queue bookkeeping

When an Array Is a Good Choice

An array can be a practical choice for implementing a queue when direct indexed storage is convenient and the expected workload is compatible with the implementation.

  • The queue needs compact contiguous storage.
  • The maximum capacity is known and a circular implementation is suitable.
  • The language provides an efficient array-based queue abstraction.
  • You want a simple implementation for learning or small workloads.
  • Memory locality and predictable storage are useful for the application.

Common Implementation Mistakes

  • Using shift() repeatedly without considering its cost for large queues.
  • Forgetting to advance the front index after dequeue.
  • Reading from the queue without checking whether it is empty.
  • Calculating the queue size as items.length when a front index is being used.
  • Allowing unused elements at the beginning of a long-lived queue to grow without considering memory usage.
  • Resetting the front index incorrectly when the queue becomes empty.

Practical Example: Processing Requests

An array-based queue can represent requests waiting to be processed. New requests are appended to the rear, while the front index identifies the next request that should be handled.

const requests = [];
let front = 0;

function addRequest(request) {
  requests.push(request);
}

function processRequest() {
  if (front >= requests.length) {
    return null;
  }

  return requests[front++];
}

addRequest("Request A");
addRequest("Request B");
addRequest("Request C");

console.log(processRequest()); // Request A
console.log(processRequest()); // Request B
console.log(processRequest()); // Request C
console.log(processRequest()); // null

The example demonstrates the core idea of an array-based queue: new work enters at the end, while processing moves forward through the array.

Key Takeaways

  • An array can be used to implement a queue by maintaining FIFO ordering.
  • New elements are normally added at the rear using an append operation.
  • A front index can identify the next element to dequeue without shifting the remaining elements.
  • Using shift() is simple but can be inefficient for large queues.
  • Front indexing provides O(1) dequeue behavior while introducing inactive space at the beginning of the array.
  • Long-lived queues may need compaction or a circular array to reuse storage.
  • Circular queues are useful when fixed array capacity and memory reuse are important.
  • The implementation should always handle empty-queue operations safely.

Code Examples

class Queue {
  constructor() {
    this.items = [];
    this.front = 0;
  }

  enqueue(value) {
    this.items.push(value);
  }

  dequeue() {
    if (this.isEmpty()) {
      return null;
    }

    return this.items[this.front++];
  }

  peek() {
    return this.isEmpty()
      ? null
      : this.items[this.front];
  }

  isEmpty() {
    return this.front >= this.items.length;
  }

  size() {
    return this.items.length - this.front;
  }
}

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.size());    // 2
    
const requests = [];
let front = 0;

function addRequest(request) {
  requests.push(request);
}

function processRequest() {
  if (front >= requests.length) {
    return null;
  }

  return requests[front++];
}

addRequest("Request A");
addRequest("Request B");
addRequest("Request C");

let request;

while ((request = processRequest()) !== null) {
  console.log("Processing:", request);
}
    
class Queue {
  constructor() {
    this.items = [];
    this.front = 0;
  }

  enqueue(value) {
    this.items.push(value);
  }

  dequeue() {
    if (this.isEmpty()) {
      return null;
    }

    const value = this.items[this.front++];

    this.compact();

    return value;
  }

  compact() {
    if (
      this.front > 0 &&
      this.front >= this.items.length / 2
    ) {
      this.items = this.items.slice(this.front);
      this.front = 0;
    }
  }

  isEmpty() {
    return this.front >= this.items.length;
  }
}

const queue = new Queue();

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

console.log(queue.dequeue()); // 10
console.log(queue.dequeue()); // 20
console.log(queue.dequeue()); // 30
    

Pro Tip

For a learning implementation, a front index is a useful improvement over repeatedly calling shift(). For fixed-capacity systems, consider a circular queue so that positions freed by dequeue operations can be reused.

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