Chapter 25:Queues

Table of contents

Intro to Queues:
Queues are a foundational data structure in computer science used in many applications like scheduling tasks, handling resource management, and organizing data flow in real-time systems. Conceptually, a queue follows the First In, First Out (FIFO) principle, where the first element added is the first to be removed. This makes it useful for situations where order matters, such as managing tasks, handling incoming requests, or processing items in a sequence.

This chapter will guide you through the core concepts of queues, from basic implementation to more advanced topics like circular queues, double-ended queues (deques), and the use of queues in real-world problem-solving scenarios. We’ll also explore practical Java implementations using various techniques and Java Collections Framework (JCF).


1. Introduction to Queues

  • 1. Introduction to Queues

    Queue Meaning:
    A queue is a data structure that’s similar in concept to a stack but with a different access pattern. While a stack is like a vertical stack of plates (Last In, First Out, or LIFO), a queue is more like a horizontal line where the first person in line is served first (First In, First Out, or FIFO).

    If we visualize a queue:

    • Front is the part where elements exit, like people leaving from the front of a line.

    • Rear is the part where elements enter, like people joining the end of a line.

In queues:

  • FIFO means that the first item added (enqueued) will be the first one removed (dequeued).

Example Visualization:

  • Imagine a line of people where the person at the front leaves first, and new people join at the end. The queue has a Front and a Rear:

    • Front: Where removal happens (dequeue)

    • Rear: Where insertion happens (enqueue)


Operations on Queues

  1. Enqueue (Add)

    • Operation: Adds an element to the rear of the queue.

    • Time Complexity: ( O(1) ) – Adding is constant time since we just place the new item at the end of the queue.

  2. Dequeue (Remove)

    • Operation: Removes the element from the front of the queue.

    • Time Complexity: ( O(1) ) – Removing is constant time since we just take out the item at the front.

  3. Peek (Front Operation)

    • Operation: Accesses the element at the front without removing it, allowing us to check what’s next.

    • Time Complexity: ( O(1) ) – Constant time since we’re only viewing the front element.


Note: Double-Ended Queue (Deque)

There’s also a variation called a Double-Ended Queue (Deque), which allows adding and removing elements from both the front and the rear.

  • Why Use Queues? They’re perfect for scenarios where we need to process things in a specific order, like customer service requests or printer jobs.

Queue Implementation

There are several ways to implement a queue in Java, each with its own advantages depending on the requirements for memory management and performance. The most common implementations are:

  1. Using an Array

  2. Using a Linked List

  3. Using Two Stacks

Here’s a quick look at each method with basic Java code examples.

2. Queue Using Arrays

When implementing a queue with an array, we face some limitations due to the fixed size of arrays. Here, the queue will have a maximum capacity, which we cannot exceed. However, this method helps us understand the basics of how queues work.

Drawbacks of Queue Using Arrays

  1. Fixed Size: Once we set the size, we cannot add more elements than the specified limit.

  2. Inefficient Dequeue Operation: Every time we remove an element from the front, we have to shift all the remaining elements forward by one position, which has a time complexity of O(n).

Due to these issues, an array-based queue is not often used in practice; a circular queue is a better alternative. But let’s first explore the array-based approach for understanding.

Code Implementation of Queue Using Array

Let’s implement a basic queue in Java using an array. We’ll cover adding elements (enqueue), removing elements (dequeue), checking if the queue is empty, and retrieving the front element.

Queue Implementation Code

public class Basic {
    // Static class for Queue
    static class Queue {
        static int[] ar;     // Array for queue elements
        static int size;     // Maximum size of the queue
        static int rear;     // Index for the rear element

        // Constructor for initializing the queue
        Queue(int n) {
            ar = new int[n];
            size = n;
            rear = -1;
        }

        // Check if the queue is empty
        public static boolean isEmpty() {
            return rear == -1;
        }

        // Add (enqueue) an element to the queue
        public static void add(int data) {
            if (rear == size - 1) { // Check if the queue is full
                System.out.println("Queue is Full");
                return;
            }
            rear++;           // Increment rear to next index
            ar[rear] = data;  // Insert data at the rear
        }

