排序算法——快速排序的非递归写法

本文介绍了如何通过栈非递归地实现快速排序算法,利用栈模拟递归过程,详细讲解了分区操作、栈顶元素处理以及栈的初始化、销毁等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

快速排序的非递归

我们写快速排序的时候,通常用的递归的方法实现快速排序,那么有没有非递归的方法实现快速排序呢?肯定是有的。思想还是一样的,不过非递归是看似是非递归其实还是递归。

思路解释

快速排序的非递归使用的是栈这个数据结构。我们知道栈是后入先出和先入后出的,所以我们可以通过栈的方式模拟递归,然后实现快速排序的非递归。

如图所示,创建一个栈。然后首先先将数组的起始和末尾的下标存进栈中,然后让left=begin,right=end,并pop出去。然后进行一次快排找到keyi。此时如果keyi两边的区间(绿色的)存在,就把keyi也存进栈。然后进行一次快排,当区间为空或者区间值有序,就把值从栈中pop出来,如果不是,就继续push进去,直到栈为空。最后别忘了把栈给销毁。

代码


int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}
int PSort(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
	int key = begin;
	int prev = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
		if (a[cur] < a[key] && ++prev != cur)
			Swap(&a[cur], &a[prev]);
		++cur;
	}
	Swap(&a[key], &a[prev]);
	key = prev;
	return key;
}
void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);
	STPush(&s, end);
	STPush(&s, begin);

	while (!STEmpty(&s))
	{
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);

		int keyi = PSort(a, left, right);
		// [left, keyi-1] keyi [keyi+1, right]
		if (left < keyi - 1)
		{
			STPush(&s, keyi - 1);
			STPush(&s, left);
		}

		if (keyi + 1 < right)
		{
			STPush(&s, right);
			STPush(&s, keyi + 1);
		}
	}

	STDestroy(&s);
}

下面是栈的头文件和.c文件

#include <stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef struct Stack
{
	int* a;
	int top;		// 标识栈顶位置的
	int capacity;
}ST;
void STInit(ST* pst);//初始化栈
void STDestroy(ST* pst);//栈的销毁
void STPush(ST* pst, int x);//栈顶插入
void STPop(ST* pst);//栈顶删除
int STTop(ST* pst);//获取栈顶元素
bool STEmpty(ST* pst);//检查栈是否为空
int STSize(ST* pst);//获取栈中元素的个数
#include "stack.h"
void STInit(ST* pst)
{
	/*ST* tmp = (ST*)malloc(sizeof(ST));
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}*/
	pst->a = NULL;
	pst->top = -1;
	pst->capacity = 0;
}
void STPush(ST* pst, int x)
{
	assert(pst);
	if (pst->top == pst->capacity - 1)
	{
		int newcapacite = pst->capacity == 0 ? 4 : pst->capacity * 2;
		int* tmp = (int*)realloc(pst->a, newcapacite * sizeof(int));
		if (tmp == NULL)
		{
			perror("realloc");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacite;
	}
	pst->a[pst->top + 1] = x;
	pst->top++;
}
void STPop(ST* pst)
{
	assert(pst);
	pst->top--;
}
int STTop(ST* pst)
{
	assert(pst);
	if (pst->top == -1)
	{
		printf("此栈为空");
		return 1;
	}
	return pst->a[pst->top];
}
bool STEmpty(ST* pst)
{
	assert(pst);
	return pst->top == -1;
}
int STSize(ST* pst)
{
	assert(pst);
	return pst->top++;
}
void STDestroy(ST* pst)
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = 0;
	pst->top = -1;

}

