快速排序详解(Hoare法, 挖坑法,双指针法,非递归实现, 优化)

前两章讲解了排序中的选择排序和插入排序,这次就来讲一讲交换排序中的快速排序。

快速排序时间复杂度:平均 O(nlogn) 最坏 O(n2)

快速排序,顾名思义,它的排序的效率是非常高的,在数据十分杂乱的时候他的效率甚至能远远超过希尔排序和堆排序。而这个排序的核心思想就是以交换为核心的划分

还是以前的方法,首先讲解单趟的思路。

快速排序单趟的思路,就是先选定某个枢轴,以这个枢轴为基准,展开划分,划分的结果就是,这个枢轴的左边的数据全都比它小,而右边的数据全都比它大。而划分后枢轴的位置也就是它的最终位置,这样就完成了枢轴的归位

一般常用的划分方法有三种:Hoare法,挖坑法,双指针法,下面就来一一讲解

单趟排序—划分


Hoare法

Hoare法的思路非常简单, 就是交换比枢轴大的值和比枢轴小的值
从两端开始,先从前面找到一个比枢轴大的数据,再从尾部找到一个比枢轴小的位置,然后将小的放在前面,大的后面,直到前后走到同一个位置,说明这个位置的前面都是比他小的,后面都是比它大的,所以再将枢轴和这个位置交换,就可以保证枢轴归位

int Hoare(int* arr, int begin, int end)
{
	int pivot = arr[end];
	//保存枢轴数据,用于对比
	int pivot_index = end;
	//保存枢轴下标
	while (begin < end)
	{
		while (begin < end && arr[begin] <= pivot)
		{
			++begin;
		}
		//从前往后找到比枢轴大的数据
		while (begin < end && arr[end] >= pivot)
		{
			--end;
		}
		//从后往前找到比枢轴小的数据
		
		Swap(&arr[begin], &arr[end]);
		//交换两个数据的位置
	}

	Swap(&arr[begin], &arr[pivot_index]);
	//枢轴归位

	return begin;
}

例如
测试数据

	int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
这就是一趟的思路,最后当两个指针走到一起时,该位置与枢轴交换,枢轴归位,左边比他大,右边比他小,单趟划分结束。

这就是Hoare划分的思路


挖坑法

挖坑法的思路的核心就是挖坑填数
首先在枢轴的位置挖一个坑, 然后从前往后找到一个比枢轴大的数据, 填进这个坑中, 再将那个数据的位置也挖一个坑, 然后从后往前找一个比枢轴小的值,填进刚刚的坑中,再将这个位置挖坑。然后循环,直到前后指针指向同一个位置,这时这就是最后一个坑,将最开始挖走的枢轴填进坑中,就完成了划分。
总结下来就是不停的将数据填进上一个数据的坑中,然后再将该数据的位置挖一个坑

int DigHole(int* arr, int begin, int end)
{
	int pivot = arr[end];
	//保留枢轴值,进行对比和填坑
	while (begin < end)
	{
		while (begin < end && arr[begin] <= pivot)
		{
			++begin;
		}

		arr[end] = arr[begin];
		//将比枢轴大的数据填入坑中
		while (begin < end && arr[end] >= pivot)
		{
			--end;
		}

		arr[begin] = arr[end];
		//将比枢轴小的数据填入坑中
	}

	arr[begin] = pivot;
	//将枢轴填入坑中,归位
	
	return begin;
}

测试数据

	int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这就是一趟的排序过程, 还是十分容易理解的


双指针法

这个方法比起前两个方法来说就显得有点难理解, 但是代码也是这三种划分方法中最简洁的

前后指针法的核心思路就是将大的数据推到后面
前指针指向大的数据,后指针指向小的数据,不停交换将大的数据推向后面, 小的数据放在前面。

当遇到cur比枢轴大的数据时, prev停下来, 只让cur走,直到cur走到小于枢轴的数据时,prev前进,如果两者不同,则交换,因为此时prev指向的是原来cur指向的大的数据,而cur是小的数据,两者交换,即可将大的数据推向后面

