C语言实现八大排序(动画演示)

常见排序算法(C/C++)--- 动画演示 本篇将介绍一些常见的排序算法,如插入排序:直接插入排序、希尔排序;选择排序:选择排序、堆排序;交换排序:快速排序、冒泡排序;以及最后的归并排序。 对于以上的排序算法,我们总结了每种排序算法的特性,接着对直接插入排序进行了优化;然后实现了归并排序和快速排序的非递归形式;并且实现了快速排序的三种类型:Hoare、前后指针、挖坑法,以及如何优化快速排序。最后,我们还通过生成大量的随机数据,来测试每种排序算法消耗的时间。 还给出了每种算法的动画演示,便于读者理解 阅读详情

前言

排序算法有很多,因其思想及依赖的数据结构不同造成对应排序方法性能的不同。本文将介绍常用的八大排序算法。让大家对各类排序有所了解。

八大排序

注意:本文介绍的排序均为排升序

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

  • 稳定性介绍:
    稳定性介绍

特性总结

为方便大家更好理解下面实现的各类排序,先对各类排序算法特性进行总结

排序方式平均情况最好情况最坏情况空间复杂度稳定性
直接插入排序O(N^2)O(N)O(N^2)O(1)稳定
选择排序O(N^2)O(N^2)O(N^2)O(1)不稳定
冒泡排序O(N^2)O(N^2)O(N^2)O(1)稳定
希尔排序O(N^1.3)O(N^1.25)O(N^1.6)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
快速排序O(N*logN)O(N*logN)O(N^2)O(logN~N)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
计数排序O(max(range,N))O(max(range,N))O(max(range,N))O(range)稳定
  • 不稳定的原因在下图:
    稳定性解释

冒泡排序

开始先上盘开胃小菜:想必冒泡排序大家都不陌生了

思想:

冒泡就是将当前位置的元素与后一个位置的元素进行比较,符合排序要求就交换两个位置的元素。

冒泡排序

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void BubbleSort(int* a, int n)
{
	//每一趟
	for (int i = 0; i < n; i++)
	{
		int flag = 1;//优化
		//每一轮
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 0;
			}
		}
		//如果一轮下来都不需要交换元素,则说明数组元素已经有序,则可直接跳出循环
		if (flag == 1)
		{
			break;
		}
	}
}

实现:

冒牌排序需要嵌套两层循环,外层循环控制趟数,内层循环控制排序元素的位置。每一趟排序完成会将当前轮的最值排到最后一个位置。需要注意的是内外循环结束的条件

  • 外层循环控制的是趟数:每一趟只将当前趟数的最值排到最后的位置,一趟只排好一个位置,也就是需要排n-1趟。
  • 内层循环控制排序位置:每一趟会将元素排好在最后面,所以每一轮就不需要排到已经排好元素的位置,所以停止条件为j < n - i - 1。

冒泡排序的时间复杂度是经典的O(N^2),这是因为嵌套两层循环,而且每轮只能排好一个元素,效率是非常低下的,即便我们增加了flag来优化:判断当前轮是否已经排完序,是则无需再排。但效率依旧很低。

选择排序

一般的选择排序一轮只选最大或者最小的元素,效率低下。我们这里直接升级一下,同时选最大和最小,一轮排一大一小两个元素。

思想:
每一次从待排序的数据元素中选出最小和最大的两个元素,存放在序列的起始,结束位置,直到全部待排序的数据元素排完 。(下图演示的是只选一个的选择排序)

//优化版,同时选最大的和最小的,将大的往后放,小的往前放,不断缩小区间
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)//要注意前一轮交换是否会影响下一轮
		{
			maxi = mini;//若最大值在第一个位置,则需要更新一下交换后最大值的下标位置
		}
		Swap(&a[end], &a[maxi]);
		begin++;//控制区间
		end--;
	}
}

选择排序

实现:
选择排序也由两层循环一起完成:外层用来检测元素是否相遇,相遇则说明已经排完序了。内层循环用来控制待排序区间:先选出最小和最大值,再将其与当前区间首尾位置元素进行交换。最后不断缩小当前区间,直到排好序。

  • 需要注意的是在每轮第一次交换是否会影响到下一一次交换。

即便我们推出优化版本,但选择排序的思想注定了它的时间复杂度只能是

直接插入排序

思想:

我们将待排序元素与其前面位置的元素进行比较(前面元素已经有序),直到找到目标位置将其插入

实际中我们玩扑克牌时,就用了类似的思想
扑克牌插入排序
直接插入排序

