数据结构(六)----排序

排序

什么是排序?

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。(概念)、

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

排序是算法领域最常见也是最简单(查找:?)的一个算法了。“八大排序”的威名,相信每个编程学习者都有所耳闻,今天,我们就来一起深入学习一下八大排序算法:

这里先给出Sort.h的内容,后面我们再来一个一个实现

// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n)
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
// 归并排序递归实现
void MergeSort(int* a, int n)
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
// 计数排序
void CountSort(int* a, int n)

插入排序

实现

插入排序也叫直接插入排序,核心思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。比如我们在玩扑克牌时就会用到这一排序:

可能静态的动画大家还不能很好理解,这里给一个可以可视化查看算法的网站:VA大家可以在这里面找到排序算发并观察各种排序算法的过程。

// 插入排序
void InsertSort(int* a, int n)
{
	assert(a);
	for (int i = 0; i < n - 1; i++)//将一个数组中所有元素升序
	{                              //,这里必须是n-1,不然后面数组会越界(本来插入的就是x=a[i+1],也就是说i最多到n-2就行)
		int end=i;
		int x=a[end+1];//x始终指向end下一个位置的值
		while (end >= 0)//每趟插入最多挪动end-1个数据
		{
			if (a[end] > x)//x前一个数大于x,就将数据往后移一格
			{
				a[end + 1] = a[end];//这里数组的值会往后覆盖
				                    //但是没关系,我们已经将a[end+1]的值保存在x当中了
				end--;
			}
			else
			{
				break;//跳出里面的while循环
			}
		}
		a[end + 1] = x;
	}
}

循环开始时,把最左边(只有一个元素)看成最初的有序数组,然后把第二个插入,后面把新的数依次插入到前面排好序的数组中。
在插入的时候,先把待插入的数存起来,然后开始比较和挪动,直到遇到比这个数小的(这里排的是升序,读者可以自行实现降序),然后放进去

效率分析

最优情况:待排数组本就有序,此时每次只需和前一个数据比较一次即可,也就是进行N-1次比较,时间复杂度:O(N)

最坏:待排序数组是逆序的,那么需要比较1+2+3+4+…+n-1次,共n(n-1)/2,时间复杂度:O(N^2)

空间复杂度:O(1),只需存储待排序的值即可。

稳定性:稳定

选择排序

实现

核心思路:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
很好理解,很少使用,因为无论什么情况都得O(N^2)的时间复杂度,而且不稳定

void SelectSort(int* a, int n)
{
	int begin = 0;//开头
	int end = n - 1;//结尾
	while (begin < end)
	{
		int maxi = begin;//初始化最值
		int mini = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;//记录下标,对原数组进行交换
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		//swap函数请读者自行实现
		swap(&a[begin], &a[mini]);//将最大最小值交换
		swap(&a[end], &a[maxi]);
		begin++;//数组范围往中间缩小
		end--;
	}
}

但是:如果begin和maxi重合就会有意外:

思考一下:为什么只有这样有意外?mini和end重合就没有意外?这和两个swap的顺序有什么关系?
然后我们对这个特例进行修改:

// 选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int maxi = begin;
		int mini = begin;
		for (int i = begin; i <=end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;//记录下标,否则会有数据被覆盖的问题
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		swap(&a[begin], &a[mini]);
		if (maxi == begin)//当最大值为begin时,交换最小值和开头元素后,maxi指向的值不再是最大值了.
		{
			maxi = mini;
		}
		swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

效率分析

无论原数组是有序的还是无序的,选择排序都要一遍一遍地遍历数组,即使我们优化之后也要走N/2次,然后乘以N/2,时间复杂度为O(N^2)
由于没有存数组等,空间复杂度还是O(1),算法是不稳定的。

冒泡排序

实现

这个各位在学习C语言的时候就已经很熟悉了吧。其实它还有一个名字,交换排序(实际上是交换排序的一种)

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		bool flag = true;//想一下这个flag的设置有什么作用?
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] < a[j + 1])
			{
				swap(&a[j], &a[j + 1]);
				flag = false;
			}
				
		}
		if (flag) break;
	}
}

