数据结构初阶:八大排序

发布于:2023-01-20 ⋅ 阅读:(340) ⋅ 点赞:(0)

1 排序的概念

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

2 插入排序

2.1直接插入排序

2.1.1 基本思想

把一个记录插入到已经排好的有序表中,从而得到一个新的有序表。
我们平时玩扑克牌时,理牌的方法就用到了直接插入排序的思想
假设我们手中现在有按顺序排好的 5,7,8三张牌,那么抽到6这张牌该放到哪个位置呢?当然是5和7的中间。此时5,6,7,8四张牌依旧是有序的。实际上,我们是将6依次和这三张牌作比较,发现6比7,8小,所以应该在它两前面,而6比5大,所以应该在5的后面,就这样确定了6的位置

2.1.2代码实现

我们先来看单趟排序
这里tmp为待插入的数,a[end]数组为有序的数组

while (end >= 0)
		{
			if (tmp < a[end])//待插入的数比a[end]小,说明tmp的位置应该在a[end]的前面,a[end]往后移动
			{
				a[end + 1] = a[end];
				end--;//换下一个数比较
			}
			else//tmp比a[end]大,跳出循环
			{
				break;
			}
		}
		a[end + 1] = tmp;//end+1的位置便是tmp的位置

单趟排序同样可以类比上文插扑克牌的过程,5,7,8即为有序数组,现在插入6,遇到7,8都比6大,所以7,8往后移动一个位置,遇到5,6比5大,插到5的后面。
单趟排序只是插入了一个数,但排序我们要使整个数组有序,即需要在单趟排序的外面再嵌套一层循环

void InsertSort(int* a, int n)//n为数组中元素的个数
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])//待插入的数比a[end]小,说明tmp的位置应该在a[end]的前面,a[end]往后移动
			{
				a[end + 1] = a[end];
				end--;//换下一个数比较
			}
			else//tmp比a[end]大,跳出循环
			{
				break;
			}
		}
		a[end + 1] = tmp;//end+1的位置便是tmp的位置
	}
	
}

因为直接插入排序的前提是数组已经是有序的,然后插入一个数,使数组依旧保持有序,所以为了满足这个要求,我们就假设待排序的数组中第一个数是有序的,然后插入第二个数,前两个数有序,插入第三个……前n-1个数有序,插入第n个数。直到所有的数都是有序的。
在这里插入图片描述

2.1.3 复杂度分析

Ⅰ时间复杂度
①最好:要排序的数组本身就是有序的,那么只需要依次比较一遍,为O(n)
②最坏:要排序的数组是逆序的假设为{5,4,3,2,1},那么5需要移动4次到达它的位置,4移动3次,3移动2次,2移动1次,所以有n个数
T(n)=1+2+3+4+……+n-1。所以时间复杂度为O(n²)。
Ⅱ空间复杂度
开辟空间常数次,为O(1)。

2.2 希尔排序

我们前面所讲的直接插入排序只是在在数组基本有序的时候效率很高,在数组完全逆序的时候就显得没有那么有优势了。因此Shell对直接插入排序算法做了改进,提出了希尔排序。

2.2.1 基本思想

先选定一个整数gap,把待排序的所有记录分成几个组,距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后再选gap,重复上述分组和排序的工作。当gap=1时,进行直接插入排序。
假设gap=3,数组为{9,1,2,5,7,4,8,6,3,5}
在这里插入图片描述

分好组后
{9,5,8,5}为一组;{1,7,6}为一组;{2,4,3}为一组,对这三组分别进行组内排序
在这里插入图片描述
可以看出,一次分组排序后,虽然数组并不是完全有序的,但相对没排序之前是有改变的,相对较大的数在跑到了后面,相对较小的数跑到了前面。接下来再选gap,再进行组内排序,会使数组越来越接近有序,(就可以利用直接插入排序的优势)最后一次进行直接插入排序。

对于gap的选择
gap越大,大的数更快到后面,小的数可以更快到前面,但是越不接近有序。
gap越小,越接近有序

