Index

Queues and Deques

Java Collections

6.1 Queue Interface and Implementations

The Queue interface in Java represents a collection designed for holding elements prior to processing. It follows the First-In-First-Out (FIFO) principle, where elements are added to the end of the queue and removed from the front. Queues are commonly used in task scheduling, buffering, and event-driven programming.

Core Operations

The Queue interface provides the following primary methods:

These methods are non-exception throwing alternatives to add(), remove(), and element() from the Collection interface.

Common Implementations

Java provides several classes that implement the Queue interface. Among the most commonly used is:

LinkedList

Example:

import java.util.*;

Queue<String> queue = new LinkedList<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");

System.out.println(queue.poll()); // A
System.out.println(queue.peek()); // B
System.out.println(queue);        // [B, C]

In this example, "A" is removed first, demonstrating FIFO behavior.

Queue vs. Deque

While Queue is primarily single-ended (add to rear, remove from front), the Deque interface (double-ended queue) extends Queue and allows insertion and removal from both ends. This makes Deque useful for implementing both queues and stacks (LIFO).

Feature Queue Deque
Add Element offer() offerFirst(), offerLast()
Remove Element poll() pollFirst(), pollLast()
Peek Element peek() peekFirst(), peekLast()

Common Deque implementations include ArrayDeque and LinkedList.

Summary

In the following sections, we’ll explore specialized queues like PriorityQueue and see practical examples of using different types of queues and deques in real-world scenarios.

Index

6.2 PriorityQueue and Natural Ordering

The PriorityQueue in Java is a special kind of queue where elements are ordered based on priority, rather than their insertion order. Unlike standard Queue implementations like LinkedList, which operate in a First-In-First-Out (FIFO) manner, a PriorityQueue retrieves elements according to natural ordering or a custom comparator.

How PriorityQueue Works

Internally, PriorityQueue is implemented using a binary heap — a complete binary tree where each parent node is smaller than its child nodes (for a min-heap). The default PriorityQueue in Java is a min-heap, meaning the smallest element (based on natural ordering or a comparator) is always at the front of the queue.

Operations like insertion (offer) and removal (poll) maintain the heap invariant and have logarithmic time complexity (O(log n)).

Natural Ordering

If elements are added without a custom comparator, PriorityQueue uses their natural ordering, defined by the Comparable interface. For instance, Integer sorts in ascending order.

Example: Integer PriorityQueue (Natural Order)

import java.util.PriorityQueue;

public class NaturalPriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();

        pq.offer(42);
        pq.offer(15);
        pq.offer(30);

        while (!pq.isEmpty()) {
            System.out.println(pq.poll()); // Prints: 15, 30, 42
        }
    }
}

Explanation: Although 42 was added first, 15 (the smallest) is retrieved first due to natural ordering.

Custom Ordering with Comparator

For more control over element priority, you can provide a custom comparator when creating the PriorityQueue. This is useful when working with objects or when sorting in descending order.

Example: Custom Comparator for Descending Order

import java.util.*;

public class CustomPriorityQueueDemo {
    public static void main(String[] args) {
        Comparator<Integer> descending = (a, b) -> b - a;
        PriorityQueue<Integer> pq = new PriorityQueue<>(descending);

        pq.offer(42);
        pq.offer(15);
        pq.offer(30);

        while (!pq.isEmpty()) {
            System.out.println(pq.poll()); // Prints: 42, 30, 15
        }
    }
}

📝 The comparator (a, b) -> b - a reverses natural ordering, making it a max-heap.

PriorityQueue with Custom Objects

Let’s consider a real-world use case like task scheduling where tasks have priorities.

import java.util.*;

class Task {
    String name;
    int priority;

    Task(String name, int priority) {
        this.name = name;
        this.priority = priority;
    }

    @Override
    public String toString() {
        return name + " (priority: " + priority + ")";
    }
}

public class TaskScheduler {
    public static void main(String[] args) {
        Comparator<Task> byPriority = Comparator.comparingInt(t -> t.priority);
        PriorityQueue<Task> taskQueue = new PriorityQueue<>(byPriority);

        taskQueue.offer(new Task("Write report", 3));
        taskQueue.offer(new Task("Fix bugs", 1));
        taskQueue.offer(new Task("Update docs", 2));

        while (!taskQueue.isEmpty()) {
            System.out.println(taskQueue.poll());
        }
    }
}

Output:

Fix bugs (priority: 1)
Update docs (priority: 2)
Write report (priority: 3)

Use Cases for PriorityQueue

Summary

PriorityQueue is a versatile tool when order of processing is driven by importance rather than arrival time.

Index

6.3 Deque Interface: ArrayDeque and LinkedList