//类似插排,将要插入的数与前面的数(已有序)一一比较,将元素后移,直到找到要插入的位置
void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int tmp = a[i];//要插入的值
		int prev = i - 1;//插入值的前一位
		while (prev >= 0)
		{
			if (a[prev] > tmp)
			{
				a[prev+1] = a[prev];//将元素后移
				prev--;//向前继续比对
			}
			else//找到插入位置
			{
				break;
			}

		}
		a[prev + 1] = tmp;//插入,prev位置之后
	}
}

实现:
由于待排序元素前面已经是有序数组,所以我们从第二个位置开始排序。先用临时变量tmp记录待排序元素,然后将其与前面位置的元素一一比较,如果前一个大于tmp(此时排升序),则将前面的元素往后覆盖,直至找到位置break出循环。这时候再将tmp插入目标位置(目标位置为前一元素之后)

  • 需要注意的是排序位置之前已经是有序数组,要记住这一点,这也是在循环里为什么不是往后覆盖数据就是break出循环的原因。
  • 还有一点就是待插入元素的位置:以升序为例,如果前一元素已经小于待插入元素,则主动跳出循环,再将其插到前一元素的后面,也就是prev + 1的位置。如果待插入元素是最小值,那么只能等prev越界,此时prev + 1便为首元素位置。

直接插入排序的时间复杂度也为O(N^2) ,但与前面两个O(N^2) 的相比,直接插入排序是O(N^2)家族里的唯一牌面了,虽说是同一级别的,但在实际中也还能拿的出手
直接插入排序
以上单位为毫秒,对100000个数据进行排序(测试方法后续再讲解)。
该测试可以看出冒泡是真的没得救了,完全上不了桌,选择排序优化后好像也还能用,插入排序与他们相比给人一种错觉:好像他们不是这个量级的。

  • 他们的时间复杂度都为O(N^2) 没错,这是由于时间复杂度表示方法的原因,将他们都归为了一类,但在实际中直接插入排序最好情况下是可以达到O(N)的
  • 结合动图及代码可以看到,当直接插入排序要排的是接近有序数组时,一趟下来就能将数组排好,做到O(N)的级别。

希尔排序

我们的大佬Shell,对上面的直接插入排序很看好,能够有O(N)级别的潜力,觉得很有潜力,是个潜力股,决心好好挖掘一下。

思想:

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap作为距离,把待排序数据中所有个数记录分成n个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。

希尔排序图示
希尔排序

//1 当gap不为1时为预排序
//2 当gap为1时是插入排序
void ShellSort(int* a, int n)
{
	int gap = n;//要保证最后gap为一,变为插入排序
	while (gap > 1)//保证最后一次gap为一
	{
		gap = gap / 3 + 1;//逐渐缩小gap
		for (int i = 0; i < n - gap; i++)//多组并排,注意边界
		{
			int prev = i;
			int tmp = a[i + gap];
			while (prev >= 0)
			{
				if (a[prev] > tmp)//升序
				{
					a[prev + gap] = a[prev];//将大于tmp的值往后覆盖
					prev-=gap;//间隔gap个
				}
				else
				{
					break;
				}
			}
			a[prev + gap] = tmp;//参照插入排序,逻辑是一样的
		}
	}
}

实现:
由图示和动画演示可以看出希尔排序是直接插入排序的优化:当gap不为1时是预排序。当gap为1时就是直接插入排序,此时的数据已经接近有序,再进行直接插入排序就能完全释放他的潜力,达到O(N)级别。

  • gap不为1:预排序:预排序的目的是将数组接近有序。我们将距离为gap的分为一组,让每个gap组接近有序,再不断缩小gap,重复以上操作,以此让数组接近有序。(预排序的排序逻辑与直接插入排序一致,具体步骤也写在注释里了)
  • 当gap为1时为直接插入排序。

对于希尔排序的时间复杂度的计算就有点麻烦了,希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定。
希尔排序时间复杂度

  • 所以我们一般按以O(N^1.3) 作为希尔排序的时间复杂度。

堆排序

在此前的博客中已经对堆排序进行了讲解,请各位移步到堆排序进行查看。

堆是二叉树结构,是一种十分高效的数据结构,堆排序也是依赖这种结构,其时间复杂度为O(N*logN)的级别,而接下来的快速排序,归并排序也是基于这种结构思想来实现的,所以我们从这里开始对各种排序的排序速度的直观对比。

排序时间测试:
可以先浅浅看一下不同时间复杂对应的变化曲线。
时间复杂度对比图
测试对比
怎么样,对这个结果感到惊讶吗?(以上单位为毫秒,对100000个数据进行排序。)
这简直就是O(N*logN)对O(N^2)的降维打击。
以下为粗略计算可以直观看出两个级别的差距(实际运算时仍有别的开销,无需具体落实差距,只需要看在哪个级别就好)
差距描述
测试的代码实现:
在堆区开辟数组大小N的数组,再利用rand()产生N个随机数,测试多组时需要开对应个数组,其内容需要一样。
clock函数的使用需要包含头文件<time.h>,其功能为记录一开始遇到该函数直到第二次遇到该函数所用的时间,我们利用这两个的差值来记录每种排序消耗的时间。