2.2.2 代码实现

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//要保证最后一次gap=1,进行直接插入排序
		for (int i = 0; i < n - gap; i++)
		{
			//这里i++是多组预排序同时进行。
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];//每一组内的数往后移动是+gap,不是加1
					end=end-gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
	
}

2.2.3 复杂度分析

希尔排序的时间复杂度不好分析,因为每一次gap的取值都不一样,导致很难去计算。我们只需要记住一个大概值为O(n^1.3)

3 选择排序

基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

3.1 直接选择排序

3.1.1 基本方法

①遍历一遍数组,选出最大值和最小值
②将最大值和最后一个元素交换位置;将最小值和第一个元素交换位置。
③将最大的数和最小的数排除在外,在剩下的数中选出次小的数和次大的数。
④重复以上操作,直到数组有序。

3.1.2 代码实现

先看单趟排序

//假设最大的数和最小的数都在a[begin]
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])//下标为i的数小于a[mini],交换下标
				mini = i;
			if (a[i] > a[maxi])//下标为i的数大于a[maxi],交换下标
				maxi = i;
		}
		//遍历完一遍,选出最大的和最小的
		//最小的数换到最前面
		Swap(&a[begin], &a[mini]);
		//这里需要注意,单独考虑最大的数的下标恰好是begin
		if (a[maxi] == a[begin])
		{
			maxi = mini;//最大的数的下标为原来最小的数的下标
		}
		Swap(&a[end], &a[maxi]);//最大的数换到最后面

接下来,要在剩下的区间内选出次大的数和次小的数,因此begin++,end- -确定新的区间

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int mini = 0;
	int	maxi = 0;
	while (begin < end)//当begin=end 的时候,说明此时区间只剩一个数,即是有序的,跳出循环
	{
		//假设最大的数和最小的数都在a[begin]
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])//下标为i的数小于a[mini],交换下标
				mini = i;
			if (a[i] > a[maxi])//下标为i的数大于a[maxi],交换下标
				maxi = i;
		}
		//遍历完一遍,选出最大的和最小的
		//最小的数换到最前面
		Swap(&a[begin], &a[mini]);
		//这里需要注意,单独考虑最大的数的下标恰好是begin
		if (a[maxi] == a[begin])
		{
			maxi = mini;//最大的数的下标为原来最小的数的下标
		}
		Swap(&a[end], &a[maxi]);
		end--;
		begin++;
	}
	
}

在这里插入图片描述
注:动图这里是每次只选出了一个数,而我们前面代码所实现的是每次选出两个数(最大的和最小的),性能相对比只选一个数有提升

3.1.3 复杂度分析

Ⅰ时间复杂度
无论数组是有序的还是无序的,都需要进行比较,第一次n个数进行比较,第二次n-2个数进行比较……
所以T(n)=n+n-2+n-4+……+0
时间复杂度为O(n²)
Ⅱ空间复杂度
O(1)

3.2 堆排序

点击这里看前面详解堆的博客

4 交换排序

基本思想 根据序列中两个关键值的比较结果来对换这两个记录在序列中的位置,使值较大的记录向序列的尾部移动,值较小的记录向序列的头部移动。

4.1 冒泡排序

4.1.1 基本方法

依次比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

4.1.2 代码实现

先是单趟排序

for (i = 1; i < n; i++)//进行单趟排序
{
		if (a[i - 1] > a[i])//两两进行比较,如果前一个大于后一个,就交换位置
			{
				Swap(&a[i - 1], &a[i]);
			}

}

进行第一趟冒泡排序,可以把最大的数交换到最后面,接着在剩下n-1个数中进行冒泡排序,把次大的数交换到后面。不断重复上述操作,直到数组有序。

void BubbleSort(int* a, int n)
{
	
	for (int j = 0; j < n - 1; j++)
	{
		int i;
		int flag = 0;
		for (i = 1; i < n - j; i++)//进行单趟排序
		{
			if (a[i - 1] > a[i])//两两进行比较,如果前一个大于后一个,就交换位置
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}

		}
		if (flag == 0)
			break;
	}
	
}

