【初探数据结构】快速排序的四种实现方式(Hoare,挖坑,前后指针,非递归)

💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习!
👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对数据结构感兴趣的朋友

前言

快速排序是一种高效的分治排序算法,核心思想是通过选定基准元素将数组划分为两部分,递归排序子数组。本文详细介绍四种实现方式:Hoare法挖坑法前后指针法非递归实现,并分析其优缺点。


一、Hoare法(左右指针法)

在这里插入图片描述
实现步骤

  1. 选基准:选最左边的元素作为基点
  2. 双指针移动
    • 右指针先向左找比基准小的元素。
    • 左指针向右找比基准大的元素。
      💡:选左作为基点,一定要右指针先动;若选右作基点,就左指针先动
  3. 交换与相遇:交换左右指针的元素,直到左右指针相遇,最后将基准放到相遇位置。
  4. 递归分区:以相遇位置为分界点,递归处理左右子数组。

代码(有缺陷):

int PartSort1(int* a, int left, int right) {
    int key = left;
    while (left < right) {
        while (left < right && a[right] >= a[key]) right--;
        while (left < right && a[left] <= a[key]) left++;
        swap(&a[right], &a[left]);
    }
    swap(&a[key], &a[left]);
    return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
		int keyi = PartSort1(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
}

有没有发现什么端倪呢?😜

发现问题

问题1

当基点每轮都是最大或者最小的元素时,性能会大幅下降,时间复杂度来到了 O ( N 2 ) O(N^2) O(N2)
在这里插入图片描述
也就是说,会递归N次。

问题2

画图我们可以发现,这个递归的逻辑实际上就是一颗二叉树。
在这里插入图片描述
学过二叉树的我们知道,二叉树随着层数的增加,结点成指数增长,二叉树的最后一层的结点甚至占总结点数的一半,也就是说,递归的深度越大,递归的代价会越来越高。那么,有什么改进的办法呢?

解决问题(优化方案)

选取基准的优化

核心思想:避免基点最大或最小

  1. 随机数定key法
    通过rand函数实现
srand((unsigned int)time(NULL));
	int randi = left + rand() % (right - left);
	swap(&a[left], &a[randi]);
	int key = left;
  1. 三数取中定key法
    通过条件语句即可实现
//三数取中
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] > a[left]) {
		if (a[left] > a[right]) {
			return left;
		}
		else if (a[mid] < a[right]) {
			return mid;
		}
		else {
			return right;
		}
	}
	else {
		if (a[left] < a[right]) {
			return left;
		}
		else if (a[mid] > a[right]) {
			return mid;
		}
		else {
			return right;
		}
	}
}

int main()
{
	int mid = GetMidNumi(a, left, right);
	if (mid != left)
		swap(&a[left], &a[mid]);
	int key = left;
}

💡:三数取中法的效果略高于随机定数,因为随机定数依然有概率选到最大最小数(尽管这可能是极小概率事件)
后文选取基点均用三数取中法。

区间优化

递归的最后几层不再进行递归,直接用插入排序即可
插入排序不再赘述【初探数据结构】直接插入排序与希尔排序详解