不再过多赘述

效率分析

时间:O(N^2) 空间:O(1) 稳定

小总结

上面这三个是比较基础的排序算法,通常归在一起比较,从性能上来说,插入>冒泡>选择(诶嘿冒泡居然还不是倒数第一)
下面将要介绍的则是一些改进的、有技术含量的排序了

希尔排序

希尔对插入排序进行了优化,将其效率提升了很多,甚至可以媲美快速排序!

实现

希尔排序的思路:越有序的数组,直接插入排序的效率越高,基于此,希尔提出,首先对数组进行预排序,使数组接近有序,然后再进行插入排序。他给出的排序方法是:将一个数组按照gap分组预排序(也是用插入排序),再将数组进行插入排序

希尔排序的特性:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
    会很快。整体而言就可以达到优化的效果。后面我们可以进行测试和对比
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的
    希尔排序的时间复杂度都不固定
for (int i = 0; i < n - gap; i+=gap)//end最多走到n-gap位置
		{                           
			int end = i;
			int x = a[end + gap];//插入排序中gap等于1,就是加一
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end =end-gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = x;
		}

希尔排序的时间复杂度取决于它的gap值的选取,我们可以有很多种不同的选取方法,但是需要遵循一个原则:先大后小。
常见的gap取值有n/2 n/3等
完整的希尔排序:

// 希尔排序
void ShellSort(int* a, int n)
{
	//多次预排序加直接插入排序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		//gap = gap / 3 + 1,这里加一是为了防止gap直接变成0

		// 多组一锅炖,也可以每次+=gap
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end =end-gap;
				}
				else
				{
					break;
				}
			}

			a[end + gap] = x;
		}
	}
}

效率分析

对于希尔排序的时间复杂度,Knuth进行了大量的试验统计

有时候我们直接看做:n^1.3
但是不管哪个取值,相比n^2都已经优化很多了(但是相比于快排的nlogn还是差点,所以只能叫“媲美”)

空间复杂度:O(1)
(不需要额外的与输入规模成正比的空间的排序算法都是O(1))
稳定性:不稳定

堆排序

实现

对于堆排序,我们在学习二叉树和堆的时候就有涉猎,我们知道,堆分为大堆和小堆,前者的堆顶是最大值,后者是最小值。
所以堆排序升序的思路就是:先建堆,找到最大值,将其与最后一个交换,然后容量减一,这样最大值就在最后,然后通过向下调整算法找新堆的最大值。

另一个例子:

明白了基本思路,我们还要考虑一个问题,那就是我们需要先把原数组调整成一个大堆,而成为一个大堆的前提是它的左右子树都是大堆,考虑到我们使用数组进行实现,这里使用循环:

for(int i = (n - 1 - 1) / 2;i>=0;i--)
{
	//n-1是最后一个数,(child-1)/2是这个孩子结点的父节点
	AdjustDown(a,n,i);
}

然后就是依次选出最大值:

for(int end = n-1;end>0;end--)
{
	//先交换
	Swap(&a[end],&a[0]);
	//再调堆
	Adjustdown(a,end,o);//这里代入n,堆的大小减一,此时最大值就在a[n]了
}

那我们能否使用小堆来建立升序呢?答案也是可以的:
我们可以先把原数组建一个小堆,这样最小值就在第一位了,但是我们把堆顶拿出来后,后面的值就是一个混乱的数组,这时候我们就要重新建堆,导致每选出一个数就要建一次小堆,效率大大地减少。

效率分析

首先是建堆:按最坏的情况分析,堆为满二叉树并且每次向下调整都走远的距离