想想添加flag标志的作用何在?

当需要交换的时候把flag置为1,如果一趟排序结束后,flag仍为0,说明没有发生交换,数组此时已经有序,就不需要进行后序的操作了,可以避免已经有序的情况下的无意义循环的判断,在性能上有一些提升。

在这里插入图片描述

4.1.3 复杂度分析

Ⅰ时间复杂度
最好:数组已经有序,只需进行一趟比较,比较为n-1次,所以为O(n)
最坏:数组完全逆序,需要进行比较的次数为 (n-1)+(n-2)+……+1次
所以时间复杂度为O(n²)
Ⅱ空间复杂度
O(1)

4.2 快速排序

基本思想:

任取待排序元素中的某元素作为基准值,按照该元素将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列分别重复该过程,直到所有元素都排列在相应位置上为止。

将区间按照基准值划分为左右两半部分的常见方式有

4.2.1 hoare版本

基本思路
①选取最左边的数做key
②右边先走,找到比key小的数就停下来
③左边走,找到比key大的数,然后左右交换。
④右边继续走,找小;左边走找大,两者交换。直到两个相遇就停。
⑤将key和此时相遇位置的数交换,此时一趟下来便可达到key左边的数都比key小,key右边的数都比key大。
我们先来康康动图
在这里插入图片描述

在这里插入图片描述
可以看出,一趟结束后,4左边的数都比4小,4右边的数都比4大,达到我们想要的结果

试着想一想,为什么我们选取左边的数做key,要右边的数先走,左边先走可不可以呢?
答案是不行,我们必须要保证left和right相遇的位置的值比key小,这样和key交换后,相遇位置的值跑到左边,左边的数都比key要小,符合要求。
小伙伴们如果没完全懂,请看下面的详细分析
第一种情况:right先走,找到小的数,right停下来,left走,遇到了right,此时相遇位置比key小。
第二种情况: right先走,right没有找到比key小的数便遇到了left,此时left要么在key的位置没动(极端情况,right遇到的数都比key大)
在这里插入图片描述
要么在上一轮交换后停下的位置(比key小)
在这里插入图片描述

如果左边先走,最后的结果会不符合要求
在这里插入图片描述

在这里插入图片描述
最后的结果,7在左边区间,但比4大,出现错误。综上所述
选取最左边的数当key,右边先走
选取最右边的数当key,左边先走

代码实现
单趟代码

//定义PartSort1是因为后面还有好几种变形的版本,用以区分
void PartSort1(int* a, int begin, int end)
{
	
	int left = begin;
	int right = end;
	int keyi = left;//选取左边当key,右边先走
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])//找到比a[keyi]小的数才停下来,注意不要越界
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])//接下来左边走,找到比a[keyi]大的数才停下来,同样注意不要越界
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//left和right相遇
	Swap(&a[left], &a[keyi]);
	keyi = left;
	
}

单趟结束后,key把整个区间划分成了左区间[begin keyi-1],和右区间[keyi+1,end],如何让整个区间都有序呢?和二叉树那里类似,不断去递归调用左右区间,又会得到新的keyi,keyi又划分为左右区间……直到区间不存在或者只剩一个数,此时便是有序的。
在这里插入图片描述

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//区间不存在,或者只剩一个数就返回
		return;
	int key = PartSort1(a, begin, end);//此时把keyi作为返回值,才能划分区间
		QuickSort(a, begin, key - 1);//递归调用左区间
		QuickSort(a, key + 1, end);//递归调用右区间
}
int PartSort1(int* a, int begin, int end)
{
	
	int left = begin;
	int right = end;
	int keyi = left;//选取左边当key,右边先走
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])//找到比a[keyi]小的数才停下来,注意不要越界
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])//接下来左边走,找到比a[keyi]大的数才停下来,同样注意不要越界
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//left和right相遇
	Swap(&a[left], &a[keyi]);
	keyi = left;
	return keyi;
}

