diff --git a/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java b/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java new file mode 100644 index 000000000000..062da01f380e --- /dev/null +++ b/src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java @@ -0,0 +1,145 @@ +package com.thealgorithms.sorts; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * A concurrent implementation of the Merge Sort algorithm. + * + *

This implementation utilizes a divide-and-conquer strategy, distributing + * the sorting of sub-arrays across multiple threads using a {@link ThreadPoolExecutor}. + * To prevent the overhead of thread creation and context switching from outweighing + * the benefits of concurrency, it falls back to a standard sequential merge sort + * when the sub-array size drops below a predefined threshold, or when the maximum + * concurrency depth is reached (preventing thread starvation deadlocks). + * + *

Complexity: + *

+ */ +public final class ConcurrentMergeSort { + + private ConcurrentMergeSort() { + } + + /** + * Fallback threshold where the algorithm switches to standard sequential + * Merge Sort to prevent thread-creation overhead from ruining performance. + */ + private static final int SEQUENTIAL_THRESHOLD = 8192; + + /** + * Sorts the specified array of integers concurrently using Merge Sort. + * + * @param array the array to be sorted + */ + public static void sort(int[] array) { + if (array == null || array.length <= 1) { + return; + } + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + + // Calculate a safe maximum depth to prevent creating more tasks than the pool can handle. + // This effectively prevents thread starvation deadlock in fixed-size thread pools, + // by forcing leaf tasks to run sequentially and eventually complete. + int maxDepth = (int) (Math.log(availableProcessors) / Math.log(2)) + 1; + + ThreadPoolExecutor executor = new ThreadPoolExecutor(availableProcessors, availableProcessors, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); + + try { + int[] tempArray = new int[array.length]; + concurrentMergeSort(array, tempArray, 0, array.length - 1, executor, maxDepth); + } finally { + // Ensure the executor is gracefully shut down + executor.shutdown(); + } + } + + /** + * Recursively sorts the array utilizing the provided executor for concurrency. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + * @param executor the {@link ThreadPoolExecutor} to handle concurrent tasks + * @param depth the remaining depth for allowing concurrent execution + */ + private static void concurrentMergeSort(int[] array, int[] temp, int left, int right, ThreadPoolExecutor executor, int depth) { + int length = right - left + 1; + + // Switch to sequential sort if the array is small or we have reached the maximum concurrent depth + if (length < SEQUENTIAL_THRESHOLD || depth <= 0) { + sequentialMergeSort(array, temp, left, right); + return; + } + + int mid = left + (right - left) / 2; + + // Submit the left half for concurrent execution + CompletableFuture leftTask = CompletableFuture.runAsync(() -> concurrentMergeSort(array, temp, left, mid, executor, depth - 1), executor); + + // Process the right half in the current thread to optimize resource usage + concurrentMergeSort(array, temp, mid + 1, right, executor, depth - 1); + + // Wait for the concurrently executed left half to complete + leftTask.join(); + + merge(array, temp, left, mid, right); + } + + /** + * Sorts the specified sub-array sequentially using standard Merge Sort. + * + * @param array the array to sort + * @param temp a temporary array for merging + * @param left the starting index of the sub-array + * @param right the ending index of the sub-array + */ + private static void sequentialMergeSort(int[] array, int[] temp, int left, int right) { + if (left >= right) { + return; + } + + int mid = left + (right - left) / 2; + sequentialMergeSort(array, temp, left, mid); + sequentialMergeSort(array, temp, mid + 1, right); + merge(array, temp, left, mid, right); + } + + /** + * Merges two sorted sub-arrays into a single sorted sub-array. + * + * @param array the original array containing the sub-arrays + * @param temp a temporary array used for merging + * @param left the starting index of the first sub-array + * @param mid the ending index of the first sub-array (and the partition point) + * @param right the ending index of the second sub-array + */ + private static void merge(int[] array, int[] temp, int left, int mid, int right) { + System.arraycopy(array, left, temp, left, right - left + 1); + + int i = left; + int j = mid + 1; + int k = left; + + while (i <= mid && j <= right) { + if (temp[i] <= temp[j]) { + array[k++] = temp[i++]; + } else { + array[k++] = temp[j++]; + } + } + + while (i <= mid) { + array[k++] = temp[i++]; + } + + // Remaining elements from the right half are already in their correct relative positions + } +} diff --git a/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java new file mode 100644 index 000000000000..454d0bd26929 --- /dev/null +++ b/src/test/java/com/thealgorithms/sorts/ConcurrentMergeSortTest.java @@ -0,0 +1,93 @@ +package com.thealgorithms.sorts; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.Arrays; +import java.util.Random; +import org.junit.jupiter.api.Test; + +/** + * JUnit 5 test class for {@link ConcurrentMergeSort}. + */ +public class ConcurrentMergeSortTest { + + @Test + public void testAlreadySortedArray() { + int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Already sorted array should remain unchanged."); + } + + @Test + public void testReverseSortedArray() { + int[] array = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Reverse sorted array should be sorted correctly."); + } + + @Test + public void testIdenticalElementsArray() { + int[] array = {5, 5, 5, 5, 5, 5, 5}; + int[] expected = {5, 5, 5, 5, 5, 5, 5}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Array with identical elements should be sorted correctly (unchanged)."); + } + + @Test + public void testLargeRandomArray() { + int size = 100_000; + int[] array = new int[size]; + int[] expected = new int[size]; + // Using a fixed seed for deterministic testing + Random random = new Random(42); + + for (int i = 0; i < size; i++) { + int value = random.nextInt(); + array[i] = value; + expected[i] = value; + } + + // Generate the expected result using Java's highly optimized built-in sort + Arrays.sort(expected); + + // This will easily trigger the concurrency threshold (8192) in the implementation + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Large random array should be sorted correctly utilizing concurrency."); + } + + @Test + public void testEmptyArray() { + int[] array = {}; + int[] expected = {}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Empty array should be handled without errors."); + } + + @Test + public void testSingleElementArray() { + int[] array = {42}; + int[] expected = {42}; + + ConcurrentMergeSort.sort(array); + + assertArrayEquals(expected, array, "Single element array should be handled without errors."); + } + + @Test + public void testNullArray() { + int[] array = null; + ConcurrentMergeSort.sort(array); + org.junit.jupiter.api.Assertions.assertNull(array, "Null array should be handled without errors."); + } +}