错位相减法可算得总调整次数2^h-1-h=n-log2 (h+1),也就是O(N)
其次是选数调堆:每选出一次最大值交换,就要向下调整高度log2N次,共需选n-1次,也就是O(NlogN)
两个相加,也就是O(N
logN)
堆排序是不稳定的,当

计数排序

我们之前用的排序算法,一个共同特点就是,都是利用了大小的比较进行移动交换等,然而我们今天学习的计数排序则是一种非比较排序法。
计数排序使用一一对应的映射关系,不用比较数据就能排好序。基本思路为:找出数组中的最大值和最小值,然后开辟一个空间,元素个数为最大-最小+1,再统计数据出现的次数(所以这个排序对数据的类型有一定要求,如果差距很大不太适合)
举个例子:

int arr[]={2,3,6,5,1,8,5,0,4,4};

数组最大是8,最小是0,那么我们就开辟9个空间,在开辟的空间中存放一个数组count,这个数组存放索引值在原数组出现的次数,如count[0]就是arr中0的个数,然后进行遍历统计。待完成后依照count数组存放的值进行排序。

缺陷:如果在计数排序中出现负数(作为索引时数组越界)、极差过大的数、浮点数,情况会变得比较麻烦。
另外由于要开空间,计数排序的本质是以空间换时间

实现

首先找最值:

	int max = a[0];
	int min = a[0];
	//找最值
	for (int i = 1; i < n; i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;//动态开辟数组的元素个数
	int* count = (int*)calloc(range, sizeof(int));//将元素初始化为0

其次计数

//计数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
		//计数是去原数组中计数
	    //所以循环次数是n
	}

最后依照次数进行排序

	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)//只要count数组中元素不为0就赋值到原数组
		{
			a[j++] = i + min;
		}
	}
	free(count);//用完后释放空间
	count = NULL;

效率分析

(N代表原数组长度,Range代表动态开辟空间数组长度)
选最值:O(N)
计数:O(N)
排序:O(Range)
因此总时间:O(max(N,Range))
计数排序不考虑其稳定性

归并排序

归并排序,又叫二路归并排序,是使用分治算法的经典案例。归并排序分为递归版本和非递归版本,这里我们就按升序分别来实现一下。

实现

首先来实现递归版本:

int a[]={10,6,7,1,3,9,4,2};

对于这样一个无序数组,要想让它变得有序,按照分而治之的思想,我们需要让他的左右两部分变得有序,那对于左半部分怎么排序呢,那就让左半部分的左右两部分变得有序,一直拆分到数组只有一个元素。

可以发现,一直分到只有两个数,这时我们只需要进行简单的比较即可,此时我们就需要对其进行合并,也就是对两个有序数组进行合并。
对两个有序数组进行合并,我们可以使用双指针法:
比如:

int a[]={2,5,8},b[]={1,6,7};

方法是:定义两个指针x和y,分别指向a、b两个数组,然后再定义一个数组arr接收其数据。循环比较大小,较小的那个指向的数据存入arr,然后向后移动。某一个指针到达数组末尾时停下来

while (begin1 <= end1 && begin2 <= end2)//begin指针指向两个有序数组的第一个元素.end为数组有效范围,某个指针走到数组结尾时循环结束
	{
		if (a[begin1] < a[begin2])//谁小谁就先进tmp数组
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)//当其中一个数组走完后.将另外一个数组所有内容直接放进tmp数组
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)//数组1先走完就将数组2剩下全部内容放进去
	{
		tmp[i++] = a[begin2++];
	}
	//将tmp数组的内容拷贝回a数组
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}
}
//这里在原数组中不好操作,我们选择开临时空间

此时我们就实现了两个有序数组的合并,这时只要继续归并即可。
整个归并排序的过程:递归、合并

//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//为临时数组tmp开辟空间
	if (tmp == NULL)
	{
		printf("动态开辟失败");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);//_MergeSort为递归函数.传参进行递归过程
	free(tmp);
	tmp = NULL;
}