        // Remove (dequeue) an element from the queue
        public static int remove() {
            if (isEmpty()) {   // Check if the queue is empty
                System.out.println("Empty Queue");
                return -1;
            }
            int front = ar[0]; // Store the front element to return later
            // Shift elements to the left by one position
            for (int i = 0; i < rear; i++) {
                ar[i] = ar[i + 1];
            }
            rear--;            // Decrement rear after shifting
            return front;      // Return the removed front element
        }

        // Retrieve (peek) the front element without removing it
        public static int peek() {
            if (isEmpty()) {
                System.out.println("Empty Queue");
                return -1;
            }
            return ar[0];      // Return the front element
        }
    }

    // Main method to test the Queue implementation
    public static void main(String[] args) {
        Queue q = new Queue(5);

        q.add(1);
        q.add(2);
        q.add(3);
        q.add(4);
        q.add(5);

        while (!q.isEmpty()) {
            System.out.println("Front Element: " + q.peek()); // Display front element
            q.remove(); // Remove the front element
        }
    }
}

Explanation of Each Function

  1. Queue Initialization: The constructor initializes the array, sets the maximum size of the queue, and sets rear to -1 (indicating an empty queue).

  2. isEmpty() Function: This function checks if the queue is empty by verifying if rear is -1.

  3. add() Function (Enqueue):

    • Checks if the queue is full (i.e., rear == size - 1). If full, it displays “Queue is Full.”

    • Otherwise, it increments the rear index by 1 and inserts the new element at the updated rear position.

  4. remove() Function (Dequeue):

    • If the queue is empty (rear == -1), it returns -1 after displaying “Empty Queue.”

    • Otherwise, it stores the front element (at ar[0]) for returning.

    • It then shifts all elements to the left to fill the gap left by the removed element.

    • Decrements rear to reflect the reduced queue size.

  5. peek() Function:

    • Checks if the queue is empty and, if so, displays “Empty Queue.”

    • If not empty, it returns the front element (ar[0]), allowing us to see the front without removing it.

1. Queue Initialization

In the beginning, our queue is empty.

  • Array Representation:

      Queue: [ , , , , ]  // Empty slots
      Rear: -1            // No elements added yet
      Size: 5             // Capacity is 5 elements
    

2. add(int data) - Enqueue Operation

First Enqueue (add(1))

  • Description: Adds 1 to the queue. Since rear is initially -1, it increments rear to 0 and places 1 at the rear position.

  • Array State:

      Queue: [1, , , , ]
      Rear: 0
    

Second Enqueue (add(2))

  • Description: Adds 2. It increments rear to 1 and places 2 at the rear.

  • Array State:

      Queue: [1, 2, , , ]
      Rear: 1
    

Third Enqueue (add(3))

  • Description: Adds 3. rear is incremented to 2, and 3 is added at the rear.

  • Array State:

      Queue: [1, 2, 3, , ]
      Rear: 2
    

Fourth Enqueue (add(4))

  • Description: Adds 4. rear moves to 3, and 4 is added.

  • Array State:

      Queue: [1, 2, 3, 4, ]
      Rear: 3
    

Fifth Enqueue (add(5))

  • Description: Adds 5. rear becomes 4, and 5 is added.

  • Array State:

      Queue: [1, 2, 3, 4, 5]
      Rear: 4
    

Sixth Enqueue (add(6))

  • Description: Attempts to add 6, but since rear == size - 1, the queue is full. It prints "Queue is Full."

  • Array State:

      Queue: [1, 2, 3, 4, 5]
      Rear: 4
    

3. remove() - Dequeue Operation

First Remove (remove())

  • Description: Removes the front element (1). It stores 1 as the removed element, shifts all remaining elements to the left by one, and decrements rear.

  • Array State:

      Queue: [2, 3, 4, 5, ]
      Rear: 3
    
    • Removed Element: 1

Second Remove (remove())

  • Description: Removes the front element (2). It stores 2, shifts the elements, and decrements rear.

  • Array State:

      Queue: [3, 4, 5, , ]
      Rear: 2
    
    • Removed Element: 2

Third Remove (remove())

  • Description: Removes 3, shifts the elements left, and updates rear.

  • Array State:

      Queue: [4, 5, , , ]
      Rear: 1
    
    • Removed Element: 3

Fourth Remove (remove())

  • Description: Removes 4, shifts the elements, and updates rear.

  • Array State:

      Queue: [5, , , , ]
      Rear: 0
    
    • Removed Element: 4