4.2.2 挖坑法

基本思路
①左边做key,形成一个坑位(挖坑,可以想象成把坑位上的数据移走)
②右边先走,找小,找到后把小的数填到左边的坑位上,此时右边又形成新的坑位。
③左边走,找大,找到后把大的数填到右边的坑位,此时左边又形成新的坑位。
④重复以上操作,直到左右相遇,把key填到相遇位置的坑位上
先来看动图
可以看出,挖坑法和hoare版本相比,并没有太多的改变,只不过是左边挖坑天然填的是小的数,右边挖坑天然填的是大的数
在这里插入图片描述

代码实现

int PartSort2(int* a, int begin, int end)
{
	int key = a[begin];
	int piti = begin;//piti为坑位
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[piti] = a[end];//右边先走,遇到小的数,把小的数填到左边的坑位上
		piti = end;//右边形成新的坑位
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[piti] = a[begin];//左边走,找到大的数,填到右边的坑位上
		piti = begin;//左边又形成新的坑位
	}
	a[piti] = key;//两者相遇,把key填到相遇的坑位上
	return  piti;
}

接下来的递归调用和第一个版本一样

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//区间不存在,或者只剩一个数就返回
		return;
	
		int key = PartSort2(a, begin, end);
		QuickSort(a, begin, key - 1);//递归调用左区间
		QuickSort(a, key + 1, end);//递归调用右区间
}

4.2.3 前后指针版本

基本思路
①先选左边当key, prev指向key的位置,cur指向prev的后一个位置
②cur先走,找比key小的数,找到后,先++prev,然后cur和prev所指向的数字交换位置
③cur继续走,找小的数,直到走到末尾。此时prev所指向的数字和key交换位置。
先看动图
在这里插入图片描述
前面两次因为cur找到比key小的数的时候,prev++和cur指向的位置一样,所以不用交换。后面cur遇到比key大的数,cur继续走,而prev不变,一直指向的是比key小的数,二者之间就有了间距(实际上中间间隔的数就是比key大的数。),所以 cur再次遇到比key小的数的时候,prev需要往后移动一位,指向比key大的数,然后二者交换。cur指向末尾的时候,prev此时指向的还是比key小的数,所以最后一步prev和key交换位置。
概括来讲,前后指针法的本质就是把比key小的数往左边放,比key大的数往右边推。
代码实现

int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		if (a[cur] < a[keyi]&&++prev!=cur)//当cur找到比key小的数的时候,还要再判断一下需不需要交换,如果prev++和cur相等就不需要交换了
		{
			
			Swap(&a[prev], &a[cur]);

		}
		cur++;//不管找没找到比key小的数,cur每次都要++
	}
	Swap(&a[prev], &a[keyi]);//cur指向末尾,此时交换a[prev]和a[keyi]
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//区间不存在,或者只剩一个数就返回
		return;
	   int key = PartSort3(a, begin, end);
		QuickSort(a, begin, key - 1);//递归调用左区间
		QuickSort(a, key + 1, end);//递归调用右区间
}

学完了快速排序的递归过程,我们当然也要来玩玩快速排序的非递归过程。

4.2.4 快速排序的非递归

我们采用栈来模拟快速排序的递归过程。先来回想下递归的过程,每次通过key把区间划分为左右两个子区间,然后左右两个子区间又通过key继续划分,直到区间内只剩下一个数或者区间不存在。那么既然是模拟递归的过程,当然也要体现划分区间的过程,即先把区间的头和尾入栈,区间出栈的时候单趟排序分割出子区间,分割好后的区间如果存在就要继续入栈,然后出栈划分子区间,直到栈为空。

