Chapter 3 of 6

Queues

Core Operations

Queue and the FIFO Principle

A queue is a linear data structure where elements are added at one end and removed from the other end. The rule followed is First In, First Out (FIFO), which means the element that entered the queue first is the one that leaves first. Think of people standing in a line at a ticket counter. The person who joined the line first gets the ticket first. New people join at the back of the line. Nobody cuts in from the front.

Example 199: Real World Example of FIFO

Think of a printer queue in an office. Document $A$ is sent first, then document $B$, then document $C$. The printer prints $A$ first, then $B$, then $C$. This is exactly FIFO. The first document sent is the first one printed, no matter what.

diagram
Core Queue Operations

A queue supports four main operations. All four take constant time, $O(1)$.

  • Enqueue(x): Add element $x$ at the REAR of the queue.

  • Dequeue(): Remove and return the element at the FRONT of the queue.

  • Front(): Look at the element at FRONT, without removing it.

  • Rear(): Look at the element at REAR, without removing it.

  • IsEmpty(): Check if the queue has no elements.

  • IsFull(): Check if the queue has no more room (for array implementation).

diagram

Array Implementation of Queue

Queue Using an Array

We implement a queue using a fixed-size array. We keep two variables, FRONT and REAR. Initially, both are set to $-1$, which means the queue is empty. When we enqueue the first element, both FRONT and REAR become $0$. After that, each enqueue increases REAR by $1$ and stores the element there. Each dequeue reads the element at FRONT and increases FRONT by $1$. The queue is empty when FRONT $>$ REAR, and full when REAR $=$ MAXSIZE $- 1$.

Example 200: Step by Step Trace

Maximum size = 5. Perform: Enqueue(10), Enqueue(20), Enqueue(30), Dequeue(), Enqueue(40), Front().

  • Start: FRONT = $-1$, REAR = $-1$, array = [\_, \_, \_, \_, \_]

  • Enqueue(10): FRONT = 0, REAR = 0, array = [10, \_, \_, \_, \_]

  • Enqueue(20): FRONT = 0, REAR = 1, array = [10, 20, \_, \_, \_]

  • Enqueue(30): FRONT = 0, REAR = 2, array = [10, 20, 30, \_, \_]

  • Dequeue(): returns 10, FRONT = 1, array = [\_, 20, 30, \_, \_]

  • Enqueue(40): FRONT = 1, REAR = 3, array = [\_, 20, 30, 40, \_]

  • Front(): returns 20 (element at FRONT = 1), FRONT stays at 1

diagram
Problem with Linear Array Queue

In a simple array implementation, even after dequeue, the slots at the beginning are empty but we never reuse them, because FRONT keeps moving right. Eventually, REAR reaches the last index, and even if there are empty slots at the beginning, we report the queue as full. This wastes memory. The solution is the Circular Queue.

diagram
diagram
diagram
Example 201: Pseudocode: Linear Queue

Initialize(Q, maxsize):

  1. Q.arr = array of size maxsize

  2. Q.FRONT = -1

  3. Q.REAR = -1

  4. Q.MAXSIZE = maxsize

Enqueue(Q, x):

  1. if Q.REAR == Q.MAXSIZE - 1:

  2.    print “Queue Overflow” and return

  3. if Q.FRONT == -1 and Q.REAR == -1:

  4.    Q.FRONT = 0, Q.REAR = 0

  5. else:

  6.    Q.REAR = Q.REAR + 1

  7. Q.arr[Q.REAR] = x

Dequeue(Q):

  1. if Q.FRONT == -1: print “Queue Underflow” and return -1

  2. x = Q.arr[Q.FRONT]

  3. if Q.FRONT == Q.REAR: Q.FRONT = -1, Q.REAR = -1

  4. else: Q.FRONT = Q.FRONT + 1

  5. return x

queue-linear.py
class Queue:
    def __init__(self, maxsize):
        self.arr     = [None] * maxsize
        self.FRONT   = -1
        self.REAR    = -1
        self.MAXSIZE = maxsize

    def is_empty(self):
        return self.FRONT == -1

    def is_full(self):
        return self.REAR == self.MAXSIZE - 1

    def enqueue(self, x):
        if self.is_full():
            print("Queue Overflow")
            return
        if self.FRONT == -1 and self.REAR == -1:
            self.FRONT = 0
            self.REAR  = 0
        else:
            self.REAR += 1
        self.arr[self.REAR] = x

    def dequeue(self):
        if self.is_empty():
            print("Queue Underflow")
            return -1
        x = self.arr[self.FRONT]
        if self.FRONT == self.REAR:  # last element
            self.FRONT = -1
            self.REAR  = -1
        else:
            self.FRONT += 1
        return x

    def front(self):
        if self.is_empty():
            print("Queue is Empty")
            return -1
        return self.arr[self.FRONT]

    def rear(self):
        if self.is_empty():
            print("Queue is Empty")
            return -1
        return self.arr[self.REAR]