Fifth Remove (remove())

  • Description: Removes 5, leaving the queue empty. Sets rear to -1 after removal.

  • Array State:

      Queue: [ , , , , ]
      Rear: -1
    
    • Removed Element: 5

Sixth Remove (remove())

  • Description: Since the queue is empty (rear == -1), it returns -1 and prints "Empty Queue."

  • Array State:

      Queue: [ , , , , ]
      Rear: -1
    

4. peek() - Retrieve Front Element without Removing

The peek() function always returns the front element without removing it. Here’s how it works for a non-empty queue and an empty queue.

Peek After Adding Elements

  • Example: After adding elements 1, 2, and 3, we call peek().

  • Queue State:

      Queue: [1, 2, 3, , ]
      Front Element: 1
    

Peek on Empty Queue

  • Example: After removing all elements, calling peek() returns -1 and prints "Empty Queue."

  • Queue State:

      Queue: [ , , , , ]
      Rear: -1
    

Summary of Array-Based Queue Operations

Each operation alters the queue’s internal state:

OperationRearQueue ArrayFront Element
Initialize Queue-1[ , , , , ]None
add(1)0[1, , , , ]1
add(2)1[1, 2, , , ]1
add(3)2[1, 2, 3, , ]1
remove()1[2, 3, , , ]2
remove()0[3, , , , ]3
remove()-1[ , , , , ]None (Empty)

Why Array-Based Queue is Inefficient

For each removal, all elements shift to the left, causing an O(n) operation, which is not efficient for large data structures. This drawback is why arrays are rarely used directly to implement queues. Instead, circular queues are more efficient as they use space more effectively without requiring shifting on every removal.


3. Circular Queue

  • What It Solves: A circular queue helps avoid wasted space in arrays by wrapping around the array's end to the beginning.

  • Benefits: It allows for more efficient space use and is especially useful in systems where memory is limited.


4. Queue Using Linked List

In a Queue using Linked List, the queue is represented by a linked list where the front points to the head (the first node), and the rear/tail points to the last node in the linked list. We can visualize the queue operations and how the linked list adapts to the queue functionality.


1. Initial State of Queue (Linked List Representation)

Let’s start with a queue that is empty. The linked list representation of the queue initially looks like this:

  • Queue (Empty):

      Front → null
      Rear → null
    

In this case, both front and rear point to null because no elements have been added to the queue yet.


2. Add Operation (Enqueue)

The add() operation in a queue means adding an element at the rear of the queue (the tail of the linked list).

Adding the First Element (1)

When we add the first element 1, we create a new node and make both the front and rear point to this node. Since it's the only element in the queue, both front and rear will point to the same node.

  • Queue State (After add(1)):

      Front → [1] → null
      Rear  → [1] → null
    

Here, 1 is the only node, and the front and rear point to it.

Adding More Elements (2, 3)

Next, let's add 2 and 3 to the queue. Each time an element is added, it is added to the rear (the tail of the linked list), and the rear pointer is updated.

  • Queue State (After add(2)):

      Front → [1] → [2] → null
      Rear  → [2] → null
    
  • Queue State (After add(3)):

      Front → [1] → [2] → [3] → null
      Rear  → [3] → null
    

Now, we have three elements in the queue: 1, 2, and 3, with the front pointing to 1 and the rear pointing to 3. Each time a new node is added, the previous rear's next pointer is updated to point to the new node, and the rear itself is moved to the new node.


3. Remove Operation (Dequeue)

The remove() operation in a queue means removing the front element. When we remove an element, we update the front pointer to the next node in the list.

Removing the First Element (1)

Let's remove the front element (1). After the removal, the front pointer moves to the next node, which is 2. The rear pointer remains unchanged.

  • Queue State (After remove()):

      Front → [2] → [3] → null
      Rear  → [3] → null
    

Here, the node with 1 is removed from the queue. The garbage collector will reclaim the memory used by the node holding 1 because no references to it remain. The front pointer is updated to point to the next node (2), and the rear remains pointing to the last node (3).

Removing More Elements (2, 3)

When we continue removing elements, the front pointer keeps moving to the next node until the queue is empty.

  • Queue State (After remove() - 2):

      Front → [3] → null
      Rear  → [3] → null
    

