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.
Competitive Programming Problems: Queues and Deques in Java
This document explores the use of Queues and Deques in solving competitive programming problems efficiently in Java. We will delve into specific problem examples, focusing on optimization techniques to achieve optimal performance.
Introduction to Queues and Deques
Before diving into problems, let's briefly review the fundamental properties of Queues and Deques:
- Queue: Follows the First-In, First-Out (FIFO) principle. Elements are added at the rear (enqueue) and removed from the front (dequeue). In Java, `java.util.Queue` is an interface, and common implementations include `java.util.LinkedList`.
- Deque (Double-Ended Queue): Allows insertion and removal of elements from both ends. In Java, `java.util.Deque` is an interface, and `java.util.ArrayDeque` is a commonly used implementation. ArrayDeque is usually preferred for its performance advantages.
Understanding these structures and their respective operations (enqueue, dequeue, peek, addFirst, addLast, removeFirst, removeLast) is crucial for solving many algorithmic problems effectively.
Problem 1: Sliding Window Maximum
Problem Statement:
Given an array of integers `nums` and an integer `k`, find the maximum value in each sliding window of size `k` as it moves from left to right through the array. For example, if nums = [1,3,-1,-3,5,3,6,7] and k = 3, the output should be [3, 3, 5, 5, 6, 7].
Solution (Java):
import java.util.ArrayDeque; import java.util.Deque; import java.util.ArrayList; import java.util.List; class SlidingWindowMaximum { public ListmaxSlidingWindow(int[] nums, int k) { Deque deque = new ArrayDeque<>(); // Stores indices List result = new ArrayList<>(); for (int i = 0; i < nums.length; i++) { // Remove elements out of window while (!deque.isEmpty() && deque.peekFirst() <= i - k) { deque.pollFirst(); } // Remove smaller elements in deque while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) { deque.pollLast(); } // Add current element index deque.offerLast(i); // Add to result if window is full if (i >= k - 1) { result.add(nums[deque.peekFirst()]); } } return result; } public static void main(String[] args) { SlidingWindowMaximum solution = new SlidingWindowMaximum(); int[] nums = {1, 3, -1, -3, 5, 3, 6, 7}; int k = 3; List result = solution.maxSlidingWindow(nums, k); System.out.println("Maximums: " + result); // Output: Maximums: [3, 3, 5, 5, 6, 7] } }
Explanation:
- We use a `Deque` (`ArrayDeque`) to store indices of elements within the current window.
- The `Deque` maintains a decreasing order of elements (by value) from front to back. This ensures the front of the `Deque` always contains the index of the largest element in the current window.
- We iterate through the array, maintaining the `Deque` as follows:
- Remove elements from the front that are outside the current window.
- Remove elements from the back that are smaller than the current element (since they can never be the maximum in a future window).
- Add the current element's index to the back of the `Deque`.
- Once the window is full (i >= k - 1), we add the value at the index at the front of the `Deque` (which is the maximum) to the result.
Problem 2: First Negative Integer in Every Window of Size K
Problem Statement:
Given an array of integers and a number K. Find the first negative integer for each and every window(contiguous subarray) of size K. If a window does not contain a negative integer, then print 0 for that window.
Solution (Java):
import java.util.ArrayDeque; import java.util.Deque; import java.util.ArrayList; import java.util.List; class FirstNegativeInteger { public ListfirstNegativeInteger(int[] nums, int k) { Deque deque = new ArrayDeque<>(); // Stores indices of negative numbers List result = new ArrayList<>(); // Process the first k elements for (int i = 0; i < k; i++) { if (nums[i] < 0) { deque.offerLast(i); } } // Process the remaining elements for (int i = k; i < nums.length; i++) { // Add the first negative number of the previous window to the result if (!deque.isEmpty()) { result.add(nums[deque.peekFirst()]); } else { result.add(0); } // Remove elements that are out of the current window while (!deque.isEmpty() && deque.peekFirst() <= i - k) { deque.pollFirst(); } // Add the current element to the deque if it's negative if (nums[i] < 0) { deque.offerLast(i); } } // Add the first negative number of the last window to the result if (!deque.isEmpty()) { result.add(nums[deque.peekFirst()]); } else { result.add(0); } return result; } public static void main(String[] args) { FirstNegativeInteger solution = new FirstNegativeInteger(); int[] nums = {12, -1, -7, 8, -15, 30, 16, 28}; int k = 3; List result = solution.firstNegativeInteger(nums, k); System.out.println("First Negative Integers: " + result); // Output: First Negative Integers: [-1, -1, -7, -15, -15, 0] } }
Explanation:
- We use a `Deque` (`ArrayDeque`) to store indices of negative numbers within the current window.
- We iterate through the array, maintaining the `Deque` to store only the indices of negative numbers in the current window.
- For each window, if the `Deque` is not empty, the element at the front of the `Deque` is the first negative number. Otherwise, we add 0.
- We remove elements from the front of the `Deque` if they are out of the current window.
- If the current element is negative, we add its index to the back of the `Deque`.
Problem 3: Minimum Cost to Connect Sticks
Problem Statement:
You have some sticks with positive integer lengths. You can connect any two sticks of lengths X and Y into one stick by paying a cost of X + Y. You perform this action until there is one stick remaining. Return the minimum total cost to connect all the given sticks into one stick. For example, if sticks = [2,4,3], the minimum cost would be 14. Explanation: First connect 2 and 3 (cost 2+3 = 5). Now you have sticks = [5,4]. Connect 5 and 4 (cost 5+4 = 9). Total cost is 5 + 9 = 14.
Solution (Java):
import java.util.PriorityQueue; import java.util.Queue; class ConnectSticks { public int connectSticks(int[] sticks) { // Use a min heap (PriorityQueue) to store stick lengths. QueueminHeap = new PriorityQueue<>(); for (int stick : sticks) { minHeap.offer(stick); } int totalCost = 0; while (minHeap.size() > 1) { // Take the two shortest sticks. int stick1 = minHeap.poll(); int stick2 = minHeap.poll(); // Connect them and add the cost to the total. int cost = stick1 + stick2; totalCost += cost; // Add the new stick back to the heap. minHeap.offer(cost); } return totalCost; } public static void main(String[] args) { ConnectSticks solution = new ConnectSticks(); int[] sticks = {2, 4, 3}; int minCost = solution.connectSticks(sticks); System.out.println("Minimum Cost: " + minCost); // Output: Minimum Cost: 14 } }
Explanation:
- This problem is best solved with a Priority Queue (min-heap) rather than directly with ArrayDeque or LinkedList Queue. However, it illustrates the concept of using a queue-like structure where the order of processing is crucial.
- We use a `PriorityQueue` to maintain a sorted order of stick lengths.
- We repeatedly take the two shortest sticks, connect them, and add the cost to the total.
- The new stick is added back to the `PriorityQueue`.
- We continue this process until only one stick remains.
Optimization Techniques
Here are some common optimization techniques when using Queues and Deques in competitive programming:
- Choose the Right Implementation: `ArrayDeque` is generally more efficient than `LinkedList` for `Deque` operations. For Queues where order matters by priority, use PriorityQueue.
- Space Optimization: Be mindful of the maximum size of the Queue/Deque, especially when dealing with large datasets. Avoid unnecessary object creation.
- Avoid Unnecessary Operations: Carefully analyze the problem to minimize the number of enqueue, dequeue, and peek operations.
- Pre-processing: If possible, pre-process the input data to reduce the workload during the main algorithm.
- Early Exit Conditions: Identify conditions under which the algorithm can terminate early to save time.
Conclusion
Queues and Deques are versatile data structures that can be used to solve a wide variety of competitive programming problems efficiently. Understanding their properties and applying appropriate optimization techniques is crucial for achieving optimal performance. The provided examples demonstrate how these data structures can be used effectively in Java.