# --- Driver Code ---
q = Queue(5)
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)
print("FRONT:", q.front())   # Output: 10
print("Dequeue:", q.dequeue())  # Output: 10
print("FRONT:", q.front())   # Output: 20

Circular Queue

Circular Queue

A circular queue fixes the problem of wasted slots. Instead of letting FRONT and REAR just move forward, we connect the end of the array back to the beginning, like a circle. When REAR reaches the last index, the next enqueue wraps REAR around to index $0$, if that slot is free. We use the modulo operator for this. REAR moves as $(REAR + 1) \bmod MAXSIZE$, and FRONT moves as $(FRONT + 1) \bmod MAXSIZE$.

diagram
Example 202: How Circular Wrapping Works

Suppose MAXSIZE = 5, and after several operations REAR is at index 4 (the last slot). The next enqueue would fail in a linear queue. But in a circular queue, \[ \text{new REAR} = (4 + 1) \bmod 5 = 0 \] So the element is placed at index 0, if it is free. Similarly, when FRONT is at index 4, \[ \text{new FRONT} = (4 + 1) \bmod 5 = 0 \] FRONT wraps back to the beginning.

Example 203: Full vs Empty Condition in Circular Queue

In a circular queue, distinguishing between full and empty needs care, because both can cause FRONT $=$ REAR.

  • Empty: FRONT $= -1$

  • Full: $(REAR + 1) \bmod MAXSIZE = FRONT$

This means we always keep one slot empty intentionally, so the full condition can be detected correctly.

diagram
diagram
diagram
queue-circular.py
class CircularQueue:
    def __init__(self, maxsize):
        self.arr     = [None] * maxsize
        self.FRONT   = -1
        self.REAR    = -1
        self.MAXSIZE = maxsize

    def is_empty(self):
        return self.FRONT == -1

    def is_full(self):
        return (self.REAR + 1) % self.MAXSIZE == self.FRONT

    def enqueue(self, x):
        if self.is_full():
            print("Queue Full")
            return
        if self.FRONT == -1:       # first element
            self.FRONT = 0
            self.REAR  = 0
        else:
            self.REAR = (self.REAR + 1) % self.MAXSIZE
        self.arr[self.REAR] = x

    def dequeue(self):
        if self.is_empty():
            print("Queue Empty")
            return -1
        x = self.arr[self.FRONT]
        if self.FRONT == self.REAR:   # last element
            self.FRONT = -1
            self.REAR  = -1
        else:
            self.FRONT = (self.FRONT + 1) % self.MAXSIZE
        return x

    def front(self):
        if self.is_empty():
            return -1
        return self.arr[self.FRONT]

    def rear(self):
        if self.is_empty():
            return -1
        return self.arr[self.REAR]

    def display(self):
        if self.is_empty():
            print("Queue is Empty")
            return
        print("Queue (FRONT to REAR):", end=" ")
        i = self.FRONT
        while True:
            print(self.arr[i], end=" ")
            if i == self.REAR:
                break
            i = (i + 1) % self.MAXSIZE
        print()


# --- Driver Code ---
cq = CircularQueue(5)
cq.enqueue(10)
cq.enqueue(20)
cq.enqueue(30)
cq.enqueue(40)
cq.display()              # 10 20 30 40
print(cq.dequeue())       # 10
print(cq.dequeue())       # 20
cq.enqueue(50)            # wraps around
cq.enqueue(60)            # wraps around
cq.display()              # 30 40 50 60

Deque (Double-Ended Queue)

Deque

A Deque (pronounced "deck"), short for Double-Ended Queue, is a queue where we can add and remove elements from both ends, the FRONT as well as the REAR. It is a more general structure than both a stack and a simple queue. A stack only allows operations at one end. A queue allows enqueue at REAR and dequeue at FRONT only. A deque allows all four combinations, add at FRONT, add at REAR, remove from FRONT, remove from REAR.

diagram
Two Types of Deque
  • Input Restricted Deque: Insertion is allowed only at REAR, but deletion is allowed at both FRONT and REAR. This is like a queue with extra deletion ability at REAR.

  • Output Restricted Deque: Deletion is allowed only at FRONT, but insertion is allowed at both FRONT and REAR. This is like a queue with extra insertion ability at FRONT.

diagram
Example 204: Pseudocode: Deque using Circular Array

Insert-Front(D, x):

  1. if IsFull(D): print “Deque Full” and return

  2. if IsEmpty(D): FRONT = 0, REAR = 0

  3. else: FRONT = (FRONT - 1 + MAXSIZE) mod MAXSIZE

  4. arr[FRONT] = x