void QuickSortNonR(int* a, int begin, int end)//非递归
{
	ST ps;
	StackInit(&ps);
	//先把区间的头和尾入栈,注意栈是先进后出的性质,所以先入尾再入头
	StackPush(&ps, end);
	StackPush(&ps, begin);
	//只要是入了栈的区间,都会拿出来,单趟排序分割
	while (!StackEmpty(&ps))
	{
		//先出来的就是区间的头
		int left=StackTop(&ps);
		StackPop(&ps);
		//后出来的就是区间的尾
		int right= StackTop(&ps);
		StackPop(&ps);
		//单趟排序继续分割
		int key = PartSort3(a, left, right);
		//如果分割好的区间存在,就继续入栈
		if (right >key + 1)
		{
			StackPush(&ps, right);
			StackPush(&ps, key+1);
		}
		if (left < key -1)
		{
			StackPush(&ps,key-1);
			StackPush(&ps,left);
		}
	}

	StackDestroy(&ps);

}

4.2.5 快速排序的复杂度分析

Ⅰ 时间复杂度
快速排序的时间性能取决于快速排序递归的深度
最好
每次key划分左右区间都划分的很均匀,
在这里插入图片描述

总共深度为logn次,每次有n个数进行排序,所以时间复杂度为
O(nlogn)
最坏
待排序的序列为正序或者逆序,每次划分都只能得到一个比上一次划分少一个记录的子序列。
在这里插入图片描述
所以T(n)=n+(n-1)+(n-2)+…+1
时间复杂度为O(n²)

Ⅱ 空间复杂度
空间复杂度主要是递归造成的栈空间的使用
最好 递归树的深度为log₂n,所以空间复杂度为O(logn)。
最坏 待排序的序列为正序或者逆序,空间复杂度为O(n)。

4.2.6 快速排序优化

(1) 三数取中法选key
既然快速排序的时间复杂度取决于递归的深度,我们就尽可能的让递归的深度更少,即每次key划分的左右区间都很均匀

int GetMidIndex(int* a, int begin, int end)
{
	//选取a[begin],a[end],a[mid]三者中处于中间的数当key
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] < a[end])//隐含条件是a[mid]>a[end]
			return end;
		else
			return begin;
 	}
	else 
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] > a[end])//隐含条件是a[mid]<a[end]
			return end;
		else
			return begin;
	}
}

(2) 递归到小的子区间时,可以考虑使用插入排序
因为小的子区间已经趋近有序,直接插入会更有优势
在这里插入图片描述
优化后,快速排序的最终版本

int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[mid], &a[keyi]);
	while (cur <= end)
	{
		if (a[cur] < a[keyi]&&++prev!=cur)//当cur找到比key小的数的时候,还要再判断一下需不需要交换,如果prev++和cur相等就不需要交换了
		{
			
			Swap(&a[prev], &a[cur]);

		}
		cur++;//不管找没找到比key小的数,cur每次都要++
	}
	Swap(&a[prev], &a[keyi]);//cur指向末尾,此时交换a[prev]和a[keyi]
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//区间不存在,或者只剩一个数就返回
		return;
	if (end - begin > 10)
	{
		int key = PartSort3(a, begin, end);
		QuickSort(a, begin, key - 1);//递归调用左区间
		QuickSort(a, key + 1, end);//递归调用右区间
	}
	else//优化小区间
		InsertSort(a + begin, end - begin + 1);
}

5 归并排序

5.1 基本思想

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
在这里插入图片描述

