Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/main/java/com/thealgorithms/sorts/ConcurrentMergeSort.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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).
*
* <p><strong>Complexity:</strong>
* <ul>
* <li>Time Complexity: $O(N \log N)$</li>
* <li>Space Complexity: $O(N)$</li>
* </ul>
*/
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<Runnable>());

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<Void> 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
}
}
Original file line number Diff line number Diff line change
@@ -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.");
}
}
Loading