//双指针法
int PrevCurMethod(int* arr, int begin, int end)
{
	int cur = begin, prev = begin - 1;
	int pivot = arr[end];
	while (cur < end)
	{
  		//当cur指向的数大于枢轴时,就只有cur前进
		//当cur指向的数据小于枢轴时,prev才前进,如果与cur位置不同则交换
		if (arr[cur] < pivot && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	++prev;
	Swap(&arr[cur], &arr[prev]);

	return prev;
}

测试数据

	int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };

在这里插入图片描述
因为前几个数都比34小,所以直到cur走到14时才找到比枢轴小的值,这时prev前进一步走到46,两者不同,交换
在这里插入图片描述
这时26也比34小,prev前进,两者交换
在这里插入图片描述
cur再继续前进,走到27,prev再前进,两者交换
在这里插入图片描述
此时34走到枢轴位置,跳出循环, prev再走一步,这时再让枢轴和prev交换,这就是枢轴的最终位置
在这里插入图片描述
这就是枢轴的最终位置,划分结束


多趟排序

递归

理解了单趟,下面就该进行多趟的排序。因为上面说过,单趟的核心就是划分,每次将数据划分为两个区间,然后再将区间再度划分。
如图:
在这里插入图片描述
是不是看起来有点类似树的结构,每次通过划分将枢轴左右边划分为两个子序列,再对子序列划分,知道所有枢轴归位,数据有序,这就是多趟的思路。

在我们实现树形操作的时候通常都会使用递归来实现,因为这样更加代码更加精简且思路简单。

void QuickSort(int* arr, int begin, int end)
{
	//当begin和end处于同一位置时说明全部子序列都划分结束
	if (begin < end)
	{
		int pivot = PrevCurMethod(arr, begin, end);
		//上面任意一种划分都可以
		
		QuickSort(arr, begin, pivot - 1);
		QuickSort(arr, pivot + 1, end);
		//分别划分枢轴左右端的区间
		//[begin][pivot - 1]和[pivot + 1][end]
	}
}

非递归

因为递归会不断在栈上创建新的栈帧,递归越深开辟的栈帧也越多,所以当数据过多时就会产生栈溢出的情况,这时如果我们利用非递归来实现,就不会有这样的问题,因为我们借助其他数据结构或者方法来进动态开辟,所以就会利用堆的空间,堆的空间比栈要大很多,所以很难有溢出的情况。

当然非递归也可以实现,但是会操作会相对来说更加复杂。

我们需要用到栈来模拟递归,我们通过将每次划分的左右区间入栈,然后通过区间来进行划分,而不是将数据入栈。

栈的实现之前有写过
https://blog.csdn.net/qq_35423154/article/details/104327861

首先放入其实的左右区间,然后将左右区间出栈,通过前面的几种方法划分后再将划分后的区间入栈,不断操作,直到栈为空,说明所有区间划分完成

void noRecursive(int* arr, int begin, int end)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, begin);
	StackPush(&s, end);

	while (!StackEmpty(&s))
	{
		//因为入栈时先入左子树再入右子树,所以先出应该是右子树再出左子树
		int right = StackTop(&s);
		StackPop(&s);

		int left = StackTop(&s);
		StackPop(&s);

		int povit = PrevCurMethod(arr, left, right);
		//划分区间[left][povit - 1] [povit + 1][right]
		if (left < povit)
		{
			StackPush(&s, left);
			StackPush(&s, povit - 1);
		}

		if (right > povit)
		{
			StackPush(&s, povit + 1);
			StackPush(&s, right);
		}
	}

	StackDestroy(&s);
}

优化

优化枢轴:三数取中法

我们现在实现的快速排序会遇到一种情况,就是如果数据本身有序,这时快速排序的时间复杂度就会非常高,有时甚至比不上较为低级的交换排序冒泡排序, 这时一种非常大的问题,原因就出在了我们默认选择固定的枢轴,所以解决的方法也很简单,优化枢轴的选择即可。

而我们用到的方法也很简单,通过比较begin , mid ,end三个数据,选择大小适中的数据作为枢轴即可。

//三数取中,优化枢轴, 保证中间值在end的位置
void chancePovit(int* arr, int begin, int end)
{
	int mid = ((end - begin) >> 1) + begin;
	//取中间数,之所以不直接相加除二是因为数据过大时可能整型溢出,同时除号效率也没有右移效率高。
	if (arr[begin] > arr[end])
	{
		Swap(&arr[begin], &arr[end]);
	}
	//当首位比末尾低时两者交换,较小的放在前面
	if (arr[mid] < arr[begin])
	{
		Swap(&arr[mid], &arr[begin]);
	}
	//将中间和首位交换,将较小的放在前面
	if (arr[end] > arr[mid])
	{
		Swap(&arr[end], &arr[mid]);
	}
	//将末尾和中间交换,将中间数据放在最后,也就是我们选择的枢轴位置end
}