//归并排序的子程序
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;//结束条件
	}
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);//进了函数一直递归,直到数组元素为1个后开始归并排序
	_MergeSort(a, mid + 1, right, tmp);//先递归左边再递归右边

	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//将tmp数组的内容拷贝回a数组
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}

}

这里选择用两个函数来实现,因为我们提前开好tmp会比在递归的过程中开tmp更方便。

在理解了递归版本的归并排序之后,接下来我们实现非递归版本的归并排序:
还是这个数组:

int a[]={10,6,7,1,3,9,4,2};

首先,最底层,我们需要对10 6;7 1;3 9;4 2进行归并排序

其次,中间层 我们需要对6 10 1 7;3 9 2 4进行归并排序

最后,最上层,我们需要对1 6 7 10 2 3 4 9进行归并。

对于外层,可以设计为:

int gap=1;
while(gap<n)
{
	//...
	gap* = 2;//最底层gap为1,中间层是2,往上是2的幂
}

对于内层:

for(int i=0;i<n;i+=2*gap)//gap=1为例,0 1;2 3;4 5;6 7
{
	//...
}

参考递归版本的开辟空间和有序数组合并:

//归并排序(非递归)偶数没问题,奇数还需要改正
void MergeSortNonRE(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("动态开辟失败");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		for (int i = 0; i < n; i++)
		{
			a[i] = tmp[i];
		}
		gap = gap * 2;
	}

	free(tmp);
	tmp = NULL;
}

但是这只对n为偶数的时候适用,如果n为奇数怎么办呢?
假设有一个新数组:

int a[]={10,6,7,1,3,9,4,2,5};

在循环中归并到4 2之后,下一步是归并5和5后面的元素,这里就会越界报错,所以代码需要针对这一点进行优化:

//归并排序(非递归完全版)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("动态开辟失败");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i = i + 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			
			//处理特殊的越界情况
			// end1 越界,[begin2,end2]不存在
			if (end1 >= n)
			{
				end1 = n - 1;
			}

			//[begin1,end1]存在 [begin2,end2]不存在,不存在怎么办?直接换成剩下的数即可
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		for (int i = 0; i < n; i++)
		{
			a[i] = tmp[i];
		}
		gap = gap * 2;
	}

	free(tmp);
	tmp = NULL;
}

效率分析

按最坏情况来分析,二分会把数据分为log2 N层,每层都有N个数据(第k层有m个数组,每个数组有N/m个元素),所以一共需要遍历Nlog2 N,也就是O(Nlog N);
此外,空间复杂度为O(N)
而且归并排序算法是稳定的

快速排序

终极大boss快速排序来咯!
之前其他排序要么是按照人名命名(希尔排序),要么按照形象命名(冒泡、归并、插入),你为什么就叫快排?你真有这么快?
快速排序:老子就是这么嚣张!

实现

快速排序有递归版本和非递归版本,其中递归版本又有三个版本,想必各位在最开始的头文件那里就已经发现了端倪,一会我们就一个一个来实现。在此之前我们要先了解快排的基本思路:
1.从待排序的数组中选取一个基准值(key)
2.再将数组分为两部分:左子数组元素<key;右子数组元素>key
3.左右子数组再重复1.2
(诶,这不是递归吗)
在选基准值时,一般选择数组第一个或者最后一个,有时这样会有缺陷,但是我们到后面再修正

递归版本:
hoare版本(hoare本人就是快排发明者)

让我们拿一个例子来讲解

int a[10]={6,1,2,7,9,3,4,5,10,8};

1.将第一个元素6作为基准值
2.定义两个指针指向:第一和最后的位置
3.左边的指针(L)找比基准值大的值;右边的指针( R)找比基准值小的值
4.R先走.R找到满足要求的值后停下;L再走,找到满足要求的值后停下
5.当L和R都停下,交换两个位置数据
6.当L和R相等时,交换当前位置与基准值的值.