After removing 2, the front moves to 3. Since 3 is the last element, both front and rear now point to the same node.

  • Queue State (After remove() - 3):

      Front → null
      Rear  → null
    

After removing 3, both front and rear are set to null, indicating the queue is empty.


4. Visualizing the Queue Using Linked List

Here's a full visualization of the Queue operations (add() and remove()) using a linked list.

Adding Elements:

  1. Start (Empty Queue):

     Front → null
     Rear → null
    
  2. After adding 1:

     Front → [1] → null
     Rear  → [1] → null
    
  3. After adding 2:

     Front → [1] → [2] → null
     Rear  → [2] → null
    
  4. After adding 3:

     Front → [1] → [2] → [3] → null
     Rear  → [3] → null
    

Removing Elements:

  1. After removing 1:

     Front → [2] → [3] → null
     Rear  → [3] → null
    
  2. After removing 2:

     Front → [3] → null
     Rear  → [3] → null
    
  3. After removing 3:

     Front → null
     Rear  → null
    

5. Code Implementation of Queue Using Linked List

Below is a simple implementation of a Queue using a linked list in Java.

public class QueueLL { 
    // Queue using Linked List   
    static class Node {
        int data;  // Data to hold the value of the node
        Node next; // Pointer to the next node in the queue

        // Constructor to create a new node with data
        Node(int data) {
            this.data = data;
            this.next = null;  // Initially, the next pointer is null
        }
    }

    static class Queue {
        static Node head = null;  // Head pointer to the front of the queue
        static Node tail = null;  // Tail pointer to the rear of the queue

        // Check if the queue is empty
        public static boolean isEmpty() {
            return head == null && tail == null;  // Queue is empty if head and tail are both null
        }

        // Add an element to the queue
        public static void add(int data) {
            Node newNode = new Node(data);  // Create a new node with the given data
            if (head == null) {  // If the queue is empty
                head = tail = newNode;  // Both head and tail point to the new node
                return;
            }
            tail.next = newNode;  // Link the current tail to the new node
            tail = newNode;       // Move the tail pointer to the new node
        }

        // Remove and return the front element of the queue
        public static int remove() {
            if (isEmpty()) {  // If the queue is empty
                System.out.println("Empty Queue");
                return -1;  // Return -1 to indicate an empty queue
            }
            int front = head.data;  // Store the front element
            if (tail == head) {  // If there is only one element in the queue
                head = tail = null;  // Set both head and tail to null, making the queue empty
            } else {
                head = head.next;  // Move the head pointer to the next node in the queue
            }
            return front;  // Return the removed front element
        }

        // Peek the front element of the queue without removing it
        public static int peek() {
            if (isEmpty()) {  // If the queue is empty
                System.out.println("Empty Queue");
                return -1;  // Return -1 to indicate an empty queue
            }
            return head.data;  // Return the data of the front element
        }
    }

    // Main method to test the Queue implementation
    public static void main(String[] args) {
        Queue q = new Queue();  // Create a new Queue object

        // Adding elements to the queue
        q.add(1);  // Add 1 to the queue
        q.add(2);  // Add 2 to the queue
        q.add(3);  // Add 3 to the queue
        q.add(4);  // Add 4 to the queue
        q.add(5);  // Add 5 to the queue

        // Process the queue until it's empty
        while (!q.isEmpty()) {
            System.out.println("Front Element: " + q.peek());  // Display the front element of the queue
            q.remove();  // Remove the front element from the queue
        }
    }
}

Explanation:

  1. Node Class:

    • Each node holds the data and a pointer to the next node.
  2. Queue Class:

    • The Queue class uses two pointers, head (for the front) and tail (for the rear).

    • The isEmpty() method checks if the queue is empty.

    • The add() method adds an element to the tail of the queue.

    • The remove() method removes the front element from the queue and returns it.

    • The peek() method returns the front element without removing it.

  3. Main Method:

    • We add elements (1, 2, 3, 4, 5) to the queue.

    • Then, in a while loop, we display the front element using peek() and remove it using remove() until the queue is empty.


Output:

Front Element: 1
Front Element: 2
Front Element: 3
Front Element: 4
Front Element: 5

Explanation of Output:

  • The peek() method shows the front element each time.

  • The remove() method removes the front element after each peek.

  • The loop continues until the queue is empty.