if ((right - left + 1) > 10) {
		int keyi = PartSort1(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
		InsertSort(a+left,(right-left+1));

正确代码:

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	//随机数定key
	/*srand((unsigned int)time(NULL));
	int randi = left + rand() % (right - left);
	swap(&a[left], &a[randi]);*/

	//三数选中定key(效果相比随机法相对较高)
	int mid = GetMidNumi(a, left, right);
	if (mid != left)
		swap(&a[left], &a[mid]);
	int key = left;
	while (left < right)
	{
		//right先走
		//right向左找小
		while (left < right && a[right] >= a[key])
		{
			right--;
		}
		//left向右找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		swap(&a[right], &a[left]);
	}
	swap(&a[key], &a[left]);
	key = left;
	return key;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	//区间优化
	if ((right - left + 1) > 10) {
		int keyi = PartSort1(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
		InsertSort(a+left,(right-left+1));
}

二、挖坑法

在这里插入图片描述

实现步骤

  1. 挖坑:选取基准后,将其值保存,原位置视为“坑”。
  2. 填坑
    • 右指针向左找比基准小的元素,填入左坑,形成新坑。
    • 左指针向右找比基准大的元素,填入右坑,形成新坑。
  3. 基准归位:当左右指针相遇时,将基准值填入最后的坑中。
  4. 递归分区

代码要点

int PartSort2(int* a, int left, int right) {
    int mid = GetMidNumi(a, left, right);
    if (mid != left)
    	swap(&a[left], &a[mid]);
    int key = a[left], hole = left;
    while (left < right) {
        while (left < right && a[right] >= key) right--;
        a[hole] = a[right];
        hole = right;
        while (left < right && a[left] <= key) left++;
        a[hole] = a[left];
        hole = left;
    }
    a[hole] = key;
    return hole;
}

优点:减少元素交换次数,逻辑更直观。


三、前后指针法

在这里插入图片描述

实现步骤

  1. 初始化指针prev指向基准位置,curprev+1开始遍历。
  2. 元素交换
    • cur指向的元素小于基准,prev先右移,再交换prevcur的元素。
  3. 基准归位:遍历结束后,将基准与prev位置的元素交换。
  4. 递归分区

代码要点

int PartSort3(int* a, int left, int right) {
    int mid = GetMidNumi(a, left, right);
    if (mid != left) swap(&a[left], &a[mid]);
    int key = left, cur = left + 1, prev = left;
    while (cur <= right) {
        if (a[cur] < a[key] && ++prev != cur) 
            swap(&a[prev], &a[cur]);
        cur++;
    }
    swap(&a[prev], &a[key]);
    return prev;
}

优点:代码简洁,适合单链表排序。


四、非递归实现(栈模拟递归)

实现步骤

  1. 栈初始化:将初始区间[left, right]压入栈。
  2. 循环处理
    • 弹出栈顶区间,进行分区。
    • 将左右子区间压入栈(需注意顺序)。
  3. 终止条件:栈为空时排序完成。

代码要点

void QuickSortNonR(int* a, int left, int right) {
    ST st;
    STInit(&st);
    STPush(&st, right);  // 先压右边界
    STPush(&st, left);   // 再压左边界
    while (!STEmpty(&st)) {
        int begin = STTop(&st); STPop(&st);
        int end = STTop(&st); STPop(&st);
        if (begin < end) {
            int keyi = PartSort3(a, begin, end);
            // 压入右子区间
            STPush(&st, end);
            STPush(&st, keyi + 1);
            // 压入左子区间
            STPush(&st, keyi - 1);
            STPush(&st, begin);
        }
    }
    STDestroy(&st);
}

注意事项

  • 栈操作顺序:需确保先处理左子区间,再处理右子区间,压栈顺序应为右子右界→右子左界→左子右界→左子左界
  • 栈的实现:需确认栈的PushPop操作符合后进先出(LIFO)特性。

五、对比与优化

方法优点缺点
Hoare法经典实现,易于理解需注意指针移动顺序
挖坑法减少交换次数,逻辑清晰代码稍复杂
前后指针代码简洁,适合单链表指针移动需仔细理解
非递归避免栈溢出,适合大数据量需手动管理栈,调试复杂

优化策略

  1. 三数取中:避免最坏时间复杂度。
  2. 小区间插入排序:对长度较小的子数组使用插入排序(如代码中(right-left+1) > 10时优化)。

六、总结

快速排序时间复杂度 O ( N ∗ l o g N ) O(N*logN) O(NlogN)
快速排序的四种实现方式各有适用场景:

  • Hoare法适合教学和理解分治思想。
  • 挖坑法在减少交换次数时表现优异。
  • 前后指针法代码简洁,适合扩展。
  • 非递归实现解决了递归栈溢出的问题,适合工程应用。

gitee源码

理解每种方法的核心逻辑和细节,才能在实际应用中灵活选择最优方案。

安装Docker安装插件,可以按照以下步骤进行操作: 1. 首先,安装Docker。可以按照官方文档提供的步骤进行安装,或者使用适合您操作系统的包管理器进行安装。 2. 安装Docker Compose插件。可以使用以下方法安装: 2.1 下载指定版本的docker-compose文件: curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 2.2 赋予docker-compose文件执行权限: chmod +x /usr/local/bin/docker-compose 2.3 验证安装是否成功: docker-compose --version 3. 在安装插件之前,可以测试端口是否已被占用,以避免编排过程中出错。可以使用以下命令安装netstat并查看端口号是否被占用: yum -y install net-tools netstat -npl | grep 3306 现在,您已经安装Docker安装Docker Compose插件,可以继续进行其他操作,例如上传docker-compose.yml文件到服务器,并在服务器上安装MySQL容器。可以参考Docker的官方文档或其他资源来了解如何使用DockerDocker Compose进行容器的安装和配置。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Docker安装docker-compose插件](https://blog.csdn.net/qq_50661854/article/details/124453329)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [Docker安装MySQL docker安装mysql 完整详细教程](https://blog.csdn.net/qq_40739917/article/details/130891879)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值