5.2 递归实现

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid,tmp);
	_MergeSort(a, mid+1, end,tmp);//递归,分割区间,一直分割到区间只剩一个元素(可以看做有序)
	//区间有序,开始归并排序(这里相当于后序遍历)
	//[begin mid]    [mid+1  end]
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin1;
	//两个区间开始归并
	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++];
	}
	//归并排好序后,需要把排好序的子序列拷贝回原数组对应的位置
	memcpy(a + begin,tmp + begin,(end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
	//malloc一个数组用于临时存放归并排序的子序列
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

在这里插入图片描述

5.3 非递归实现

思路
归并排序的前提是要把区间划分成只剩一个元素,然后开始归并排序,一个和一个归,归完后两个和两个归,然后四个和四个归…(相当于后序遍历),那么改成非递归的时候就不需要递归划分区间,我们直接从一个和一个归开始,然后两个和两个归,四个和四个归……直到区间完全有序
在这里插入图片描述
代码实现

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	int gap = 1;//刚开始先一个数和一个数归并
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//确定要进行归并排序的两个区间
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			//对于区间越界问题要做相应的修改
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
	         int j = begin1;
			int m = end2 - begin1 + 1;
			//开始归并排序
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
					tmp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//每次归并排序完,先把排好的拷贝回原数组
			memcpy(a + i, tmp + i, sizeof(int) * m);
		}
			gap = gap * 2;//一一归完之后,二二归,四四归......
	}
	free(tmp);
}

5.4 复杂度分析

时间复杂度
每一趟排序都需要把n个记录扫描一遍,共需进行log₂n次(每一次分割区间都是均分),所以时间复杂度为O(nlogn)
空间复杂度
O(n+logn) n为额外开辟的数组空间,logn为递归调用的栈空间。

6 计数排序(非比较排序)

6.1 基本思想

统计每个数据出现的次数,根据统计结果将序列回写到原来的序列中。
在这里插入图片描述
局限性

①只适用于整数
②当数据范围很大时,空间复杂度就会很高

计数排序更适用于数据范围较为集中的情况。但此时还需注意,如果数据范围为1000~1009时,我们还是要开辟从1-1009的空间,然后去统计每个数出现的次数吗?明显这样很浪费空间,所以我们采用相对映射法
即开辟的空间数为数组中最大数-最小数+1
每个数对应的映射位置不在是自己本身,而是本身-最小的数
在这里插入图片描述

6.2 代码实现

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	//找到最大数和最小数
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	//开辟数组空间统计每个数据出现的次数
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	//数组的值都先初始化为0
	memset(count, 0, sizeof(int) * range);
	//统计每个数据出现的次数(相对映射)
	for (int j = 0; j < n; j++)
	{
		count[a[j] - min]++;
	}
	int i = 0;
	//回写回原数组,注意转换为原来的数
	for (int k = 0; k < range; k++)
	{
		while (count[k]--)
		{
			a[i++] = k+ min;
		}
	}
}

7 排序算法复杂度及稳定性总结

在这里插入图片描述
稳定性是指:假定在待排序的记录序列中,存在多个具有相同的关键字记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的。

8 源代码

sort.h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void PrintArray(int* a, int n);
void InsertSort(int* a, int n);
void ShellSort(int* a, int n);
void SelectSort(int* a, int n);
void BubbleSort(int* a, int n);
void QuickSort(int* a, int begin, int end);
void MergeSort(int* a, int n);
void CountSort(int* a, int n);

stack.h

#include <stdio.h>
#include<assert.h>
#include <stdbool.h>
#include <stdlib.h>
typedef int STDataType;
typedef struct stack
{
	STDataType* a;//指向一块数组空间
	int top;//记录当前栈顶的位置
	int capacity;//记录顺序栈的最大容量
}ST;
void StackInit(ST* ps);//初始化栈
void StackDestroy(ST* ps);//销毁栈
void StackPush(ST* ps, STDataType x);//入栈
void StackPop(ST* ps);//出栈
STDataType StackTop(ST* ps);//返回栈顶元素
bool StackEmpty(ST* ps);//判断栈是否为空
int StackSize(ST* ps);//返回栈中元素的个数

sort.c