void TopTest()
{
	//srand((unsigned int)time(NULL));
	srand(time(0));
	int N = 100000;
	int* a1 = (int*)malloc(sizeof(int)*N);
	int* a2 = (int*)malloc(sizeof(int)*N);
	int* a3 = (int*)malloc(sizeof(int)*N);
	int* a4 = (int*)malloc(sizeof(int)*N);
	int* a5 = (int*)malloc(sizeof(int)*N);
	
    //确保每组数据相同
	for (int i = 0; i < N; i++)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];

	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	printf("InsertSort: %d\n", end1 - begin1);

	int begin2 = clock();
	BubbleSort(a2, N);
	int end2 = clock();
	printf("BubbleSort: %d\n", end2 - begin2);

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();
	printf("SelectSort: %d\n", end3 - begin3);

	int begin4 = clock();
	ShellSort(a4, N);
	int end4 = clock();
	printf("ShellSort: %d\n", end4 - begin4);

	int begin5 = clock();
	HeapSort(a5, N);
	int end5 = clock();
	printf("HeapSort: %d\n", end5 - begin5);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);

}

int main()
{
	TopTest();
	
	return 0;
}
  • 接下来的排序的时间测试都以此为模板,自己对应添加测试就好
    release
    测试时需要改为release版本,让程序得到最好的优化,否则可能体现不出各种排序的差距。

快速排序

快排可以算得上是各种排序算法中的名人了,就连库里面的qsort也是用快速排序写的。

思想:

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

快速排序是Hoare大佬提出的一种二叉树结构的交换排序方法,采用了类似二叉树的结构,选定基准值,划分左小右大的区间,再通过递归重复以上操作,最终达到排序的目的。
快排结构
快排得益于其思想,衍生出了很多版本,我们在此介绍三个版本。

Hoare版本

开始先介绍最原汁原味的版本
Hoare版快排
Hoare排序动图

