快速排序——霍尔排序,前后指针排序,非递归排序

快速排序(Quick Sort)

基本思想

快速排序由 C. A. R. Hoare 于 1962 年提出,是一种基于 分治法 的高效交换排序算法。

  • 任取待排序序列中的一个元素作为 基准值(pivot)
  • 将序列划分为两个子区间:
    • 左子区间:所有元素 < 基准值
    • 右子区间:所有元素 > 基准值
  • 对左右子区间 递归执行相同操作,直到子区间长度 ≤ 1。

该递归过程与 二叉树的前序遍历 高度相似:先处理当前节点(分区),再递归处理左子树和右子树。


递归实现

Hoare 分区法 左基准版

c 复制代码
void Swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//该排序由于固定选取边界值(左值)作为key,所以当数组排序为顺序或逆序时,每次遍历交换后key的位置只会在最左侧或最右侧,无法将数组分为两部分递归,递归深度就从本来理想情况(每次key都在中间)的logN变成了N,容易发生栈溢出。每次处理的数据总量都是N,对于理想情况,所有数据在每一层都只处理一次,大致为N*logN,但是对于最坏情况,每层数据都要被重复处理情况是n-1,n-2...,1次,合计为n²,所以总效率就从N*logN变成了N²。
void QuickSort(int* arr,int left,int right)
{

	if (left >= right)return;//其实递归条件就是序列被划分成若只干有一个元素的序列。
	int begin = left;//一定不能取left+1,否则没有检查自身的话原数组本身是升序时会造成交换错误
	int end = right; 
	int key = left;
	while (begin<end)
	{
		while (arr[key] <= arr[end] && begin < end)//一定要先移动右指针再移动做左指针,arr[key]必须小于等于其要交换的值,否则不能保证其交换后顺序完全正确,其交换的值如果大于arr[key],那么该值左边的值也可能大于arr[key],不符合快排的要求;先移动右指针再移动左指针,可以保证key最后一定停留在右指针end最后停留的位置,也就是该位置的值一定小于arr[key],而先走左指针最终停留的位置一定是大于pivot基准的位置。
		{
			end--;
		}
		while (arr[key] >= arr[begin] && begin < end)
		{
			begin++;
		}
		
		Swap(&arr[end], &arr[begin]);
	}
	Swap(&arr[begin], &arr[key]);//如果取left+1,原数组升序时此处会造成交换错误,因为会将较大值放在最左边,但是下次递归时又不检查最左侧
	key = begin;
	QuickSort(arr, left, key - 1);
	QuickSort(arr, key + 1, right);

}

该排序实现是固定选取下标begin位置作为key,使用双指针从数组头和数组尾双向遍历数组,将大与基准值的元素交换到右边,小于基准值的元素交换到左边,最终,将key元素交换到两指针相遇的位置,将该位置的原元素移动至下标begin。接着进入递归,将原数组分为左右两部分,借助递归对左右两部分实行相同操作。当"递"至某一部分只剩一个元素时,即排序完成,可以返回。但是这种以固定位置作为基准值的写法在遇到完全顺序或逆序时,就会成为最坏情况,因为基准值无法将数组分为两部分,会导致递归深度大幅度增加。

快速排序统一优化

无论是何种思想实现的快速排序,针对基准值选取,如果该值是待排序部分的最大值或最小值,就会大大增加递归深度或者循环层数,所以我们需要通过对key进行筛选来进行优化。

优化方法主要有两种,其一是随机选取基准值,第二种方法更为稳定,三数取中,从待排序数组中,取左中右三处坐标,取其中中值元素作为基准值,可以大大减少选取到最值的概率。

c 复制代码
int GetMiddle(int *arr,int left,int right)
{
	int mid = (left + right) / 2;


	if (arr[left] > arr[right])
	{
		if (arr[mid] < arr[right]) { return right; }
		else if (arr[mid] < arr[left]) { return mid; }
		else return left;
	}
	else//arr[left]<arr[right]
	{
		if (arr[mid] < arr[left]) { return left; }
		else if (arr[mid] > arr[right]) { return right; }
		else return mid;
	}
}

同时,结合插入排序,当元素过少时使用插入排序可以大幅提高运行效率,减少递归深度。


快速排序:前后指针法(Prev-Cur Two Pointers)

核心思想

  • 使用两个指针 prevcur 从左向右遍历;
  • prev 维护 小于基准值(pivot)的区域右边界
  • cur 负责扫描整个数组;
  • arr[cur] < pivot 时,扩展小值区,并在必要时交换。

算法步骤

  1. 选取 arr[left] 作为 pivot
  2. 初始化 prev = left, cur = left + 1
  3. 遍历 curleft+1right
    • arr[cur] < pivot
      • prev++
      • prev != cur,交换 arr[prev]arr[cur]
  4. 循环结束后,交换 arr[left]arr[prev],使 pivot 归位;
  5. 递归排序 [left, prev-1][prev+1, right]