走完之后,数组变成了:

{3,12,54,69,710,8}

最开始的基准值6,左边全是小于6的,右边全是大于6的,说明这个6已经到了它自己的位置上。
接下来对左右两个子数组继续使用快排即可
如果是拿最右边作为基准值,那么就要从左边开始走,找比它大的,找到后停下来,右边开始找,都停下时交换左右的值,当左右碰面时交换基准值位置和交点位置的下一个位置的值(好好思考一下)
单趟快排代码实现:

void Partion(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[left], &a[right]);
	}
	//当left和right相遇,交换此位置和key的值
	Swap(&a[left], &a[right]);
}
//要注意的是,内层的left<right也要加上,不然可能会越界

单趟快排不好递归,我们也再写一个函数:

//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int key = Partion(a, left, right);
	QuickSort(a, left, key - 1);//递归左子序列
	QuickSort(a, key + 1, right);//递归右子序列
}

此时需要单趟快排返回基准值的位置,所以我们再调整一下单趟快排

int Partion(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		//左边为key,那么右边先走,找小
		while (a[right] >= a[key] && left < right)
		{
			right--;
		}
		//左边后走,找大
		while (a[left] <= a[key] && left < right)
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[key]);//将key位置和左右相遇的位置交换
	return left;//将左或者右的值返回去当下一次递归的头或尾
}

如果我们对一个已经排好序的数组进行快速排序时,我们会发现,每一趟快排我们的基准值位置都不变,我们的基准值会遍历整个数组,那么我们指针的移动次数就是:1+2+3+4+…+N,也就是O(N^2)的时间复杂度。
怎么办呢?
这时我们就要对key值的选取进行优化了:
三数取中法:从最左、最右、中间三个的元素中,选择一个既不是最大也不是最小的元素作为key,这样我们就不会选到最大或者最小值了。
(不会有同学会说选中间值吧,要是能知道中间值就不需要快排了)
但是如果数据很多重复,使用三数取中效率也很低怎么办?
答:在数据重复性很高的时候不要使用快排

挖坑法:

和hoare不同的是,挖坑法首先用一个变量记录基准值key,然后在基准值的位置挖一个坑位。
然后启动双指针,R先走,找比key小的,放在坑位上,自己成为新坑位;然后L走,找比key大的,放在坑位上,自己成为新坑位,以此类推知道LR相遇,相遇时的坑位放key,此时key左边全是小于它的,右边全是大于它的。最后递归左右子数组即可

//递归调用函数类似
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int key = Partion2(a, left, right);
	QuickSort(a, left, key - 1);//递归左子序列
	QuickSort(a, key + 1, right);//递归右子序列
}

//单趟快排(挖坑版本)
int Partion2(int* a, int left, int right)
{
	//三数取中--面对有序的情况不会栈溢出(key不会选到最大或者最小的数)
	int mini = GetMidIndex(a, left, right);
	swap(&a[left], &a[mini]);
	int key = a[left];
	int pit = left;//坑位起始位置是基准值的位置
	
	while (left < right)
	{
		//右边找小,放在左边的坑位中
		while (a[right] >= key && left < right)
		{
			right--;
		}
		a[pit] = a[right];//将找到的值扔进坑位
		pit = right;//自身变成新坑位
		
		//左边找大,放在右边的坑位中
		while (a[left] <= key && left < right)
		{
			left++;
		}
		a[pit] = a[left];//找到的值扔进坑位
		pit = left;//自己变成新坑位
	}
	a[pit] = key;//最终将相遇时的坑位给上最开始记录的key值
	return pit;//返回基准值(或者叫坑位)的位置,方便递归
}

前后指针法:
步骤:
1.定义两个指针cur和prev,cur指向第二个元素,prev指向cur前面的元素
2.cur找比基准值小的值,找到后停下,p向后走一格,再交换c和p指向的值
3.最后c走完数组后交换key和prev的值
相当于cur把比key小的筛出来放在prev上,筛一个prev递增一个,最后pre和key交换,这样key左边就全是小于key 的了