//hoare版:选出基准值(默认选左边第一位),让右边先往左走,停在小于基准值的位置,左边的再出发选出大于基准值的,交换两数
//类似二叉树结构,使用递归,划分左右区间,左边的小于基准值,右边的大于基准值
int PartSort1(int *a,int left,int right)
{
	int keyi = left;
	while (left < right)
	{   //一轮换一次             //右边找小
		while (right > left && a[right] >= a[keyi])//防止极端情况下越界,=是为了防止死循环
		{
			right--;
		}
		                      //左边找大
		while (left <right&& a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//此时已经相遇
	Swap(&a[keyi], &a[left]);
	return left;//返回相遇位置的下标,再以此为划分点继续划分左右区间
}
//每次递归划分两个区间,左边的比keyi对应的值小,右边比keyi对应的值大
//二叉树结构,递归分治
void QuickSort(int* a, int begin,int end)//end为下标
{
     //递归终止条件
	if (begin >= end)//只有一个的时候或不存在的区间
	{
		return;
	}
	int keyi= PartSort1(a, begin, end);
	//[0,keyi-1] keyi [keyi+1,end]
	QuickSort(a, begin, keyi - 1);//左区间
	QuickSort(a, keyi + 1,end);//右区间
}

实现:
以上图为例,默认将下标为left(也就是区间最左边的元素)的值选做基准值。从右边开始往左找,直到找到比基准值小的元素,然后停下。这时开始从左往右找,直到找到比基准值大的元素,停下然后交换他们的值。重复这个循环直到左右下标相遇,这时再将基准值与相遇坐标的值交换,便完成了区间的划分。基准值左边的元素都是小于基准值,右边都是大于基准值的元素再通过递归继续划分区间,最终达到排序的目的。

需要注意的细节有:

  • 默认左边为基准值则需从右边开始往左动,默认右边为基准值则需要从左边开始往右动。
  • 递归的终止条件:当区间只有一个元素时则无需再排;由于keyi-1,keyi+1的操作会导致最终出现不存在的区间。
  • 右边找小,左边找大的循环里要注意是否越界;找值时一定要大于等于或小于等于基准值,否则以下情况就会出现死循环
  • 死循环情况

思想核心:

该思想最为重要的问题——如何确保相遇位置的值一定小于基准值呢?(以左边为基准值为例,右边也是同理。)
因为基准值默认为区间最左边的值,那么在左右下标相遇,与基准值交换后,基准值左边的元素都要小于基准值,基准值右边都要大于基准值。所以在左右下标相遇交换基准值时就必须保证相遇位置的元素一定是小于基准值的,否则该思想就不成立了。

对于以上疑问,可以分为两种情况:

  • 情况一:L(动)遇R,R停了,L在走;R先走,R停下来的位置就以确保一定比基准值小,此时R的位置如果是相遇位置,则一定小于基准值。
  • 情况二:R(动)遇L,在相遇轮,L就没动过,R在动,直到与L相遇,相遇位置就是L的位置(L的位置有可能就是区间最左侧位置或者交换过一些轮次的位置),都确保小于等于基准值。

由于快速排序是基于二叉树结构的交换排序方法,其时间复杂度一般为:O(N*logN)。

性能对比:
快排性能
此时的数据量为100000个。

挖坑法

这一版本虽然叫挖坑法,但是坑却要比Hoare版少不少,而且实际排序时也像在挖坑,故叫挖坑法。

思想:

挖坑法思想与Hoare版的思想是一致的,只是在实现形式上有所区别。

挖坑法
挖坑法动图


//挖坑法
//仍为hoare思想,但更好理解
//第一个位置为坑,记录值,右边找小,填到坑位并变成新的坑位,然后左边找大填到坑位后变成新坑位
//直到区间收缩到只有一个位置,即最后的坑位,此时将一开始记录的值填进去,并返回坑位(即划分左右区间的位置)完成一次递归
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{        //右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		     //左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return left;
}

//每次递归划分两个区间,左边的比keyi对应的值小,右边比keyi对应的值大
//二叉树结构,递归分治
void QuickSort(int* a, int begin,int end)//end为下标
{
	if (begin >= end)//只有一个的时候或不存在的区间
	{
		return;
	}

	int keyi= PartSort2(a, begin, end);
	//[0,keyi-1] keyi [keyi+1,end]

	QuickSort(a, begin, keyi - 1);//左区间
	QuickSort(a, keyi + 1,end);//右区间
}

实现:
挖坑法先用Key将最初坑位(默认为区间左边第一个)的值(也可叫基准值)先保存起来,并记录坑位。然后先从右往左找小于基准值的,找到后填入上一个坑位,并更新坑的位置。然后到左边找大也是一样的逻辑。直到左右下标相遇,这时再将一开始保存的基准值key填进相遇位置,完成左右区间的划分,返回中间位置下标,完成一趟区间的划分。再通过递归直到完成排序。

由于思想是一样的,挖坑法的时间复杂度也是O(N*logN)。

前后指针法

再介绍一个前后指针法的快排版本

前后指针法
前后指针法动图

思路

默认最左的为基准值,让cur一直走,当cur走到的值小于基准值,prev跟走cur一起走,当cur走到的值大于基准值,prev不走,此时他们间隔的就是大于基准值的,当下标cur与++prev不等时,就可交换他们两个二对应的值,这样就相当于把大的翻滚式往前推,同时将小的放到左边。当cur越界后返回prev的位置,这样又完成了单趟的区间划分,再通过递归以上操作完成排序。

  • 三种快排思想都是一致的,都是选出基准值,然后划分左右区间,左区间小于基准值,右区间大于基准值,再通过递归完成排序。
int PartSort3(int* a, int left, int right)
{
	int keyi = left;//默认最左的为keyi
	int prev = left;//先前指针从最左边开始
	int cur = left + 1;//从第二位开始
	while (cur <= right)
	{   //当cur位置的值比keyi小或相等时,prev跟着跟随cur一起动
		if (a[cur] <= a[keyi] && ++prev != cur)//前置++,即做到了上述要求,又可以减少不必要的交换
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;//cur一直往前走
	}
	Swap(&a[keyi], &a[prev]);//此时说明已经相遇
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int begin,int end)//end为下标
{
	if (begin >= end)//只有一个的时候或不存在的区间
	{
		return;
	}

	int keyi = PartSort3(a, begin, end);
	//[0,keyi-1] keyi [keyi+1,end]

	QuickSort(a, begin, keyi - 1);//左区间
	QuickSort(a, keyi + 1,end);//右区间
}

实现:
仍以区间最左边元素为基准值,prev也从基准值位置出发,cur在prev的下一位开始出发;cur每轮都往前走,当cur位置的值大于基准值时,prev不走,这样cur和prev间隔的就是大于基准值的元素;当cur位置的值小于等于基准值时,让prev也往前走,并且当++prev与cur不等时,才交换这两个位置的值(如果两下标相等则为同一值,交换没有意义)。直到cur越界跳出循环,此时prev位置就是划分左右区间的位置,将其与基准值交换,返回该位置,完成一趟区间划分。再通过递归完成所有区间的划分,完成排序。

三种版本的思想相同,时间复杂度都为O(N*logN)。

快排总结:
对于这三种版本的,其核心思想都是一致的,只是实现形式不同,其实际效率也是一样的
三种快排效率

  • N为1000000。

快排小优化

三数取中

快排是一种二叉树结构的交换排序方法,其O(N*logN)的时间复杂度依赖于他的结构思想。当基准值为数组中位数时,能够快速划分好区间,效率是最快的。
快排结构
但是当数组是接近有序时,也就是最坏的情况,使时间复杂度就会变成O(N^2)

快排最坏情况
快排最坏情况

  • 此时快排排的是一个有序的数组

所以为了避免这种最坏的情况发生,我们通过三数取中的办法来解决这个问题。

  • 三数取中也就是在排序前将数组的前中后三个位置的元素进行比较,将三者的中位数与区间的第一位交换,让其成为基准值。
  • 这个方法是为了防止数组已经有序,出现最坏的情况。
    三数取中
int GetMidIndex(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else//a[left] > a[midi]
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else if (a[midi] > a[right])
		{
			return midi;
		}
		else
		{
			return right;
		}
	}
}

void QuickSort(int* a, int begin,int end)//end为下标
{
	if (begin >= end)//只有一个的时候或不存在的区间
	{
		return;
	}
	int midi = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[midi]);

	int keyi= PartSort1(a, begin, end);
	//[0,keyi-1] keyi [keyi+1,end]

	QuickSort(a, begin, keyi - 1);//左区间
	QuickSort(a, keyi + 1,end);//右区间
}

  • 通过if else语句便能完成三数取中的逻辑。
  • 在区间划分前加上三数取中的操作即可。
  • 注意要交换的是当前区间最左侧的值与中位数,即begin位与midi位。