只需要在选择枢轴前执行即可。


优化递归:尾递归优化

虽然讲了非递归实现,但是那种方法还是有点复杂,所以我们还有一种优化递归的方法,借助迭代来减少一轮的递归,减少对栈的消耗。

我们将if改为while,然后在递归划分左区间时, 将begin改为右区间的首部,这样进入下一次循环的时候就会变为划分[pivot + 1][end],也就是我们原本的右区间,这样我们就将原本的一部分递归改为了循环,大大的减少了对栈的消耗

void QuickSort_Plus(int* arr, int begin, int end)
{
	//这里将if改为while
	while (begin < end)
	{
		int pivot = PrevCurMethod(arr, begin, end);

		QuickSort(arr, begin, pivot - 1);
		begin = pivot + 1;
		//将begin的值改为右区间的首部, 这时到进入下一次循环的时候就等价于原来的递归右区间
	}
}

到这里,快速排序的几种划分和多趟思路就全部讲完了,其实快速排序并没有想象中的难,了解了具体思路后实现起来还是挺简单的。
下面给出所有代码

#include"Stack.h"

//交换
void Swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

//三数取中,优化枢轴, 保证中间值在end的位置
void chancePovit(int* arr, int begin, int end)
{
	int mid = ((end - begin) >> 1) + begin;

	if (arr[begin] > arr[end])
	{
		Swap(&arr[begin], &arr[end]);
	}

	if (arr[mid] < arr[begin])
	{
		Swap(&arr[mid], &arr[begin]);
	}

	if (arr[end] > arr[mid])
	{
		Swap(&arr[end], &arr[mid]);
	}
}

// Hoare法
int Hoare(int* arr, int begin, int end)
{
	chancePovit(arr, begin, end);
	int pivot = arr[end];
	int pivot_index = end;
	while (begin < end)
	{
		while (begin < end && arr[begin] <= pivot)
		{
			++begin;
		}

		while (begin < end && arr[end] >= pivot)
		{
			--end;
		}

		Swap(&arr[begin], &arr[end]);
	}

	Swap(&arr[begin], &arr[pivot_index]);
	return begin;
}


//挖坑法
int DigHole(int* arr, int begin, int end)
{
	chancePovit(arr, begin, end);
	int pivot = arr[end];
	while (begin < end)
	{
		while (begin < end && arr[begin] <= pivot)
		{
			++begin;
		}

		arr[end] = arr[begin];
		while (begin < end && arr[end] >= pivot)
		{
			--end;
		}

		arr[begin] = arr[end];
	}

	arr[begin] = pivot;
	return begin;
}


//双指针法
int PrevCurMethod(int* arr, int begin, int end)
{
	int cur = begin, prev = begin - 1;
	chancePovit(arr, begin, end);
	int pivot = arr[end];
	while (cur < end)
	{
		if (arr[cur] < pivot && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	++prev;
	Swap(&arr[cur], &arr[prev]);

	return prev;
}


//递归多趟
void QuickSort(int* arr, int begin, int end)
{
	if (begin < end)
	{
		int pivot = PrevCurMethod(arr, begin, end);
		
		QuickSort(arr, begin, pivot - 1);
		QuickSort(arr, pivot + 1, end);
	}
}

//优化递归
void QuickSort_Plus(int* arr, int begin, int end)
{
	while (begin < end)
	{
		int pivot = PrevCurMethod(arr, begin, end);

		QuickSort(arr, begin, pivot - 1);
		begin = pivot + 1;
	}
}

//非递归
void noRecursive(int* arr, int begin, int end)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, begin);
	StackPush(&s, end);

	while (!StackEmpty(&s))
	{
		//因为入栈时先入左子树再入右子树,所以先出的应该是右子树再到左子树
		int right = StackTop(&s);
		StackPop(&s);

		int left = StackTop(&s);
		StackPop(&s);

		int povit = DigHole(arr, left, right);
		//划分区间[left][povit - 1] [povit + 1][right]
		if (left < povit)
		{
			StackPush(&s, left);
			StackPush(&s, povit - 1);
		}

		if (right > povit)
		{
			StackPush(&s, povit + 1);
			StackPush(&s, right);
		}
	}

	StackDestroy(&s);
}
发布了60 篇原创文章 · 获赞 78 · 访问量 6322

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/105099196
今日推荐