#include"sort.h"
#include"stack.h"
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void PrintArray(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])//待插入的数比a[end]小,说明tmp的位置应该在a[end]的前面,a[end]往后移动
			{
				a[end + 1] = a[end];
				end--;//换下一个数比较
			}
			else//tmp比a[end]大,跳出循环
			{
				break;
			}
		}
		a[end + 1] = tmp;//end+1的位置便是tmp的位置
	}
	
}
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//要保证最后一次gap=1,进行直接插入排序
		for (int i = 0; i < n - gap; i++)
		{
			//这里i++是多组预排序同时进行。
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];//每一组内的数往后移动是+gap,不是加1
					end=end-gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
	
}
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int mini = 0;
	int	maxi = 0;
	while (begin < end)//当begin=end 的时候,说明此时区间只剩一个数,即是有序的,跳出循环
	{
		//假设最大的数和最小的数都在a[begin]
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])//下标为i的数小于a[mini],交换下标
				mini = i;
			if (a[i] > a[maxi])//下标为i的数大于a[maxi],交换下标
				maxi = i;
		}
		//遍历完一遍,选出最大的和最小的
		//最小的数换到最前面
		Swap(&a[begin], &a[mini]);
		//这里需要注意,单独考虑最大的数的下标恰好是begin
		if (a[maxi] == a[begin])
		{
			maxi = mini;//最大的数的下标为原来最小的数的下标
		}
		Swap(&a[end], &a[maxi]);
		end--;
		begin++;
	}
	
}
void BubbleSort(int* a, int n)
{
	
	for (int j = 0; j < n - 1; j++)
	{
		int i;
		int flag = 0;
		for (i = 1; i < n - j; i++)//进行单趟排序
		{
			if (a[i - 1] > a[i])//两两进行比较,如果前一个大于后一个,就交换位置
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}

		}
		if (flag == 0)
			break;
	}
	
}
int GetMidIndex(int* a, int begin, int end)
{
	//选取a[begin],a[end],a[mid]三者中处于中间的数当key
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] < a[end])//隐含条件是a[mid]>a[end]
			return end;
		else
			return begin;
 	}
	else 
	{
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] > a[end])//隐含条件是a[mid]<a[end]
			return end;
		else
			return begin;
	}
}
int PartSort1(int* a, int begin, int end)
{

	int left = begin;
	int right = end;
	int keyi = left;//选取左边当key,右边先走
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])//找到比a[keyi]小的数才停下来,注意不要越界
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])//接下来左边走,找到比a[keyi]大的数才停下来,同样注意不要越界
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//left和right相遇
	Swap(&a[left], &a[keyi]);
	keyi = left;
	return keyi;
}

