当前位置: 首页 > news >正文

【排序算法】——快速排序

目录

  • 1.快速排序的核心思想
  • 2.快速排序的实现方式
    • 2.1 递归版本
      • 2.1 hoare版本
      • 2.2 lomuto版本
    • 2.2 非递归版本(仅了解即可)
  • 3.以上实现的快排方式的缺陷
  • 4.快排的优化
    • 4.1 基准值优化
    • 4.3 优化后的测试
    • 4.2 基准值分化区间优化

1.快速排序的核心思想

快速排序是排序算法中较为优异的一种,它的核心思想就是:选取一个数据作为基准值,然后遍历需要排序的数据,使这个基准值放到它该放的位置,再对剩下的子区间进行递归,最后所有的数据都会放到它该放的位置,就完成了排序

2.快速排序的实现方式

这里我们先默认选取每一次递归区间的最左边的元素作为基准值,当然这种选取方式其实在某些特殊的数据存在很大的问题(待会再讨论)

2.1 递归版本

2.1 hoare版本

a.思路:给定left指针(代表下标)和right指针,分别从数据的左边和右边进行遍历,left指针从左往右找大,right指针从右往左找小,这里的比较是与基准值

b.画图展示:
注意:这里可以先让left指针先走一步,让比较的次数减少,一定程度上优化了一点点

在这里插入图片描述
在这里插入图片描述
从画图的方式可知:>思路(续):left指针和right指针找的过程当left比right大则停止,此时right指向的位置就是keyi该放的地方,交换即可

这里的递归左右区间的意思是:交换完keyi和right以后,该轮的keyi已经排序了,只要再对它的左右两段区间重复该一操作即可(递归),如图:
在这里插入图片描述
跳出循环的right会作为基准值返回,有了基准值就能得到左右区间的范围了

c. 代码实现:
这里将找基准值的方法和主体逻辑进行分离:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//找基准值的方式
int  re_QuickSort(int* arr, int left, int right)
{
	int keyi = left;
	++left;
	//这里left与right取等也要进入循环
	//如果它们相遇时指向的值比基准值大,不进入循环指向的元素就没有放到该放的位置
	while (left <= right)
	{
		//right从右往左找比基准值小的数
		// left从左往右找比基准值大的数
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}

		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}

		if (left <= right)
		{
		    //让left和right继续走
			Swap(&arr[right--], &arr[left++]);
		}
	}
	//在right下标和left下标交换后,left和right都会移动
	//可能在移动过程就不满足left <=right
	//所以让right与keyi进行交换应在循环外
	Swap(&arr[right], &arr[keyi]);
	return right;
}

//主体
void reQuickSort(int* arr, int left, int right)
{
	//保证区间是有效的
	if (left >= right)
	{
		return;
	}

	int keyi = re_QuickSort(arr, left, right);
	//将区间分为[left,keyi-1]   [keyi+1,right]
	reQuickSort(arr, left, keyi - 1);
	reQuickSort(arr, keyi + 1, right);

}

另:
在上面我们让left先走了一步,但是如果不让left先走一步,应该怎样处理呢?
如图:
在这里插入图片描述
在这里插入图片描述
注意:这里是为了画图方便在交换位置少了&操作符,它只是一段“伪代码”

在这里插入图片描述
此时如果再使用原来找keyi的方法只会让那两元素不断进行交换造成死递归,通过该图可以发现,这里只要right < keyi,此时的right在该段区间就是属于非法的,所以只需在跳出循环的时候判断两者即可,如果非法返回keyi

代码实现:

//left不++的基准值选取方法
int noneleft_Quick_sort(int* arr, int left, int right)
{
	int keyi = left;
	while (left <= right)
	{
		//right找小,left找大
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}

		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}

		if (left <= right)
		{
			Swap(&arr[left++], &arr[right--]);
		}

	}
	if (right < keyi)
	{
		return keyi;
	}
	else
	{
		//尽量进行有效交换
		if (right != keyi)
		{
			Swap(&arr[keyi], &arr[right]);
		}
		return right;
	}

}

笔者建议:
从这个例子就可以看出,当我们想要改动代码的时候会造成不可预料的结果,如果要进行改动,需要弥补相应的措施,动手画图 + 调试会渐渐让你变得富有经验,耐心一点儿,问题总会解决的

2.2 lomuto版本

前言:该一实现方式又名双指针法,在初次接触的时候会感觉非常奇妙很难想,但是只要理解了背后的算法设计,就觉得还好

a.思路:
1.定义两个指针prev(left位置),pcur(left+1位置)
2.pcur在前面探路,只要pcur下标的元素比keyi小,就先让prev++,再与pcur++交换;如果比keyi大,直接++
3.跳出循环的时候,prev此时的指向就是基准值该待的位置,与keyi交换