<think>嗯,用户想了解Java中ForkJoinTask的作用和使用场景。这个问题很有深度,看来用户可能正在学习并发编程或者需要优化计算密集型任务。 从用户引用的资料来看,ForkJoinTask是Fork/Join框架的核心组件,主要用于将大任务拆分成小任务并行处理。这让我想到几个关键点需要解释清楚:首先是任务拆分机制(fork),然后是结果合并机制(join),最后是工作窃取算法如何提升效率。 用户提供的参考资料很有价值。引用[1]和[2]说明了ForkJoinTask的基类地位及其两个具体子类RecursiveTask和RecursiveAction的区别;引用[3]则揭示了底层的工作窃取机制。这些正好构成回答的骨架。 考虑到用户可能实际编码需求,应该给出具体使用模板。对于有返回值的计算(如数列求和),用RecursiveTask示例最合适;对于无返回值的操作(如并行处理数组),则展示RecursiveAction用法。用户提到性能分析,所以阈值设置和任务粒度控制这些优化点也要强调。 用户似乎更关注实际应用而非理论,所以避免过多讨论AQS队列实现细节,重点放在使用模式上。但需要说明适用场景——计算密集型且可分解的任务才适合,像IO操作就不合适。 最后生成的问题要覆盖用户可能的后续疑问:性能优化、异常处理、线程池配置等实用技巧。特别是要提醒common pool的线程数限制,很多开发者容易忽略这点导致性能不达预期。</think>好的,我们来详细解析 Java 中 `ForkJoinTask` 的作用及其使用指南。 **Java ForkJoinTask 的作用及核心概念** `ForkJoinTask<V>` 是 Java Fork/Join 框架的核心抽象类,代表一个可以在 `ForkJoinPool` 中执行的任务。它的主要作用是为**可递归分解的并行计算**提供高效的执行模型。其核心思想是“分而治之”(Divide and Conquer): 1. **分解任务 (Fork):** 一个大型任务可以被递归地拆分成更小的子任务(通常是相同类型),直到子任务足够小,可以直接计算(达到设定的阈值)。使用 `fork()` 方法可以将子任务异步提交到 `ForkJoinPool` 中执行。 2. **执行任务:** 被拆分的子任务(包括最初的大任务)在 `ForkJoinPool` 的线程中并发执行。 3. **合并结果 (Join):** 当一个任务需要其子任务的结果时,它调用 `join()` 方法等待子任务完成并获取其结果。父任务可以继续执行其他工作或在必要时阻塞等待子任务结果。 4. **工作窃取 (Work-Stealing):** 这是 `ForkJoinPool` 实现高性能的关键。每个工作线程维护一个双端队列(Deque)来存放自己的任务。当一个线程完成了自己队列中的所有任务时,它可以从其他忙碌线程的队列“尾部”窃取任务来执行。这有效地平衡了负载,减少了线程空闲时间[^3]。 **使用场景** `ForkJoinTask` 特别适合解决以下类型的问题: 1. **计算密集型任务:** 需要大量 CPU 计算,而非等待 I/O 操作。 2. **可递归分解的任务:** 问题本身可以被自然地分解成更小的、相似的子问题。 3. **子任务相互独立或依赖关系简单:** 子任务之间没有复杂的依赖关系,或者依赖关系可以通过 `join()` 清晰地表达(通常是父任务等待子任务的结果)。 4. **子任务工作量大致均衡:** 虽然工作窃取能处理一定的不均衡,但任务划分应尽量使子任务工作量相近,以获得最佳性能。 **常见应用示例:** * **大型数组/集合处理:** 排序(如归并排序、快速排序)、搜索、并行遍历、过滤、映射(如 `Arrays.parallelSort()`, `Stream.parallel()` 底层可能使用 Fork/Join)。 * **数值计算:** 大规模矩阵运算、数值积分(如蒙特卡洛模拟)、求解线性方程组。 * **递归算法:** 斐波那契数列、遍历树或图结构(在合适的情况下)。 * **图像处理:** 分块处理图像(如应用滤镜、转换)。 **使用指南:继承 `RecursiveTask` 或 `RecursiveAction`** 开发者通常不直接继承 `ForkJoinTask`,而是使用它的两个更具体的抽象子类: 1. **`RecursiveTask<V>`:** 用于**有返回值**的任务。 * 需要重写 `compute()` 方法。 * 在 `compute()` 中实现任务分解逻辑(`fork`)和结果合并逻辑(`join`)。 2. **`RecursiveAction`:** 用于**没有返回值**的任务。 * 需要重写 `compute()` 方法。 * 在 `compute()` 中实现任务分解逻辑(`fork`)和同步(`join` 用于等待子任务完成),但不需要合并计算结果。 **核心方法** * **`final ForkJoinTask<V> fork()`:** 将当前任务异步提交到当前任务所在的 `ForkJoinPool` 中执行。调用 `fork()` 后,当前线程可以继续执行其他任务。通常在 `compute()` 方法内调用此方法提交新创建的子任务。 * **`final V join()`:** 等待任务计算完成并返回计算结果(对于 `RecursiveTask`)。如果任务被取消或遇到未检查异常,`join()` 会抛出异常。通常在 `compute()` 方法内调用此方法等待并获取子任务的结果。 * **`protected abstract V compute()` (在 `RecursiveTask`/`RecursiveAction` 中):** 这是任务执行的核心方法,需要由子类实现。在此方法中包含: * 判断任务是否足够小(达到阈值),如果是则直接计算(**基本情形**)。 * 如果任务较大,则将任务拆分成子任务。 * 调用子任务的 `fork()` 提交子任务。 * 调用子任务的 `join()` 等待子任务完成并获取结果(对于 `RecursiveTask`)。 * 合并子任务的结果(对于 `RecursiveTask`)或执行必要的后处理(对于 `RecursiveAction`)。 **使用步骤 (以 `RecursiveTask` 为例)** 1. **定义任务类:** 创建一个继承自 `RecursiveTask<V>` 的类(`V` 是结果类型)。 2. **定义构造函数:** 传递任务需要处理的数据范围(如数组起始索引、结束索引)。 3. **实现 `compute()` 方法:** * 检查是否达到阈值(可直接计算的规模)。如果是,直接计算结果并返回(基本情形)。 * 如果未达到阈值: * 计算中点或拆分点。 * 创建两个(或多个)新的子任务实例,分别处理拆分后的子问题。 * 调用 `leftTask.fork()` 和 `rightTask.fork()` 提交子任务(异步执行)。 * 调用 `rightResult = rightTask.join()` 和 `leftResult = leftTask.join()` 等待子任务完成并获取结果(注意:`join()` 调用顺序很重要,通常先 `fork` 所有子任务,再 `join`,或一个 `fork` 紧接着其 `join` 以优化)。 * 合并 `leftResult` 和 `rightResult` 得到最终结果并返回。 4. **创建 ForkJoinPool:** 可以显式创建 `ForkJoinPool`,或者使用公共池 `ForkJoinPool.commonPool()`。 5. **提交任务并获取结果:** 创建你的任务实例,调用 `pool.invoke(task)` 或 `task.invoke()` 提交任务并等待结果,或者调用 `pool.submit(task)` 获取 `ForkJoinTask<V>` 对象后再调用其 `get()` 或 `join()`。 **示例代码:使用 `RecursiveTask` 计算数组元素和** ```java import java.util.concurrent.RecursiveTask; import java.util.concurrent.ForkJoinPool; public class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 1000; // 阈值,小于此值则直接计算 private final long[] array; private final int start; private final int end; public SumTask(long[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { int length = end - start; // 1. 基本情形:如果任务足够小,直接计算 if (length <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } // 2. 分解任务 int middle = start + length / 2; SumTask leftTask = new SumTask(array, start, middle); // 左半部分求和任务 SumTask rightTask = new SumTask(array, middle, end); // 右半部分求和任务 // 3. 异步执行子任务 (fork) leftTask.fork(); // 提交左任务异步执行 // rightTask.fork(); // 也可以选择fork右任务,但下面有优化写法 // 4. 等待右任务完成并获取结果 (直接在当前线程计算右任务可能更高效) // 方案A: 都fork,然后都join (可能增加任务队列开销) // rightTask.fork(); // long rightResult = rightTask.join(); // long leftResult = leftTask.join(); // 方案B: 优化 - 一个fork,一个在当前线程compute (推荐) long rightResult = rightTask.compute(); // 直接在当前线程计算右半部分 long leftResult = leftTask.join(); // 等待之前fork的左任务完成 // 5. 合并子任务结果 return leftResult + rightResult; } public static void main(String[] args) { long[] array = ... // 初始化一个很大的数组 ForkJoinPool pool = new ForkJoinPool(); // 或使用 ForkJoinPool.commonPool() SumTask task = new SumTask(array, 0, array.length); Long result = pool.invoke(task); // 提交任务并等待结果 System.out.println("Sum: " + result); } } ``` **关键注意事项与最佳实践** 1. **选择合适的阈值 (`THRESHOLD`):** 这是性能调优的关键。阈值太小会导致任务过多,管理开销增大;阈值太大则无法充分利用并行性。需要通过基准测试找到最佳点。 2. **避免不必要的任务拆分:** 在 `compute()` 方法中,总是先检查是否达到基本情形(任务足够小)。 3. **高效使用 `fork()` 和 `join()`:** * 优先使用 `invokeAll(task1, task2, ...)` 来提交多个任务并等待它们全部完成,这比连续调用 `fork()` 更简洁高效。 * 考虑“顺序执行优化”:对于拆分的子任务,可以 `fork` 一个,然后在当前线程直接调用另一个子任务的 `compute()` 方法(如示例中的方案B),这样可以减少一个任务入队的开销。然后 `join()` 之前 `fork` 的那个任务。 * 避免在 `join()` 之前 `fork()` 所有任务后不做其他工作而直接 `join()`,这可能导致线程阻塞等待。 4. **避免阻塞操作:** Fork/Join 框架设计用于计算密集型任务。在任务中进行阻塞 I/O 操作会严重影响性能,因为工作线程会被阻塞,无法执行其他任务,也会阻碍工作窃取。 5. **结果合并应轻量:** 合并子任务结果的操作(在 `RecursiveTask` 的 `compute()` 最后)应该尽量简单快速。复杂的合并操作可能成为瓶颈。 6. **任务独立性:** 确保子任务尽可能独立操作不同的数据区域,避免共享可变状态带来的同步开销和复杂性。如果必须共享状态,需使用线程安全机制(如 `java.util.concurrent.atomic` 包中的类或显式锁),但这会降低可扩展性。 7. **使用公共池谨慎:** `ForkJoinPool.commonPool()` 是一个全 JVM 共享的池。对于非计算密集型或长时间运行的任务,使用它可能会耗尽公共池资源,影响其他使用公共池的代码(如 `parallel streams`)。对于特定任务或需要特殊配置(线程数、线程工厂等)时,最好创建自己的 `ForkJoinPool` 实例。 8. **异常处理:** `ForkJoinTask` 会捕获任务执行过程中的异常。调用 `get()` 或 `join()` 时,如果任务抛出了异常,这些方法会抛出 `ExecutionException`(`get()`)或 `CancellationException` / `RuntimeException`(`join()`)。可以在 `compute()` 中使用 `try-catch` 处理任务内部特定异常。 **总结** `ForkJoinTask`(及其子类 `RecursiveTask` 和 `RecursiveAction`)是 Java 提供用于高效执行可递归分解并行计算的强大工具。它通过 `fork()` 进行任务分解与异步提交,通过 `join()` 进行结果等待与合并,底层依赖 `ForkJoinPool` 的工作窃取算法实现高效的负载均衡。它最适合计算密集、可分解、子任务相对独立的问题。使用时需注意阈值设定、`fork/join` 调用模式、避免阻塞操作和合理管理共享状态,才能充分发挥其并行性能优势[^1][^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值