The Deque interface in Java represents a double-ended queue — a collection that supports adding, removing, and examining elements from both ends (front and back). This flexibility makes it useful for implementing both queues (FIFO) and stacks (LIFO).

What is a Deque?

Unlike a standard Queue, which primarily supports insertion at the tail and removal at the head, a Deque allows operations at both the head and tail:

This versatility lets you use a Deque as:

Implementations: ArrayDeque vs LinkedList

Java provides two common Deque implementations:

Feature ArrayDeque LinkedList
Internal structure Resizable array Doubly linked list
Performance Faster for most operations due to array access Slightly slower due to node traversal
Null elements Does not allow null elements Allows null elements
Memory overhead Lower overhead (array-backed) Higher overhead (node objects with pointers)
Thread safety Neither are thread-safe by default Neither are thread-safe

ArrayDeque is generally recommended for most use cases because of its better performance and lower memory footprint, unless you need the extra flexibility of LinkedList (such as null values or using it also as a List).

When to Use Deque Instead of Queue?

Use Deque when your application needs to:

Common Deque Operations

Here is a quick rundown of important Deque methods with examples using ArrayDeque:

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeExample {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();

        // Add elements at the tail (like a queue)
        deque.offerLast("A");  // same as offer()
        deque.offerLast("B");
        deque.offerLast("C");

        System.out.println(deque); // [A, B, C]

        // Add element at the front
        deque.offerFirst("Start");
        System.out.println(deque); // [Start, A, B, C]

        // Peek first and last elements
        System.out.println("First: " + deque.peekFirst()); // Start
        System.out.println("Last: " + deque.peekLast());   // C

        // Remove from front (like dequeue)
        System.out.println("Removed first: " + deque.pollFirst()); // Start
        System.out.println(deque); // [A, B, C]

        // Use push/pop as stack operations (LIFO)
        deque.push("X");  // equivalent to offerFirst()
        System.out.println(deque); // [X, A, B, C]

        System.out.println("Popped: " + deque.pop()); // X
        System.out.println(deque); // [A, B, C]
    }
}

Summary

Mastering Deque opens up many flexible data structure possibilities, enabling you to solve a wide range of problems efficiently with just one interface.

Index

6.4 Runnable Examples: FIFO, LIFO, Priority Queues

Example 1: FIFO behavior using Queue (LinkedList)

import java.util.*;

public class QueueFIFOExample {
    public static void main(String[] args) {
        Queue<String> fifoQueue = new LinkedList<>();

        // Adding elements to the queue
        fifoQueue.offer("First");
        fifoQueue.offer("Second");
        fifoQueue.offer("Third");

        System.out.println("FIFO Queue:");
        // Poll elements in insertion order (FIFO)
        while (!fifoQueue.isEmpty()) {
            System.out.println(fifoQueue.poll());
        }
        // Output:
        // First
        // Second
        // Third
    }
}

Example 2: LIFO behavior using Deque (ArrayDeque as Stack)

import java.util.*;

public class DequeLIFOExample {
    public static void main(String[] args) {
        Deque<String> lifoStack = new ArrayDeque<>();

        // Pushing elements onto the stack
        lifoStack.push("Bottom");
        lifoStack.push("Middle");
        lifoStack.push("Top");

        System.out.println("\nLIFO Stack:");
        // Pop elements in reverse order of insertion (LIFO)
        while (!lifoStack.isEmpty()) {
            System.out.println(lifoStack.pop());
        }
        // Output:
        // Top
        // Middle
        // Bottom
    }
}

Example 3: PriorityQueue with custom priorities

import java.util.*;

public class PriorityQueueExample {
    static class Task implements Comparable<Task> {
        String name;
        int priority; // Lower number = higher priority

        Task(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }

        @Override
        public int compareTo(Task other) {
            return Integer.compare(this.priority, other.priority);
        }

        @Override
        public String toString() {
            return name + " (priority: " + priority + ")";
        }
    }

    public static void main(String[] args) {
        PriorityQueue<Task> taskQueue = new PriorityQueue<>();

        taskQueue.offer(new Task("Low priority task", 5));
        taskQueue.offer(new Task("High priority task", 1));
        taskQueue.offer(new Task("Medium priority task", 3));

        System.out.println("\nPriority Queue:");
        // Poll tasks by priority (lowest priority number first)
        while (!taskQueue.isEmpty()) {
            System.out.println(taskQueue.poll());
        }
        // Output:
        // High priority task (priority: 1)
        // Medium priority task (priority: 3)
        // Low priority task (priority: 5)
    }
}

Explanation:

These examples show how different queue and deque implementations can be used to solve diverse real-world problems based on ordering and priority requirements.

Index