Insert-Rear(D, x):

  1. if IsFull(D): print “Deque Full” and return

  2. if IsEmpty(D): FRONT = 0, REAR = 0

  3. else: REAR = (REAR + 1) mod MAXSIZE

  4. arr[REAR] = x

Delete-Front(D):

  1. if IsEmpty(D): print “Deque Empty” and return -1

  2. x = arr[FRONT]

  3. if FRONT == REAR: FRONT = -1, REAR = -1

  4. else: FRONT = (FRONT + 1) mod MAXSIZE

  5. return x

Delete-Rear(D):

  1. if IsEmpty(D): print “Deque Empty” and return -1

  2. x = arr[REAR]

  3. if FRONT == REAR: FRONT = -1, REAR = -1

  4. else: REAR = (REAR - 1 + MAXSIZE) mod MAXSIZE

  5. return x

deque-array.py
class Deque:
    def __init__(self, maxsize):
        self.arr     = [None] * maxsize
        self.FRONT   = -1
        self.REAR    = -1
        self.MAXSIZE = maxsize

    def is_empty(self):
        return self.FRONT == -1

    def is_full(self):
        return (self.REAR + 1) % self.MAXSIZE == self.FRONT

    def insert_front(self, x):
        if self.is_full():
            print("Deque Full")
            return
        if self.is_empty():
            self.FRONT = 0
            self.REAR  = 0
        else:
            # move FRONT one step back (wrap around)
            self.FRONT = (self.FRONT - 1 + self.MAXSIZE) % self.MAXSIZE
        self.arr[self.FRONT] = x

    def insert_rear(self, x):
        if self.is_full():
            print("Deque Full")
            return
        if self.is_empty():
            self.FRONT = 0
            self.REAR  = 0
        else:
            self.REAR = (self.REAR + 1) % self.MAXSIZE
        self.arr[self.REAR] = x

    def delete_front(self):
        if self.is_empty():
            print("Deque Empty")
            return -1
        x = self.arr[self.FRONT]
        if self.FRONT == self.REAR:
            self.FRONT = -1
            self.REAR  = -1
        else:
            self.FRONT = (self.FRONT + 1) % self.MAXSIZE
        return x

    def delete_rear(self):
        if self.is_empty():
            print("Deque Empty")
            return -1
        x = self.arr[self.REAR]
        if self.FRONT == self.REAR:
            self.FRONT = -1
            self.REAR  = -1
        else:
            self.REAR = (self.REAR - 1 + self.MAXSIZE) % self.MAXSIZE
        return x

    def get_front(self):
        return -1 if self.is_empty() else self.arr[self.FRONT]

    def get_rear(self):
        return -1 if self.is_empty() else self.arr[self.REAR]


# --- Driver Code ---
d = Deque(6)
d.insert_rear(10)
d.insert_rear(20)
d.insert_rear(30)
d.insert_front(5)       # insert at FRONT
print("FRONT:", d.get_front())   # Output: 5
print("REAR:", d.get_rear())     # Output: 30
print("Delete REAR:", d.delete_rear())   # Output: 30
print("Delete FRONT:", d.delete_front()) # Output: 5
print("FRONT:", d.get_front())           # Output: 10

Applications of Queues

Where Queues are Used

Queues appear naturally in any situation where items are processed in the order they arrived. The FIFO property makes queues the right tool for scheduling, traversal, and buffering tasks.

diagram
Application: Breadth-First Search (BFS)

BFS uses a queue to explore all nodes level by level in a graph. Starting from a source node, we enqueue it, then repeatedly dequeue a node, visit it, and enqueue all its unvisited neighbours. The FIFO property ensures we explore nodes in the order they were discovered, which gives level-by-level traversal.

Application: Level-Order Traversal of a Tree

In a binary tree, level-order traversal visits all nodes level by level, from left to right. We enqueue the root, then repeatedly dequeue a node, print it, and enqueue its left and right children. The queue ensures that all nodes at level $k$ are processed before any node at level $k+1$.

Application: CPU Scheduling

Operating systems use queues to manage processes waiting for CPU time. Processes are added to the ready queue in arrival order, and the CPU picks the next process from the FRONT of the queue, following FIFO scheduling. This ensures fairness and prevents starvation of processes.

Application: Printer Spooler

When multiple users send print jobs to the same printer, each job is added to a print queue. The printer processes jobs in the order they were received. This is a classic real-world queue, where FIFO ensures fairness.

Application: I/O Buffering and Web Server Request Handling

Queues are used to buffer input and output data between a fast processor and a slow device. In web servers, incoming client requests are queued and handled one at a time in order of arrival, so no request is lost even if the server is momentarily busy.