Data Structures: Queues and Deques in Java

Understanding and implementing Queues and Deques in Java, with example problems demonstrating their uses in Breadth-First Search and other scenarios.


Queues in Breadth-First Search (BFS)

Breadth-First Search (BFS) is a fundamental graph traversal algorithm that explores a graph or tree level by level. It starts at a chosen node (the root) and explores all the neighbor nodes at the present depth prior to moving on to the nodes at the next depth level. A queue data structure is absolutely crucial for implementing BFS effectively. This document explains how queues are used in BFS and provides Java examples relevant to competitive programming.

Why Use Queues in BFS?

Queues adhere to the FIFO (First-In, First-Out) principle. This principle perfectly aligns with the level-by-level exploration strategy of BFS. Here's why:

  • Level-Order Traversal: The queue ensures that nodes at the same level are visited before moving to the next level. When a node is visited, its unvisited neighbors are added to the queue. These neighbors, being one level deeper, will naturally be processed after all the nodes at the current level have been visited.
  • Maintaining Exploration Order: The queue maintains the order in which nodes should be visited. The first node added to the queue will be the first node to be processed, ensuring that the search expands outward from the starting node in a breadth-first manner.
  • Avoiding Cycles: By keeping track of visited nodes, BFS avoids infinite loops caused by cycles in the graph. The queue helps manage the order of node visits and ensure that each node is visited only once.

Algorithm Steps using a Queue

  1. Initialization:
    • Choose a starting node.
    • Create a queue and add the starting node to it.
    • Mark the starting node as visited.
  2. Iteration:
    • While the queue is not empty:
    • Dequeue a node from the front of the queue.
    • Process the node (e.g., print its value, perform calculations).
    • For each unvisited neighbor of the dequeued node:
      • Add the neighbor to the queue.
      • Mark the neighbor as visited.
  3. Termination: The algorithm terminates when the queue is empty, indicating that all reachable nodes from the starting node have been visited.

Java Implementation Examples

Example 1: Basic BFS on a Graph (Adjacency List Representation)

 import java.util.*;

class Graph {
    private int V;   // Number of vertices
    private LinkedList<Integer> adj[]; // Adjacency Lists

    // Constructor
    Graph(int v) {
    V = v;
    adj = new LinkedList[v];
    for (int i=0; i<v; ++i)
        adj[i] = new LinkedList<Integer>();
}

// Function to add an edge into the graph
void addEdge(int v,int w) {
    adj[v].add(w);
}

// prints BFS traversal from a given source s
void BFS(int s) {
    // Mark all the vertices as not visited(By default
    // set as false)
    boolean visited[] = new boolean[V];

    // Create a queue for BFS
    LinkedList<Integer> queue = new LinkedList<Integer>();

    // Mark the current node as visited and enqueue it
    visited[s]=true;
    queue.add(s);

    while (queue.size() != 0) {
        // Dequeue a vertex from queue and print it
        s = queue.poll();
        System.out.print(s + " ");

        // Get all adjacent vertices of the dequeued vertex s
        // If a adjacent has not been visited, then mark it
        // visited and enqueue it
        Iterator<Integer> i = adj[s].listIterator();
        while (i.hasNext()) {
            int n = i.next();
            if (!visited[n]) {
                visited[n] = true;
                queue.add(n);
            }
        }
    }
}

public static void main(String args[]) {
    Graph g = new Graph(4);
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);

    System.out.println("Following is Breadth First Traversal "+
                       "(starting from vertex 2)");

    g.BFS(2); // Output: 2 0 3 1
}
}

Explanation: This code demonstrates basic BFS on a graph represented using an adjacency list. It initializes a `visited` array to track visited nodes and a `queue` to manage the order of exploration. The poll() method removes the first element from the queue, following FIFO. The add() method adds elements to the end of the queue.

Example 2: BFS on a Binary Tree

 import java.util.LinkedList;
import java.util.Queue;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

public class BinaryTreeBFS {
    public void bfs(TreeNode root) {
    if (root == null) return;

    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root); //offer adds to queue

    while (!queue.isEmpty()) {
        TreeNode node = queue.poll(); //poll removes first element in queue.
        System.out.print(node.val + " ");

        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}

public static void main(String[] args) {
    TreeNode root = new TreeNode(1);
    root.left = new TreeNode(2);
    root.right = new TreeNode(3);
    root.left.left = new TreeNode(4);
    root.left.right = new TreeNode(5);

    BinaryTreeBFS bfsTraversal = new BinaryTreeBFS();
    System.out.println("BFS Traversal of Binary Tree:");
    bfsTraversal.bfs(root); // Output: 1 2 3 4 5
}
}

Explanation: This example shows BFS traversal on a binary tree. It uses a Queue<TreeNode> to store tree nodes. The offer() method enqueues nodes, and poll() dequeues nodes. The traversal ensures that nodes are visited level by level.

Example 3: Finding the Shortest Path in a Graph using BFS

 import java.util.*;

public class ShortestPathBFS {

    static int shortestPath(int n, int[][] edges, int start, int end) {
    // Build Adjacency List
    ArrayList<ArrayList<Integer>> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        adj.add(new ArrayList<>());
    }

    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);
        adj.get(edge[1]).add(edge[0]); // Assuming undirected graph
    }

    // BFS to find shortest path
    Queue<Integer> q = new LinkedList<>();
    int[] dist = new int[n];
    Arrays.fill(dist, -1); // Initialize distances to -1 (unvisited)

    q.offer(start);
    dist[start] = 0;

    while (!q.isEmpty()) {
        int node = q.poll();

        for (int neighbor : adj.get(node)) {
            if (dist[neighbor] == -1) {
                dist[neighbor] = dist[node] + 1;
                q.offer(neighbor);
            }
        }
    }

    return dist[end]; // Return shortest distance to end node
}


public static void main(String[] args) {
    int n = 7; // Number of nodes
    int[][] edges = {{0,1}, {0,2}, {1,3}, {2,3}, {2,4}, {3,5}, {4,6}, {5,6}};
    int start = 0;
    int end = 6;

    int shortestDistance = shortestPath(n, edges, start, end);
    System.out.println("Shortest distance from " + start + " to " + end + " is: " + shortestDistance); // Output: Shortest distance from 0 to 6 is: 3
}
}

Explanation: This example uses BFS to find the shortest path between two nodes in an undirected graph. It constructs an adjacency list from the given edges. A `dist` array tracks the distance of each node from the starting node. The BFS algorithm iteratively explores the graph, updating the `dist` array whenever a shorter path to a node is found. The `shortestPath` function returns the calculated shortest distance to the end node (or -1 if no path exists).

Key Considerations for Competitive Programming

  • Space Complexity: BFS can consume significant memory, especially for dense graphs, because the queue might need to hold a large number of nodes. Be mindful of memory constraints in competitive programming.
  • Time Complexity: BFS typically has a time complexity of O(V + E), where V is the number of vertices and E is the number of edges.
  • Choosing the Right Data Structure: In Java, LinkedList is often used as a queue implementation for BFS. ArrayDeque is generally recommended for performance reasons. However, if you need more advanced queue functionalities (e.g., priority queues), you may consider PriorityQueue.
  • Edge Cases: Handle edge cases carefully, such as disconnected graphs (where BFS might only explore a connected component) or invalid inputs.
  • Optimizations: For very large graphs, consider techniques like bidirectional BFS, which can significantly reduce search time in certain scenarios.