6. Time Complexity:

  • add(): O(1) — We add the element at the rear, which is a constant-time operation.

  • remove(): O(1) — We remove the element from the front, which is also a constant-time operation.

  • peek(): O(1) — Accessing the front element is a constant-time operation.


Conclusion

Using a Linked List to implement a queue overcomes the size limitation seen with arrays. It allows for dynamic resizing as we add and remove elements, making it more flexible and efficient for certain operations. The main advantage here is the O(1) time complexity for both enqueue and dequeue operations, which is more efficient compared to the array-based approach, where removing elements can take O(n) time.


5. Queue Using Java Collections Framework (JCF)

  • Why JCF?: Java’s built-in libraries make queues easy to implement with classes like LinkedList and PriorityQueue.

  • How It Helps: You get a ready-to-use queue that’s efficient and backed by Java’s built-in optimizations.


6. Queue Using Two Stacks

  • Idea: Use two stacks to simulate a queue by reversing the Last In, First Out (LIFO) nature of stacks into the FIFO order of queues.

  • How It Works: Push elements onto one stack, then transfer to another to reverse their order when needed.

Queue Using Two Stacks

A queue follows the FIFO (First In First Out) principle, meaning the first element added is the first to be removed.
A stack, on the other hand, follows the LIFO (Last In First Out) principle, meaning the last element added is the first to be removed.

To implement a queue using two stacks, we need to carefully manipulate the order of elements to maintain the FIFO behavior.


Why Do We Use Two Stacks for a Queue?

A queue operates on the FIFO (First In First Out) principle, meaning the first element added is the first to be removed. A stack, however, operates on the LIFO (Last In First Out) principle, meaning the last element added is the first to be removed.

When we use a single stack, the natural ordering would violate the queue’s FIFO behavior. To simulate a queue:

  • We need one stack (s1) to hold elements in the LIFO order.

  • Another stack (s2) helps temporarily reverse this order, allowing us to maintain FIFO behavior.


Two Possible Approaches

Option 1 (Enqueue: O(n), Dequeue: O(1))

  1. During the enqueue operation (add), we ensure that the elements in s1 are arranged in the correct order (FIFO).

  2. The dequeue operation (remove) simply pops from the top of s1, making it highly efficient.

