Skip to content

Commit 1f4e2f8

Browse files
Add Concurrent Merge Sort Implementation (#7544)
* feat: add ConcurrentMergeSort implementation * fix: resolve checkstyle, dead code, and add test coverage * style: fix clang-format issues * fix: resolve maven build failure * fix: resolve spotbugs exception softening violation * refactor: use CompletableFuture to bypass SpotBugs exception softening rule * style: fix trailing blank line to satisfy clang-format
1 parent f0b7778 commit 1f4e2f8

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.thealgorithms.sorts;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.LinkedBlockingQueue;
5+
import java.util.concurrent.ThreadPoolExecutor;
6+
import java.util.concurrent.TimeUnit;
7+
8+
/**
9+
* A concurrent implementation of the Merge Sort algorithm.
10+
*
11+
* <p>This implementation utilizes a divide-and-conquer strategy, distributing
12+
* the sorting of sub-arrays across multiple threads using a {@link ThreadPoolExecutor}.
13+
* To prevent the overhead of thread creation and context switching from outweighing
14+
* the benefits of concurrency, it falls back to a standard sequential merge sort
15+
* when the sub-array size drops below a predefined threshold, or when the maximum
16+
* concurrency depth is reached (preventing thread starvation deadlocks).
17+
*
18+
* <p><strong>Complexity:</strong>
19+
* <ul>
20+
* <li>Time Complexity: $O(N \log N)$</li>
21+
* <li>Space Complexity: $O(N)$</li>
22+
* </ul>
23+
*/
24+
public final class ConcurrentMergeSort {
25+
26+
private ConcurrentMergeSort() {
27+
}
28+
29+
/**
30+
* Fallback threshold where the algorithm switches to standard sequential
31+
* Merge Sort to prevent thread-creation overhead from ruining performance.
32+
*/
33+
private static final int SEQUENTIAL_THRESHOLD = 8192;
34+
35+
/**
36+
* Sorts the specified array of integers concurrently using Merge Sort.
37+
*
38+
* @param array the array to be sorted
39+
*/
40+
public static void sort(int[] array) {
41+
if (array == null || array.length <= 1) {
42+
return;
43+
}
44+
45+
int availableProcessors = Runtime.getRuntime().availableProcessors();
46+
47+
// Calculate a safe maximum depth to prevent creating more tasks than the pool can handle.
48+
// This effectively prevents thread starvation deadlock in fixed-size thread pools,
49+
// by forcing leaf tasks to run sequentially and eventually complete.
50+
int maxDepth = (int) (Math.log(availableProcessors) / Math.log(2)) + 1;
51+
52+
ThreadPoolExecutor executor = new ThreadPoolExecutor(availableProcessors, availableProcessors, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
53+
54+
try {
55+
int[] tempArray = new int[array.length];
56+
concurrentMergeSort(array, tempArray, 0, array.length - 1, executor, maxDepth);
57+
} finally {
58+
// Ensure the executor is gracefully shut down
59+
executor.shutdown();
60+
}
61+
}
62+
63+
/**
64+
* Recursively sorts the array utilizing the provided executor for concurrency.
65+
*
66+
* @param array the array to sort
67+
* @param temp a temporary array for merging
68+
* @param left the starting index of the sub-array
69+
* @param right the ending index of the sub-array
70+
* @param executor the {@link ThreadPoolExecutor} to handle concurrent tasks
71+
* @param depth the remaining depth for allowing concurrent execution
72+
*/
73+
private static void concurrentMergeSort(int[] array, int[] temp, int left, int right, ThreadPoolExecutor executor, int depth) {
74+
int length = right - left + 1;
75+
76+
// Switch to sequential sort if the array is small or we have reached the maximum concurrent depth
77+
if (length < SEQUENTIAL_THRESHOLD || depth <= 0) {
78+
sequentialMergeSort(array, temp, left, right);
79+
return;
80+
}
81+
82+
int mid = left + (right - left) / 2;
83+
84+
// Submit the left half for concurrent execution
85+
CompletableFuture<Void> leftTask = CompletableFuture.runAsync(() -> concurrentMergeSort(array, temp, left, mid, executor, depth - 1), executor);
86+
87+
// Process the right half in the current thread to optimize resource usage
88+
concurrentMergeSort(array, temp, mid + 1, right, executor, depth - 1);
89+
90+
// Wait for the concurrently executed left half to complete
91+
leftTask.join();
92+
93+
merge(array, temp, left, mid, right);
94+
}
95+
96+
/**
97+
* Sorts the specified sub-array sequentially using standard Merge Sort.
98+
*
99+
* @param array the array to sort
100+
* @param temp a temporary array for merging
101+
* @param left the starting index of the sub-array
102+
* @param right the ending index of the sub-array
103+
*/
104+
private static void sequentialMergeSort(int[] array, int[] temp, int left, int right) {
105+
if (left >= right) {
106+
return;
107+
}
108+
109+
int mid = left + (right - left) / 2;
110+
sequentialMergeSort(array, temp, left, mid);
111+
sequentialMergeSort(array, temp, mid + 1, right);
112+
merge(array, temp, left, mid, right);
113+
}
114+
115+
/**
116+
* Merges two sorted sub-arrays into a single sorted sub-array.
117+
*
118+
* @param array the original array containing the sub-arrays
119+
* @param temp a temporary array used for merging
120+
* @param left the starting index of the first sub-array
121+
* @param mid the ending index of the first sub-array (and the partition point)
122+
* @param right the ending index of the second sub-array
123+
*/
124+
private static void merge(int[] array, int[] temp, int left, int mid, int right) {
125+
System.arraycopy(array, left, temp, left, right - left + 1);
126+
127+
int i = left;
128+
int j = mid + 1;
129+
int k = left;
130+
131+
while (i <= mid && j <= right) {
132+
if (temp[i] <= temp[j]) {
133+
array[k++] = temp[i++];
134+
} else {
135+
array[k++] = temp[j++];
136+
}
137+
}
138+
139+
while (i <= mid) {
140+
array[k++] = temp[i++];
141+
}
142+
143+
// Remaining elements from the right half are already in their correct relative positions
144+
}
145+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.thealgorithms.sorts;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
5+
import java.util.Arrays;
6+
import java.util.Random;
7+
import org.junit.jupiter.api.Test;
8+
9+
/**
10+
* JUnit 5 test class for {@link ConcurrentMergeSort}.
11+
*/
12+
public class ConcurrentMergeSortTest {
13+
14+
@Test
15+
public void testAlreadySortedArray() {
16+
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
17+
int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
18+
19+
ConcurrentMergeSort.sort(array);
20+
21+
assertArrayEquals(expected, array, "Already sorted array should remain unchanged.");
22+
}
23+
24+
@Test
25+
public void testReverseSortedArray() {
26+
int[] array = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
27+
int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
28+
29+
ConcurrentMergeSort.sort(array);
30+
31+
assertArrayEquals(expected, array, "Reverse sorted array should be sorted correctly.");
32+
}
33+
34+
@Test
35+
public void testIdenticalElementsArray() {
36+
int[] array = {5, 5, 5, 5, 5, 5, 5};
37+
int[] expected = {5, 5, 5, 5, 5, 5, 5};
38+
39+
ConcurrentMergeSort.sort(array);
40+
41+
assertArrayEquals(expected, array, "Array with identical elements should be sorted correctly (unchanged).");
42+
}
43+
44+
@Test
45+
public void testLargeRandomArray() {
46+
int size = 100_000;
47+
int[] array = new int[size];
48+
int[] expected = new int[size];
49+
// Using a fixed seed for deterministic testing
50+
Random random = new Random(42);
51+
52+
for (int i = 0; i < size; i++) {
53+
int value = random.nextInt();
54+
array[i] = value;
55+
expected[i] = value;
56+
}
57+
58+
// Generate the expected result using Java's highly optimized built-in sort
59+
Arrays.sort(expected);
60+
61+
// This will easily trigger the concurrency threshold (8192) in the implementation
62+
ConcurrentMergeSort.sort(array);
63+
64+
assertArrayEquals(expected, array, "Large random array should be sorted correctly utilizing concurrency.");
65+
}
66+
67+
@Test
68+
public void testEmptyArray() {
69+
int[] array = {};
70+
int[] expected = {};
71+
72+
ConcurrentMergeSort.sort(array);
73+
74+
assertArrayEquals(expected, array, "Empty array should be handled without errors.");
75+
}
76+
77+
@Test
78+
public void testSingleElementArray() {
79+
int[] array = {42};
80+
int[] expected = {42};
81+
82+
ConcurrentMergeSort.sort(array);
83+
84+
assertArrayEquals(expected, array, "Single element array should be handled without errors.");
85+
}
86+
87+
@Test
88+
public void testNullArray() {
89+
int[] array = null;
90+
ConcurrentMergeSort.sort(array);
91+
org.junit.jupiter.api.Assertions.assertNull(array, "Null array should be handled without errors.");
92+
}
93+
}

0 commit comments

Comments
 (0)