b.代码实现:

//双指针版本
int dp_QuickSort(int* arr, int left, int right)
{
	int keyi = left;
	int prev = left, pcur = prev + 1;
	while (pcur <= right)
	{
		//pcur探路,找到比基准值小的数,先++prev然后与pcur交换
		//如果没有找到比基准值小的数,pcur++
		//pcur越界了,此时prev指向的就是keyi该待的位置
		if (arr[pcur] < arr[keyi] && ++prev != pcur)
		{
			Swap(&arr[prev], &arr[pcur]);
		}
		++pcur;
	}
	Swap(&arr[prev], &arr[keyi]);
	return prev;
}

void dpQuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int keyi = dp_QuickSort(arr, left, right);
	dpQuickSort(arr, left, keyi - 1);
	dpQuickSort(arr, keyi + 1, right);

}

c.算法思想:
pcur对整个数组进行遍历,需要指向right(也是有效区间值)才结束,这里本质上其实就是对数组中进行三区间分化:

在这里插入图片描述
这里可以看出:从left~prev指向的区域都是≤keyi(5)的:

在这里插入图片描述

2.2 非递归版本(仅了解即可)

在非递归版本的快排实现中,我们需要借助一种数据结构:栈,主要的思路就是将区间放入栈中,并不断入栈出栈

代码实现:

void QuickSortNonR(int* arr, int left, int right)
{
	ST stack;
	STInit(&stack);

	//先将left和right入栈
	//注意栈的顺序:先进后出
	StackPush(&stack,right);
	StackPush(&stack,left);

	while (!StackEmpty(&stack))
	{
		int begin = StackTop(&stack);
		StackPop(&stack);
		int end = StackTop(&stack);
		StackPop(&stack);
		int prev = begin, pcur = begin + 1;
		int keyi = begin;
		while (pcur <= end)
		{
			//pcur探路,找到比基准值小的数,先++prev然后与pcur交换
			//如果没有找到比基准值小的数,pcur++
			//pcur越界了,此时prev指向的就是keyi该待的位置
			if (arr[pcur] < arr[keyi] && ++prev != pcur)
			{
				Swap(&arr[prev], &arr[pcur]);
			}
			++pcur;
		}
		Swap(&arr[prev], &arr[keyi]);
		keyi =  prev;


		//区间:[begin,keyi-1] [keyi+1,end]
		if (keyi + 1 < end)
		{
			StackPush(&stack,end);
			StackPush(&stack, keyi+1);
		}
		if (begin < keyi - 1)
		{
			StackPush(&stack, keyi - 1);
			StackPush(&stack,begin);
		}
	}

	STDestroy(&stack);
}

3.以上实现的快排方式的缺陷

上述快排的实现,我们都直接选取了最左边的元素为基准值,从各个图进行分析就可以看出快速排序一般来说就是一颗递归树,递归树的时间复杂度为O(n*logn),但是碰到刚好完全有序的数据或者存有大量相同数据时,快排就会退化

本身就有序的情况:
这里以双指针版进行演示:

在这里插入图片描述
可以看到这里的快排就不是一颗递归树了,如果当存在大量数据时,效率就变得非常低,所以对于基准值的选取需要更进

4.快排的优化

优化大致就是对提到的两种情况进行相应的处理,尽量使得快排为一颗递归树

4.1 基准值优化

1.三数取中
要想让递归的区间从中间开始,就需要选取的基准值处在数据中不大也不小的状态,这样选取的基准值在第一轮时就会被放到中间,进而就大大缩减了递归的次数

代码实现:

//三数取中 -- 三个数中取中位数,基准值的选择尽量为数组中不大不小的元素
//如果只让基准值处于较大/较小的元素,递归的次数会增多,时间复杂度退化至o(n^2)
int midthree(int* arr, int left, int mid, int right)
{
	//0^1=1^0=1,0^0=1^1=0
	if ((arr[left] < arr[mid]) ^ (arr[left] < arr[right]))
	{
		//说明:ar[left]不大也不小
		return left;
	}
	else if ((arr[mid] < arr[left]) ^ (arr[mid] < arr[right]))
	{
		//说明:arr[mid]不大也不小
		return mid;
	}
	else
	{
		//说明:arr[right]不大也不小
		return right;
	}
}

优化后的快排——hoare版本

//三数取中版本——针对有序的数组做了优化,但是处理不了含有大量重复数据的情况
int  mid_QuickSort(int* arr, int left, int right)
{
	int med = midthree(arr, left, (left + right) / 2, right);
	//让中位数与最左边元素交换
	Swap(&arr[med], &arr[left]);
	int keyi = left;
	++left;

	//这里left与right取等也要进入循环
	//如果它们相遇时指向的值比基准值大,不进入循环指向的元素就没有放到该放的位置
	while (left <= right)
	{
		//right从右往左找比基准值小的数
		// left从左往右找比基准值大的数
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}

		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}

		if (left <= right)
		{
			Swap(&arr[right--], &arr[left++]);
		}
	}
	//在right下标和left下标交换后,left和right都会移动
	//可能在移动过程就不满足left <=right
	//所以让right与keyi进行交换应在循环外
	Swap(&arr[right], &arr[keyi]);

	return right;
}

void midQuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int keyi = mid_QuickSort(arr, left, right);

	midQuickSort(arr, left, keyi - 1);
	midQuickSort(arr, keyi + 1, right);

}

4.3 优化后的测试

这里以一道排序算法题作为测试:数组排序

优化前:
在这里插入图片描述

优化后:
在这里插入图片描述
2.随机数作基准值
这里还提供了另外一种方法作基准值:使用随机数,该数的范围在left~right之间,这种方式在算法导论中有进行严格的数学推到证明,有兴趣的读者可以自行了解

代码实现:

//设置种子
srand((unsigned int)time(NULL));
int keyi = arr[rand() % (right - left + 1) + left];

4.2 基准值分化区间优化

前言:当数组中含有大量相同数据时,我们实现的递归还是会退化,有了前面双指针算法的思想,可以衍生出另一个算法:三指针;双指针算法只是将区域分化成两部分:≤keyi、>keyi,但是如果再进一步划分区域:将=keyi的区域单独划分,那么快排的算法效率就会得到显著上升:

算法思想:
pleft:标记左区间,处理最后一个<keyi的位置
pright:标记右区间,处理第一个>keyi的位置
pcur:遍历整个数组

如图:
在这里插入图片描述

处理时的行为:
这里大于keyi时的情况已经在双指针讨论过了,这里照搬即可,等于keyi时,直接++即可,就剩下小于keyi时,我们用pright处理,这里只需先–pright,再与pcur交换,但是注意:pcur此时还不能动,因为–pright指向的元素还是不确定的,需要pcur进行判断,最后遍历完以后只需递归<keyi区间和>keyi区间,这种实现对于大量相同数据做了很好的处理

代码实现:

void tpQuickSort(int* arr, int left, int right)
{

	if (left >= right)
	{
		return;
	}
	//使用pleft、pright进行三路划分
	//采用随机取keyi的方式
	srand((unsigned int)time(NULL));
	int keyi = arr[rand() % (right - left + 1) + left];
	//int keyi = arr[left];
	int pleft = left - 1, pright = right + 1;
	int pcur = left;
	while (pcur < pright)
	{
		if (arr[pcur] < keyi)
		{
			Swap(&arr[++pleft], &arr[pcur++]);
		}
		else if (arr[pcur] > keyi)
		{
			Swap(&arr[--pright], &arr[pcur]);
		}
		else
		{
			pcur++;
		}
	}

	//(left,pleft)   (pleft+1,pright-1)   (pright,right)
 	// < keyi              ==keyi                 >keyi
	tpQuickSort(arr, left, pleft);
	tpQuickSort(arr, pright, right);
}

相关文章:

  • 数据分析异步进阶:aiohttp与Asyncio性能提升
  • Kafka自定义分区机制
  • HTTP和RPC的区别
  • 稳定运行的以Microsoft Azure SQL database数据库为数据源和目标的ETL性能变差时提高性能方法和步骤
  • 大模型之蒸馏模型
  • HashMap添加元素的流程图
  • Fiddler使用(一)
  • 嵌入式八股,什么是线程安全
  • 稀疏矩阵的存储
  • 美团 web 最新 mtgsig1.2
  • Spring MVC 拦截器使用
  • 大模型详细配置
  • 人工智能之数学基础:线性方程组求解的得力助手——增广矩阵
  • HarmonyOS Next~鸿蒙系统架构设计解析:分层、模块化与智慧分发的技术革新
  • DeDeCMS靶场攻略
  • pytest的测试报告allure
  • MongoDB 配合python使用的入门教程
  • 微软产品的专有名词和官方视频教程
  • 柔性PZT压电薄膜触觉传感器在人形机器人的应用
  • Android Launcher3终极改造:全屏应用展示实战!深度解析去除Hotseat的隐藏技巧
  • 内塔尼亚胡:以军将在未来几天“全力进入”加沙
  • 三亚通报救护车省外拉警报器开道旅游:违规违法,责令公司停业整顿
  • 首映|奥斯卡最佳国际影片《我仍在此》即将公映
  • 2025上海科技节本周六启幕,机器人和科学家同走AI科学红毯
  • 专访|西蒙·斯特朗格:以“辞典”的方式讲述二战家族史
  • 5年建成强化城市核心功能新引擎,上海北外滩“风景文化都是顶流”