Option 2 (Enqueue: O(1) Dequeue: O(n)

  1. During the enqueue operation, we directly push elements into s1 in O(1).

  2. During the dequeue operation, we transfer all elements to s2 and pop the front element.

We are implementing Option 1, where adding takes O(n) and removing takes O(1).


Process Explanation

  1. Adding Elements (add method):

    • Move all elements from s1 to s2 (to reverse the order).

    • Add the new element to the empty s1.

    • Move all elements back from s2 to s1.

  2. Removing Elements (remove method):

    • Simply pop the top element from s1 (constant time O(1)O(1)).
  3. Peeking the Front Element (peek method):

    • Return the top element from s1 without removing it.
  4. Checking if Empty (isEmpty method):

    • Return true if s1 is empty, otherwise false.

Code with Comments

import java.util.Stack;

public class Example {
    static class Queue {
        static Stack<Integer> s1 = new Stack<>(); // Main stack holding elements
        static Stack<Integer> s2 = new Stack<>(); // Auxiliary stack for reversing order

        // Check if the queue is empty
        public static boolean isEmpty() {
            return s1.isEmpty(); // Queue is empty if s1 is empty
        }

        // Add an element to the queue
        public static void add(int data) {
            // Step 1: Move all elements from s1 to s2
            while (!s1.isEmpty()) {
                s2.push(s1.pop());
            }

            // Step 2: Add the new element to s1
            s1.push(data);

            // Step 3: Move all elements back from s2 to s1
            while (!s2.isEmpty()) {
                s1.push(s2.pop());
            }
        }

        // Remove and return the front element
        public static int remove() {
            if (isEmpty()) {
                System.out.println("Queue is Empty");
                return -1;
            }
            return s1.pop(); // Directly pop from s1
        }

        // Peek the front element without removing it
        public static int peek() {
            if (isEmpty()) {
                System.out.println("Queue is Empty");
                return -1;
            }
            return s1.peek(); // Return the top of s1
        }
    }

    // Main method to test the Queue implementation
    public static void main(String[] args) {
        Queue q = new Queue();

        // Adding elements to the queue
        q.add(1); // Add 1
        q.add(2); // Add 2
        q.add(3); // Add 3

        // Process and display all elements in the queue
        while (!q.isEmpty()) {
            System.out.println(q.peek()); // Display the front element
            q.remove(); // Remove the front element
        }
    }
}

Visualization

Adding Elements (enqueue):

  1. Initial State:
    Both s1 and s2 are empty.

     s1: []
     s2: []
    
  2. Add 1:

    • Move all elements from s1s2 (no elements yet).

    • Add 1 to s1.

    • Move elements from s2s1 (no elements to move).

    s1: [1]
    s2: []
  1. Add 2:

    • Move 1 from s1s2.

    • Add 2 to s1.

    • Move 1 from s2s1.

    s1: [2, 1]
    s2: []
  1. Add 3:

    • Move 2 and 1 from s1s2.

    • Add 3 to s1.

    • Move 2 and 1 from s2s1.

    s1: [3, 2, 1]
    s2: []

Removing Elements (dequeue):

  1. Initial State:

     s1: [3, 2, 1]
    
  2. Remove:

    • Pop 1 from s1.
    s1: [3, 2]
  1. Remove:

    • Pop 2 from s1.
    s1: [3]
  1. Remove:

    • Pop 3 from s1.
    s1: []

Output

1
2
3

Each element is displayed in FIFO order, maintaining the behavior of a queue despite using stacks.


Complexity Analysis

  1. Add (enqueue): O(n)O(n)

    • Moving elements between stacks takes linear time.
  2. Remove (dequeue): O(1)O(1)

    • Direct pop operation on s1.
  3. Space Complexity: O(n)O(n)

    • Both stacks can hold all elements.

This implementation ensures FIFO behavior using two stacks effectively.


7. Stack Using Two Queues

  • Concept: This is the reverse of the above – we use two queues to simulate a stack.

  • Application: This teaches us how to manipulate basic structures to behave in new ways.


8. First Non-Repeating Character

  • Problem: Find the first character in a stream that doesn’t repeat.

  • Solution: A queue tracks the order of characters, making it easy to check for non-repeating ones by frequency.


9. Interleave Two Halves of a Queue

  • What It Does: This technique interweaves elements from the first and second halves of a queue.

  • Why Use It? Useful for balancing data or merging streams of information.


10. Queue Reversal

  • Goal: Reverse the elements in a queue.

  • Approach: We can use recursion or an auxiliary stack to reverse the order.


11. Deque (Double-Ended Queue)

  • What It Is: A deque allows adding/removing elements from both the front and the back.

  • Why It’s Useful: Deques are flexible, allowing access from both ends for efficient management in certain problems.


12. Deque in Java Collections Framework (JCF)

  • How to Use It: Java has the Deque interface and classes like ArrayDeque for efficient double-ended operations.

  • Advantages: Using JCF means we don’t need to reinvent the wheel – it’s faster and ready-made.


13. Queue Using Deque

  • Concept: Since a deque supports operations at both ends, it can work like a queue by using only one side for adding and the other for removing.

14. Practice Questions

  • Why Practice? Reinforces concepts by applying them to solve real-world problems, from simple queue operations to complex scenarios.

Each section is packed with examples and code snippets to help you implement and understand these concepts better. Let’s tackle each one to master queues!

Related Posts in My Series:

DSA in Java Series:

  • Chapter 2: Operators in Java – Learn about the different types of operators in Java, including arithmetic, relational, logical, and assignment operators, along with examples to understand their usage.

  • Chapter 33: Trie in Java – Explore the implementation and use of Trie data structure in Java, a powerful tool for handling strings, with practical coding examples.

Other Series:

  • Full Stack Java Development – Master the essentials of Java programming and web development with this series. Topics include object-oriented programming, design patterns, database interaction, and more.

  • Full Stack JavaScript Development – Become proficient in JavaScript with this series covering everything from front-end to back-end development, including frameworks like React and Node.js.


Connect with Me
Stay updated with my latest posts and projects by following me on social media:

Your feedback and engagement are invaluable. Feel free to reach out with questions, comments, or suggestions. Happy coding!

Rohit Gawande
Full Stack Java Developer | Blogger | Coding Enthusiast