递归函数:

//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int key = Partion3(a, left, right);
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}

单趟快排

//单趟快排(前后指针版本)
int Partion3(int* a, int left, int right)
{
	//三数取中--面对有序的情况不会栈溢出(key不会选到最大或者最小的数)
	int mini = GetMidIndex(a, left, right);
	swap(&a[left], &a[mini]);
	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		while (a[cur] >= a[key] && cur <= right)//cur指针找小于key的
		{
			++cur;
		}
		if (cur <= right)
		{
			swap(&a[++prev], &a[cur]);
			cur++;//交换完后,cur要再往后走一步
		}
	}
	swap(&a[key], &a[prev]);// 最后交换prev和key的值
	return prev;//返回基准值的位置,方便下次递归
}

非递归快排
在学习非递归快排的时候,首先我们需要明白一件事,那就是非递归只是把递归方式改成了循环,至于单趟快排的算法,其实和递归快排一样。非递归相对递归的一个很大的优势是:非递归不需要很深的空间,栈帧消耗远低于递归
还是拿一个简单的例子说明:

int a[]={6,1,2,7,9,3,4,5,10,8};//一个无序数组

a是一个长度为10的数组,下标范围0-9,我们先想象递归的顺序:

第一次快排要遍历0-9的元素,然后以6为key把a分成0-4和6-9两部分
按照左右顺序,第二次排的是0-4,然后再把0-4这个数组分成两部分,而且还是左右顺序,先左再右,假设分别是0-1和3-4,此后0-4这几个排好了,然后才开始6-9,然后进行6-9的左右快排。

也就是说,我们的非递归,也就是循环结构,需要实现这样的一个顺序。
此时就用到了我们之前学的数据结构:栈
我们把0-9入栈,然后出栈遍历,然后分别把6-9、0-4入栈,0-4出栈遍历,然后3-4、0-1入栈,然后0-1出栈,3-4出栈…这样就和我们递归的时候顺序一致了。(实际上不用先右入栈后左入栈也能排好,只是不好理解)

单趟快排(这里用哪个都行,我用的前后指针):

//单趟快排(前后指针版本)
int Partion3(int* a, int left, int right)
{
	//三数取中--面对有序的情况不会栈溢出(key不会选到最大或者最小的数)
	int mini = GetMidIndex(a, left, right);
	swap(&a[left], &a[mini]);
	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		while (a[cur] >= a[key] && cur <= right)//cur指针找小于key的
		{
			++cur;
		}
		if (cur <= right)
		{
			swap(&a[++prev], &a[cur]);
			cur++;//交换完后,cur要再往后走一步
		}
	}
	swap(&a[key], &a[prev]);// 最后交换prev和key的值
	return prev;//返回基准值的位置,方便下次递归
}


非递归函数:

//快速排序(非递归)
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))//当栈不为空时就继续
	{
		//从栈中找end和begin
		int end = StackTop(&st);//将栈顶元素赋值给下标遍历的尾
		StackPop(&st);//删除栈顶元素
		int begin = StackTop(&st);//再将新的栈顶元素给下标遍历的头
		StackPop(&st);//再把这个值删除
		int key = Partion3(a, begin, end);//单趟快排返回基准值对应位置下标
		if (key < end)
		{
			StackPush(&st, key + 1);
			StackPush(&st, end);
		}
		if (begin < key - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, key);
		}
	}
	StackDestroy(&st);
}

效率分析

理想情况下:快排类似于二分,key值选到中位数,这样总共需要log2 N次快排,每次快排左右指针共移动N次,总共是Nlog2 N,所以快速排序的时间复杂度是O(Nlog N)

总结

完结撒花!!!相信如果你掌握了本讲的所有排序算法并且能熟练手撕出来,你绝对是面试中的佼佼者1

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值