int PartSort2(int* a, int begin, int end)
{
	int key = a[begin];
	int piti = begin;//piti为坑位
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[piti] = a[end];//右边先走,遇到小的数,把小的数填到左边的坑位上
		piti = end;//右边形成新的坑位
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[piti] = a[begin];//左边走,找到大的数,填到右边的坑位上
		piti = begin;//左边又形成新的坑位
	}
	a[piti] = key;//两者相遇,把key填到相遇的坑位上
	return  piti;
}
int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[mid], &a[keyi]);
	while (cur <= end)
	{
		if (a[cur] < a[keyi]&&++prev!=cur)//当cur找到比key小的数的时候,还要再判断一下需不需要交换,如果prev++和cur相等就不需要交换了
		{
			
			Swap(&a[prev], &a[cur]);

		}
		cur++;//不管找没找到比key小的数,cur每次都要++
	}
	Swap(&a[prev], &a[keyi]);//cur指向末尾,此时交换a[prev]和a[keyi]
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//区间不存在,或者只剩一个数就返回
		return;
	if (end - begin > 10)
	{
		int key = PartSort3(a, begin, end);
		QuickSort(a, begin, key - 1);//递归调用左区间
		QuickSort(a, key + 1, end);//递归调用右区间
	}
	else//优化小区间
		InsertSort(a + begin, end - begin + 1);
}
void QuickSortNonR(int* a, int begin, int end)//非递归
{
	ST ps;
	StackInit(&ps);
	//先把区间的头和尾入栈,注意栈是先进后出的性质,所以先入尾再入头
	StackPush(&ps, end);
	StackPush(&ps, begin);
	//只要是入了栈的区间,都会拿出来,单趟排序分割
	while (!StackEmpty(&ps))
	{
		//先出来的就是区间的头
		int left=StackTop(&ps);
		StackPop(&ps);
		//后出来的就是区间的尾
		int right= StackTop(&ps);
		StackPop(&ps);
		//单趟排序继续分割
		int key = PartSort3(a, left, right);
		//如果分割好的区间存在,就继续入栈
		if (right >key + 1)
		{
			StackPush(&ps, right);
			StackPush(&ps, key+1);
		}
		if (left < key -1)
		{
			StackPush(&ps,key-1);
			StackPush(&ps,left);
		}
	}

	StackDestroy(&ps);

}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid,tmp);
	_MergeSort(a, mid+1, end,tmp);//递归,分割区间,一直分割到区间只剩一个元素(可以看做有序)
	//区间有序,开始归并排序(这里相当于后序遍历)
	//[begin mid]    [mid+1  end]
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin1;
	//两个区间开始归并
	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++];
	}
	//归并排好序后,需要把排好序的子序列拷贝回原数组对应的位置
	memcpy(a + begin,tmp + begin,(end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
	//malloc一个数组用于临时存放归并排序的子序列
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	int gap = 1;//刚开始先一个数和一个数归并
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//确定要进行归并排序的两个区间
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			//对于区间越界问题要做相应的修改
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
	         int j = begin1;
			int m = end2 - begin1 + 1;
			//开始归并排序
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
					tmp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//每次归并排序完,先把排好的拷贝回原数组
			memcpy(a + i, tmp + i, sizeof(int) * m);
		}
			gap = gap * 2;//一一归完之后,二二归,四四归......
	}
	free(tmp);
}
void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	//找到最大数和最小数
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	//开辟数组空间统计每个数据出现的次数
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		return;
	}
	//数组的值都先初始化为0
	memset(count, 0, sizeof(int) * range);
	//统计每个数据出现的次数(相对映射)
	for (int j = 0; j < n; j++)
	{
		count[a[j] - min]++;
	}
	int i = 0;
	//回写回原数组,注意转换为原来的数
	for (int k = 0; k < range; k++)
	{
		while (count[k]--)
		{
			a[i++] = k+ min;
		}
	}
}

test.c

#include "sort.h"
void testInsertSort()
{
	int a[]= { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	InsertSort(a, n);
	PrintArray(a, n);
}
void testShellSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	ShellSort(a, n);
	PrintArray(a, n);
}
void testSelectSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	SelectSort(a, n);
	PrintArray(a, n);
}
void testBubbleSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	BubbleSort(a, n);
	PrintArray(a, n);
}
void testQuickSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	QuickSort(a,0,n-1);
	PrintArray(a, n);
	QuickSortNonR(a, 0, n - 1);
	PrintArray(a, n);
}
void testMergeSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	MergeSort(a,n);
	PrintArray(a, n);
	MergeSortNonR(a, n);
	PrintArray(a, n);
}
void testCountSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	int n = sizeof(a) / sizeof(int);
	CountSort(a, n);
	PrintArray(a, n);
}
int main()
{
	testInsertSort();
	testShellSort();
	testSelectSort();
	testBubbleSort();
	testQuickSort();
	testMergeSort();
	testCountSort();
	return 0;
}

stack.c

#include "stack.h"
void StackInit(ST* ps)
{
	assert(ps);
	ps->a = (STDataType*)malloc(sizeof(STDataType));
	ps->top = 0;
	ps->capacity = 0;
}
void StackDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->top = 0;
	ps->capacity = 0;
}
void StackPush(ST* ps,STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)//栈满,考虑扩容
	{
		int newcapacity=ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(int));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;//重新指向扩容好的空间
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;//因为top指向的是栈顶元素的下一个,所以直接将top位置的值赋值为x
	ps->top++;//top++,继续指向栈顶元素的下一个

}
void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));//栈不能为空
	ps->top--;
}
STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));//栈不能为空
	return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}
本文含有隐藏内容,请 开通VIP 后查看