代码实现

c 复制代码
void QuickSortPrevCur(int *arr,int left,int right)//前后指针实现快速排序
{
	int prev = left;
	int cur = prev + 1;
	int key = left;
	if (left >= right)return;//其实递归条件就是序列被划分成若只干有一个元素的序列。
	while (cur <= right)
	{
		if (arr[cur] < arr[key] && ++prev != cur)//与运算是短路求值,如果左值为false,右值不会执行,所以实际执行结果就是,当cur遇见大值时prev就不会移动,停留在小值的位置上(下一个位置为大值),直到cur遇见小值,且prev的下一个位置不是cur,prev会进入那个大值并将它与cur所在的小值交换。根据此规则,循环结束后,prev 指向的是小于 pivot 的最后一个元素的位置,即分区点。pivot 应与 arr[prev] 交换,使其归位。
		{
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	Swap(&arr[prev], &arr[key]);//没有在内层交换时,prev永远处于小值,当cur移动出数组时,prev一定在最后一个最小值处,也就是大值和小值的分割点。
	QuickSortPrevCur(arr, left, prev - 1);//prev才是此时key所在位置的真正下标
	QuickSortPrevCur(arr, prev + 1, right);
}

非递归快速排序(基于栈,按自定义压栈顺序实现)

核心思想

使用栈手动模拟递归过程。

本实现采用如下约定:

  • 压栈顺序 :对于区间 [left, right]先压 right,再压 left
  • 出栈顺序 :由于栈是 LIFO(后进先出),出栈时先取出 left,再取出 right
  • 只要压栈与出栈的顺序保持一致,逻辑即自洽,排序结果正确。

代码实现

c 复制代码
//单次排序
int PartSort(int* arr, int left, int right) {
    int prev = left;
    int cur = prev + 1;
    int key = left; // 基准值下标

    while (cur <= right) {
        // 利用短路求值:仅当 arr[cur] < arr[key] 时才执行 ++prev
        if (arr[cur] < arr[key] && ++prev != cur) {
            Swap(&arr[cur], &arr[prev]);
        }
        cur++;
    }
    Swap(&arr[prev], &arr[key]); // 基准值归位
    return prev; // 返回基准值最终位置
}

void StackQuickSort(int* arr, int left, int right)//借助栈实现非递归快速排序
{
	Stack s1;
	STInit(&s1);
	STPush(&s1, right);
	STPush(&s1, left);

	while (!STEmpty(&s1))//栈为空时,代表所有元素已经排序完成
	{
		
		left = STTop(&s1);
		STPop(&s1);
		right = STTop(&s1);
		STPop(&s1);

		int key = PartSort(arr, left, right);//本质上还是前序遍历
		if (key + 1 < right)//先压右部分数组,这样在出栈时相当于递归调用中的最后调用,因为排序时要先把所有被分割数组的左边都排序完才能排序右边。
		{
			STPush(&s1, right);//与最开始保持一至,先压要排序列的右边界,再压左边界
			STPush(&s1, key + 1);
			
		}
			
		if (key - 1 > left)//后压左部分数组,这样在出栈时先对左部分数组进行排序,下次循环压栈时也是先压右后压左,先出左后出右,由于左子区间后入栈、先出栈,算法会深度优先地处理左侧数组,直到叶子节点(单元素),然后回溯处理右侧子树(数组)。"
			//。并且右侧数组出栈时也是从最底层(两个元素为一组的序列)开始出栈排序。
		{
			STPush(&s1, key - 1);
			STPush(&s1, left);
			
		}
	}

}
相关推荐
齐落山大勇2 小时前
数据结构——单链表
数据结构
Tansmjs2 小时前
C++编译期数据结构
开发语言·c++·算法
金枪不摆鳍2 小时前
算法-字典树
开发语言·算法
diediedei3 小时前
C++类型推导(auto/decltype)
开发语言·c++·算法
皮皮哎哟3 小时前
深入浅出双向链表与Linux内核链表 附数组链表核心区别解析
c语言·数据结构·内核链表·双向链表·循环链表·数组和链表的区别
独断万古他化3 小时前
【算法通关】前缀和:从一维到二维、从和到积,核心思路与解题模板
算法·前缀和
loui robot3 小时前
规划与控制之局部路径规划算法local_planner
人工智能·算法·自动驾驶
格林威3 小时前
Baumer相机金属焊缝缺陷识别:提升焊接质量检测可靠性的 7 个关键技术,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·计算机视觉·视觉检测·堡盟相机
wWYy.3 小时前
指针与引用区别
数据结构