排序算法是计算机科学中的基础问题,在大规模数据处理场景下,如何提高排序速度成为一个关键挑战。快速排序(QuickSort)因其平均时间复杂度为 O(nlogn) 而备受青睐。然而,在单线程环境下,传统快速排序无法充分利用多核 CPU 的并行计算能力。
本篇文章将介绍如何使用 Fork/Join 框架 实现 并行快速排序,并与单线程版本进行性能对比,探索其在大规模数据处理中的优势。
快速排序基于分治 思想,其具体实现主要有三个步骤:
1.基准选取: 选择数组中一个元素作为基准并将它与数组中第0个元素交换,记录下该基准值,视数组第0个元素为空。
2.划分: 找到基准应该放的数组中的位置,基准值的左边全部小于该值,基准值的右边全部大于该值。将基准值填入该位置。
3.分治: 对基准值的左半部分数组和右半部分数组分别重复这三个步骤。直到数组长度为1。
代码实现:
public static void quickSort(int[] array, int left, int right) {
if (right <= left) return;
int mid = partition(array, left, right);
quickSort(array, left, mid - 1);
quickSort(array, mid + 1, right);
}
public static int partition(int[] array, int left, int right) {
int pivot = array[left];
while (left < right) {
while (left < right && array[right] >= pivot) right--;
array[left] = array[right];
while (left < right && array[left] <= pivot) left++;
array[right] = array[left];
}
array[left] = pivot;
return left;
}
该算法在 最坏情况下(例如已经排序的数组)会退化为 O(n^2) ,但在一般情况下仍能保持 O(nlogn) 的时间复杂度。
现代 CPU 具有多核架构,如果我们能将排序任务分配给多个线程并行执行,将能显著提升性能。Java 提供的 Fork/Join 框架 非常适合用于分治和递归任务的并行化处理。
而快速排序就是一种基于分治思想的排序算法,它在每次分治将当前排序任务拆分成为两个小的排序任务的时候都可以使用多线程并行技术对其进行优化。让两个小排序任务可以并行计算。
Fork/Join 框架是 Java 7 引入的一个 工作窃取(Work Stealing) 线程池,适用于将大任务拆分为多个子任务,并行执行,然后合并结果。
完整代码如下:
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.ForkJoinPool;
static class Task extends RecursiveAction {
int[] array;
int left;
int right;
public Task(int[] array, int left, int right) {
this.array = array;
this.left = left;
this.right = right;
}
public int partition(int[] array, int left, int right) {
int pivot = array[left];
while (left < right) {
while (left < right && array[right] >= pivot) right--;
array[left] = array[right];
while (left < right && array[left] <= pivot) left++;
array[right] = array[left];
}
array[left] = pivot;
return left;
}
@Override
protected void compute() {
if (right <= left) return;
int mid = partition(array, left, right);
Task leftTask = new Task(array, left, mid - 1);
Task rightTask = new Task(array, mid + 1, right);
invokeAll(leftTask, rightTask);
}
}
在 Fork/Join 框架中,我们需要继承 RecursiveAction(无返回值任务)或 RecursiveTask<T>(有返回值任务)。这里,我们使用 RecursiveAction,因为快速排序的操作只是在数组上进行排序,不需要返回任何值。
static class Task extends RecursiveAction {
int[] array;
int left;
int right;
public Task(int[] array, int left, int right) {
this.array = array;
this.left = left;
this.right = right;
}
Task 类表示一个子任务,它接受待排序的数组和当前处理的区间 left 到 right。当 Task 被执行时,它会递归地拆分子任务,直到满足递归终止条件。
快速排序的关键步骤是分区(partition),即选择一个基准元素(pivot),然后调整数组,使得:
public int partition(int[] array, int left, int right) {
int pivot = array[left]; // 选择最左边的元素作为基准
while (left < right) {
while (left < right && array[right] >= pivot) right--; // 从右向左找小于 pivot 的元素
array[left] = array[right]; // 交换到左侧
while (left < right && array[left] <= pivot) left++; // 从左向右找大于 pivot 的元素
array[right] = array[left]; // 交换到右侧
}
array[left] = pivot; // 放置 pivot 到正确的位置
return left; // 返回 pivot 的最终索引
}
我每次选取 arrayleft 作为基准值,并利用 双指针(left 和 right)进行交换。循环终止后,pivot 被放置到最终位置,并返回它的索引 mid,以便后续递归排序。
compute() 方法是 RecursiveAction 任务的核心执行逻辑:
@Override
protected void compute() {
if (right <= left) return; // 递归终止条件
int mid = partition(array, left, right); // 进行分区
Task leftTask = new Task(array, left, mid - 1); // 处理左半部分
Task rightTask = new Task(array, mid + 1, right); // 处理右半部分
invokeAll(leftTask, rightTask); // 并行执行两个子任务
}
ForkJoinPool pool = new ForkJoinPool(); // 创建线程池
Task task = new Task(array, 0, array.length - 1); // 创建任务
pool.invoke(task); // 执行任务
ForkJoinPool 是Fork/Join框架中的线程池,能够自动管理任务分配和线程调度。
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
Random random = new Random();
int size = 10_000_000;
int[] array1 = new int[size];
int[] array2 = new int[size];
for (int i = 0; i < size; i++) {
int randomValue = random.nextInt();
array1[i] = randomValue;
array2[i] = randomValue;
}
long start = System.nanoTime();
pool.invoke(new Task(array1, 0, array1.length - 1));
long duration = System.nanoTime() - start;
System.out.println("并行快速排序耗时:" + duration / 1e6 + "ms");
start = System.nanoTime();
quickSort(array2, 0, array2.length - 1);
duration = System.nanoTime() - start;
System.out.println("单线程快速排序耗时:" + duration / 1e6 + "ms");
}
数组大小 | 单线程 | 多线程 |
---|---|---|
1000 | 0.2437ms | 9.9459ms |
10000 | 1.7561ms | 14.001ms |
100000 | 13.4926ms | 30.2836ms |
1000000 | 93.7132ms | 64.5085ms |
10000000 | 985.7065ms | 343.8639ms |
100000000 | 10856.8635ms | 1986.0832ms |
可以看出,数据规模在较小的时候,单线程的性能优于多线程,这时因为在数据较小的时候,多线程的创建和管理的开销比快速排序本身的开销还要大。
但随着数据规模的增大,多线程并行计算的优势逐渐显现出来,充分利用了多核 CPU 的计算能力。
线程管理开销: 小数据量时,多线程的创建与管理可能比排序本身更耗时。
分区不均衡: 若数据本身有序,可能导致负载不均,影响性能。
本篇文章介绍了 快速排序 的原理,并通过 Fork/Join 框架 实现了 并行快速排序。通过实验对比,我们发现 并行计算 在大规模数据集上具有显著优势,是高性能数据处理中的重要优化手段。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。