小区间优化:
快排是通过递归划分区间完成排序的。当区间元素个数较多时,递归可以快速划分,但是当区间元素个数不多时,直接调用别的排序要比继续递归划分更为快速。

无小区间优化
无小区间优化

有小区间优化
小区间优化

  • 当区间个数小于20个时直接使用希尔排序
void QuickSort(int* a, int begin,int end)//end为下标
{
	if (begin >= end)//只有一个的时候或不存在的区间
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 20)
	{
		ShellSort(a + begin, end - begin + 1);
		return;
	}
    //三数取中
	int midi = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[midi]);

	int keyi= PartSort1(a, begin, end);
	//[0,keyi-1] keyi [keyi+1,end]

	QuickSort(a, begin, keyi - 1);//左区间
	QuickSort(a, keyi + 1,end);//右区间
}
	
  • 当然这种做法只是锦上添花,并不会使快排有本质的提升。

归并排序

思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序核心步骤:
归并排序
归并排序动画

  • 归并排序要好好观看动图,实际排序就是按动图这样排序的。
void _MergeSort(int* a, int begin, int end, int* tmp)//结合动图看会更加好理解
{
	if (begin == end)//与快排的递归条件有些不同,这里不会存在大于的区间
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 20)
	{
		ShellSort(a + begin, end - begin + 1);
		return;
	}
	//二分,不断缩小区间,也类似二叉树,采用递归
	int mid = (begin + end) / 2;
	   //[0 mid] [mid+1 end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	//已经不能分了,开始合并
	//类似二叉树,有“左右子树”
	int begin1 = begin, end1 = mid;
	int begin2 = mid+1, end2 = end;

	int i = begin;//**一直划分到不可分,所以并不是从最开始的位置归并
	while (begin1 <= end1 && begin2 <= end2)//放入tmp数组,只会放完一组
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//将剩余组的放进tmp
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//拷回原数组,注意元素位置和元素个数
	memcpy(a + begin, tmp + begin, sizeof(int)*(end-begin+1));

}

void MergeSort(int* a,int n)//n为个数
{
	//n -= 1;//转为下标
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed");
		return 0;
	}
	//需要一个额外的数组来存放归并的元素,所以多写一个子程序来实现,否则递归调用时频繁开辟新的数组
	_MergeSort(a, 0, n-1 , tmp);//传下标
	free(tmp);
}

实现:
准备工作:
归并排序在排序时需要一个额外的数组来存放排好序的元素,所以先开辟一个额外是数组tmp,并在子函数_MergeSort进行递归操作。

实现过程:
在子函数_MergeSort进行递归,通过将区间一分为二的方式进行区间划分到只有一个元素时停止递归 (在一开始实现时,小区间优化不要加上,先理解归并排序是如何进行的),开始排序;排序时要清楚当前区间的范围,通过begin1,end1,begin2,end2来标识当前区间,因为归并排序是两组归并成一组的,所以要标识这两个区间,才能进行排序。通过循环比较这两个区间,最终会将这两个区间元素有序放入tmp数组。最后将tmp数组元素拷贝回原本数组a:这里是归并好两个区间就拷贝回a数组,所以要注意拷贝的起始位置和要拷贝的个数(memcpy(a + begin, tmp + begin, sizeof(int)*(end-begin+1)))。

归并排序也是二叉树结构思想,其时间复杂度为:O(N*logN)。

归并排序时间复杂度

  • 可以看到归并与快排都是一个级别的,都是O(N*logN)

计数排序

思想:

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤如下:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中
  • 计数排序不通过比较来进行排序:其通过相对映射来确定元素的大小关系,再结合统计的元素个数来完成排序。

计数排序

void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)//找出两个最值
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//开辟对应范围的数组
	int range = max - min + 1;//最值差加一为元素个数
	//int* countA = (int*)malloc(sizeof(int) * range);
	//memset(countA, 0, sizeof(int) * range);
	int* countA = (int*)calloc(range, sizeof(int));//开辟对应的元素个数数组
	
	//统计个数
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;//相对映射定位
	}

	//排序
	int i = 0;
	for (int j = 0; i < range; j++)
	{
		while (countA[j]--)//对应位置个数
		{
			a[i++] = j + min;//对应位置加min变回原来值
		}
	}
	free(countA);
}

实现:
1.求范围:要统计出现元素的次数,首先需要知道数据的范围,所以先通过一次循环遍历找出最大最小值,这样就可以知道数据的范围了。

2.开辟统计次数的数组CountA:知道范围后便能开辟出对应个数的数组CountA(该数组用来记录数组a对应位置出现元素的个数)。

3.在CountA数组统计a数组中元素出现的次数:开辟好用来统计元素出现次数的数组CountA后,便开始统计a数组中每个元素出现的次数,通过相对映射(a数组中的元素减去最小值min后的值便是CountA数组的下标)的原则在CountA数组对应位置统计出现的次数。

4.排序:最后就是按照CountA数组中对应位置的值按相对映射原则加回min的值放回a,完成排序。
计数排序

计数排序只能用于整型数据的排序,浮点型数据不能用计数排序,原因是不能使用相对映射的原则。而且对数据也有一定要求,适用于较为集中的,不是很分散的数据(要开辟数组CountA)。
但是它的时间复杂度是O(1)!

计数排序时间复杂度

O ( 1 ) O(1) O(1)对 O ( l o g N ) O(logN) O(logN)又是一波降维打击,在效率上让快排也黯然失色。但由于其有一定限制,所以在排序算法中快排还是综合性较好的排序算法。

篇幅较长,难免有错误地方,欢迎佬们指出。

c语言数据结构——八大排序算法实现 c语言实现八大排序算法 阅读详情

相关推荐

数据结构——八大排序算法c语言实现

数据结构——八大排序算法c语言实现 插入,希尔,选择,冒泡,堆排,快排,归并,计数 c语言实现,并分析其时间,空间复杂度以及稳定性 #include<stdio.h> #include<stdlib.h> #include"Sort.h" #include<memory.h> void Swap(int* array, int i, int j){ int tmp=array[i]; array[i]=array[j]; array[j]=tmp; } //插入

qq_33431840的博客 666

动画图解:十大经典排序算法动画与解析,看我就够了!(配代码完全版)

排序算法是《数据结构与算法》中最基本的算法之一。 排序算法可以分为内部排序和外部排序。 内部排序是数据记录在内存中进行排序。 而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。 用一张图概括: 时间复杂度与空间复杂度 关于时间复杂度: ...

程序员吴师兄的博客 6万+

Obsidian与GPT协作:数学公式从KaTeX到MathJax的无缝转换实践

本文针对Obsidian用户从GPT等AI助手复制数学公式时遇到的KaTeX与MathJax格式不兼容问题,提供了高效的解决方案。重点介绍了KaTeX to MathJax插件的核心作用,它能自动将AI生成的KaTeX格式公式(如\(...\))无缝转换为Obsidian默认支持的MathJax格式($...$),实现复制粘贴后公式即时渲染,极大提升了数学笔记与AI协作的效率和流畅度。

weixin_29209825的博客 137

动画图解:十大经典排序算法动画与解析,看我就够了(配代码完全版)

排序算法是《数据结构与算法》中最基本的算法之一。 排序算法可以分为内部排序和外部排序。 内部排序是数据记录在内存中进行排序。 而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。 常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。 用一张图概括: 时间复杂度与空间复杂度 关于时间复杂度: 平方阶 (O(n...

lgxzzz的博客 6524

MOSFET热失效机理与保护电路设计:实战案例

深入解析MOSFET工作原理及其在高温下的失效机制,结合实际案例探讨有效保护电路设计,提升系统可靠性与安全性。

weixin_28713083的博客 851

避坑指南:ZYNQ平台上AXI 1G/2.5G Ethernet Subsystem实现IEEE1588的5个关键点

本文详细解析了在ZYNQ平台上使用AXI 1G/2.5G以太网子系统实现IEEE1588精密时间协议(PTP)的5个关键避坑点。从硬件选型、Vivado工程配置到Linux驱动适配,提供了实战经验与优化技巧,帮助工程师在工业自动化、电力系统等场景中实现纳秒级时间同步。

weixin_30687051的博客 236

十大经典排序算法动画与解析,看我就够了

作者 |程序员小吴转载自五分钟学算法(ID: CXYxiaowu)排序算法是《数据结构与算法》中最基本的算法之一。排序算法可以分为内部排序和外部排序。内部排序是数据记录...

AI科技大本营 1万+

飞牛NAS 0day漏洞导致断网?完整的排查修复方案来了(含一键检测修复工具)

家里网络莫名断网,折腾十来天才发现是NAS被0day漏洞利用了?🔍 本文详细记录了从发现问题到最终解决的全过程,包括如何自查系统是否被入侵、使用检测脚本和专杀脚本进行修复,以及如何通过端口管理、访问控制、监控预警等措施加固系统安全 🔒,帮助遇到同样问题的用户快速排查和解决NAS安全风险 ✅

XiaoqiangClub的博客 5517

数据结构与算法-十大排序算法(动画演示)

本文系统介绍了10种经典排序算法,包括概念、分类、复杂度分析及具体实现。主要内容包括: 算法基础概念:稳定性、时间/空间复杂度,比较类与非比较类排序的区别; 常见算法实现: 插入排序:通过元素扫描插入实现 冒泡排序:相邻元素比较交换 快速排序:分治思想,基准值分区 选择排序:选择最小元素交换 堆排序:构建最大堆交换排序 归并排序:分治合并有序子序列 希尔排序:增量分组插入排序 线性时间非比较排序: 计数排序:统计元素频次 桶排序:分桶后单独排序 基数排序:按数位分配收集 每种算法均包含详细描述、动画演示和代

分享前端工程化、AI 应用、Agent 开发与大模型实践! 2993

手把手教你学 Simulink (1.3):探索Simulink模块库的奇妙世界

Simulink模块库按照功能分为多个类别,每个类别包含专门用于特定任务的模块。% 打开Simulink库浏览器% 或者以编程方式获取库列表disp('可用的库列表:');end常用模块(Simulink):最基础和最常用的模块信号处理模块(DSP System Toolbox):专业信号处理工具通信模块(Communications Toolbox):通信系统设计控制系统模块(Control System Toolbox):控制系统设计与分析% 创建自定义模块库。

QQ_778132974的博客 384

【YOLO】YOLOV5-Seg的Segment模块解析

Segment模块同时输出检测结果和分割掩模,通过轻量级分割头实现实时实例分割

浩瀚之水的专栏 599

C语言——八大排序

一、冒泡排序 /时间复杂度:O(n^2) //空间复杂度:O(1) //稳定性:稳定 主要思路如下: 1: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2:对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 3:针对所有的元素重复以上的步骤,除了最后一个。 4: 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字...

Victor的BLOG 1万+

基于灰度区域生长算法的红外目标检测算法matlab仿真

基于灰度区域生长算法的红外目标检测算法是一种常用的红外目标检测技术。它通过对红外图像进行灰度区域生长,找到具有相似灰度值的像素区域来检测目标。在本文中,我们将详细介绍基于灰度区域生长算法的红外目标检测算法的原理、步骤和应用。一、原理灰度区域生长算法是一种基于像素相似性的区域分割算法。它通过对图像中具有相似灰度值的像素进行聚类,将它们组合成更大的区域。灰度区域生长算法通常包括以下步骤:1.初始化:选择一个种子点作为起始点,将其作为第一个区域的中心点。

平时工作较为繁忙,私信消息一般晚上回复,谢谢大家~~ 251

【数据结构基础】之八大排序(C语言实现)

算法和数据结构是有一定的关联的,八大排序算法在数据结构里算是比较重要的一个部分,今天本篇博客将介绍八大排序算法的原理和实现。

wszhj_的博客 1724

【100%通过率】华为od机试C卷【特殊的加密算法】Python 实现

有一种特殊的加密算法,明文为一段数字串,经过密码本查找转换,生成另一段密文数字串。规则如下1.明文为一段数字串由0-9组成2.密码本为数字0-9组成的二维数组3.需要按明文串的数字顺序在密码本里找到同样的数字串,密码本里的数字串是由相邻的单元格数字组成,上下和左右是相邻的,注意:对角线不相邻,同一个单元格的数字不能重复使用。

MISAYAONE的博客 5924

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

八大排序总结(2)——选择排序(Selection Sort)(c语言实现)

&gt;&gt;&gt;八大排序总结(1)——冒泡排序(Bubble Sort)(c语言实现)&lt;&lt;&lt; &gt;&gt;&gt;八大排序总结(3)——插入排序(Insertion Sort)(c语言实现)&lt;&lt;&lt; &gt;&gt;&gt;八大排序总结(4)——快速排序(Quick Sort)(c语言实现)&lt;&lt;&lt;

Zebra的博客 3319

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

八大排序算法(C语言实现)

本文详细的介绍了常见的7大排序和一个小众的计数排序,介绍了冒泡、选择、插入、希尔、堆排、快排、归并、计数排序,里面都是用C语言编写的,图文并茂,写的非常详细,希望大家进来看完都能有所收获

weixin_47271425的博客 3698

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

算法入门必备:C语言实现八大排序算法对比表

在计算机科学领域,排序算法是最基础且重要的算法之一。它在数据处理、搜索算法优化等众多场景中都有广泛的应用。对于C语言开发者而言,掌握常见的排序算法不仅能提升编程能力,还能在实际项目中更好地处理数据。本文将详细介绍八大排序算法,并使用C语言实现它们,最后通过对比表的形式分析各算法的优缺点和适用场景。

星辰夜语的博客 1137

八大排序总结(3)——插入排序(Insertion Sort)(c语言实现)

>>>八大排序总结(1)——冒泡排序(Bubble Sort)(c语言实现)<<< >>>八大排序总结(2)——选择排序(Selection Sort)(c语言实现)<<< >>>八大排序总结(4)——快速排序(Quick Sort)(c语言实现)<<< >>>八大排...

Zebra的博客 3196

“手把手”教你C语言八大排序

C语言八大排序详解 目录排序的概念及其应用常见的八大排序算法直接插入排序直接插入排序代码实现直接插入排序算法时间复杂度及稳定性分析希尔排序希尔排序代码实现希尔排序算法时间复杂度及稳定性分析选择排序选择排序代码实现选择排序算法时间复杂度及稳定性分析 排序的概念及其应用 排序的概念及其运用 1、排序的概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原

qq_49613557的博客 1971

【排序算法】八大排序(下)(c语言实现)(附源码)

堆排序、快速排序、归并排序、计数排序、八大排序大数据量的运行时间对比

2301_80955819的博客 1783

八大排序算法汇总(C语言实现)

本专栏内容为:八大排序汇总 通过本文章的深入学习,你可以了解并掌握八大排序以及相关的排序算法。文章内容:直接插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序(Hore版本,挖坑法,前后指针法),归并排序(递归与非递归实现)计数排序,排序算法的稳定性总结

小小unicorn的博客 1030

【C语言】八大排序实现及稳定性和总结

假设第一个数据(下标0)有序,后面的n-1个数据都需要插入到有序数组里保存有序数据后面一个数据(tmp)移动数据,需要把新数据插入有序数据中的合适位置移动数据停止条件有两个tmp比全部有序的数据要小,有序数据从头到尾都需要移动,end移动到-1,说明移动完成。当tmp > end下标的数据,说明已经有序了。插入数据到end+1的下标位置,因为end下标的数据是小于tmp。// 插入排序(升序)

Eraser的博客 1770

八大排序算法的实现--C语言

八大排序算法的实现–C语言 1.插入排序 void InsertSort(int* arr, int size) { for (int i = 0; i < size - 1; i++) { int end = i; int tmp = arr[end + 1]; while (end >= 0) { if (arr[end] > tmp) { ...

weixin_43554618的博客 392

【转】C语言——八大排序

C语言——八大排序在这里插入代码片 #一、冒泡排序 时间复杂度:平均情况:O(n^2) 最好情况:O(n) 空间复杂度:O(1) 稳定性:稳定 主要思路: 1.比较相邻的元素。如果第一个比第二个大,就交换它们两个。 2.对每一个相邻元素做同样的工作,从开始第一对到结尾的每一对。在这一 点,最后的元素应该会是最大的数。 3.针对多有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上...

ChristinaXinny的博客 430
上一篇: 【DS】C语言实现堆排序及应用(动画演示)
下一篇: C语言过渡C++
Mesar_
博客等级 码龄3年 926粉丝 56原创
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值