八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)

Posted 梦乡回雪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)相关的知识,希望对你有一定的参考价值。

八大排序算法


如果点个赞


万字总结


前言

排序是数据结构中很重要的一章,先介绍几个基本概念。

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

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

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


一、插入排序

时间复杂度

最坏:-----------O(N^2)
最好:-----------O(N)
平均:-----------O(N^2)

空间复杂度

O(1)
稳定性:稳定

-插入排序 』:顾名思义就是把每一个数插入到有序数组中对应的位置。
就相当于你玩扑克牌的过程,抓来一张牌,就放在对应有序位置

  • 直接插入排序:
    当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

代码实现(升序):

void InsertSort(int* a, int n)

	for (int i = 0; i < n - 1; i++)
	
		int x = a[end+1];//x为待排序的值
		int end = i;//从end开始往前和x依次比较

		while (end >= 0)
		
			if (a[end] > x)//只要当前的值大于x继续往前找
			
				a[end+1] = a[end];
				end--;
			
			else
			
				break;//跳出循环说明a[end] <= x
			
		
		a[end + 1] = x;//跳出循环说明a[end] <= x,需要把x插入到end前边
	

  • 那么我们可以看到,越是接近有序的数组,插入排序的效率越高(有序时对于任何一个数只需要和前边的数比较一次)。

二、希尔排序

时间复杂度

O(n^(1.3—2))

空间复杂度

O(1)

稳定性:稳定

  • 希尔排序 』(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
  • 该方法实质上是一种『 分组插入 』方法,因为插入排序对于接近有序的数组排序效率非常高,那么希尔提出:
  • 算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行分组,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
  • 一般的初次取序列的一半为增量,以后每次减半,直到增量为1。
    并且插入排序可以看成分组是1的希尔排序。动图如下:

  • 因为插入排序可以看做gap==1的希尔排序,因此只需要改变插入排序中for循环的增量控制排序即可。

代码:

void ShellSort(int* a, int n)

	//按gap分组进行预排序
	int gap = n;
	while (gap>1)
	
		//gap = gap / 2;
		gap = gap / 3 + 1;//这里分组选每次折半或者/3都可以

		for (int j = 0; j < gap; j++)//gap个组
			for (int i = j; i < n - gap; i+=gap)//每个组从j开始每个增量gap	
			
				int end = i;
				int x = a[end + gap];
				while (end >= 0)
				
					if (a[end] > x)
					
						a[end + gap] = a[end];
						end -= gap;
					
					else
					
						break;
					
				
				a[end + gap] = x;
			
	

  • 关于希尔排序时间复杂度证明比较复杂,取决于gap怎么取,如果按照Knuth提出的/3,来取是O(n^(1.25)- 1.6*O(n^1.25).
  • 希尔排序的特性总结:
  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
    会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

三、选择排序

时间复杂度

最坏:-----------O(N^2)
最好:-----------O(N^2)
平均:-----------O(N^2)

空间复杂度

O(1)
稳定性:不稳定

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

代码:

void SelectSort(int* a, int n)

	int begin = 0;
	int end = n - 1;
	int mini = begin;//记录最小值下标
	while (begin<end)
	
		for (int i = begin; i < end; i++)
		
			if (a[i] < a[mini])
			
				mini = i;//更新最小值下标
			
		
		Swap(&a[mini],&a[begin]);//把最小值放到左边
		++begin;//左边对应起始位置++
	

  • 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用。

四、堆排序

时间复杂度

最坏:-----------O(N * logN)
最坏:-----------O(N * logN)
平均:-----------O(N*logN)

空间复杂度

O(1)

  • 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
  • 具体可见另一篇文章堆排序和TopK问题
  • 动图:

代码:

void Swap(int* px,int* py)

	int t = (*px);
	(*px) = (*py);
	 (*py)= t ;

void AdjustDown(int* a, int n, int parent)

	int child = parent * 2 + 1;
	while (child < n)
	
		if (child + 1 < n && a[child + 1] > a[child])
		
			child++;
		
		if (a[child] > a[parent])
		
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		
		else
		
			break;
		
	


void HeapSort(int* a, int n)

	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	
		AdjustDown(a, n, i);
	
	for (int i = n - 1; i > 0; i--)
	
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	

五、冒泡排序

时间复杂度

最坏:-----------O(N^2)
最好:-----------O(N)
平均:-----------O(N^2)

空间复杂度

O(1)

  • 冒泡排序 』是大家最熟悉的也是最容易理解的排序,如下图:
  • 冒泡排序基本思想 』就是每一次将相邻的数据进行『 两两比较 』,选出最大的依次比较送到右边,那么最右边就是最大值,而左边留下的自然就是小的(排升序)
    -冒泡排序 』需要两层循环
    内层循环 』表示一次冒泡,也就是两两比较先选出最大的放到最右边,同时注意每一次冒泡选出最大元素,那么两两比较次数-1(下一次不用比较选好的最右边)
    外层循环 』控制的是冒泡的次数(假设数组N 个元素)也就是N-1次冒泡选出N-1个最大的元素
    初版代码如下:
//初版:
void Swap(int* px, int* py)

	int t = (*px);
	*px = (*py);
	(*py) = t;

void BubbleSort(int* a, int n)

	for (int i = 0; i < n-1; i++)//外层循环
	
		for (int j = 0; j < n-1-i; j++)
		
			if(a[j]>a[j+1])
				Swap(&a[j],& a[j + 1]);//交换
			flag = 1;
			
	

  • 时间复杂度分析:每一次比较次数是N-1,N-2,N-3***1.因此是N(N-1)/2
  • 但是这种写法还是有缺陷,时间复杂度永远是O(N^2) , 对于一个已经排好序的数组来说,还是需要N^2的复杂度,但对于有序的数组,每一次冒泡都不会进行交换因为有序,因此如果只要任何一次冒泡中没有数据交换就证明数组有序了。时间复杂度最好也可以达到0(N)。

代码优化如下:

//优化:
void BubbleSort(int* a, int n)

	for (int i = 0; i < n-1; i++)
	
		int flag = 0;
		for (int j = 0; j < n-1-i; j++)
		
			if(a[j]>a[j+1])
			Swap(&a[j],& a[j + 1]);
			flag = 1;
		
		if (flag == 0)
			break;

	

六、快排排序

时间复杂度

最坏:-----------O(N^2)
最好:-----------O(logN)
平均:-----------O(logN)

空间复杂度

O(logN)

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

递归写法:

  • 框架:
// 假设按照升序对a数组中[left, right)区间中的元素进行排序
void QuickSort(int* a, int left, int right) 
	 if(right >= left )
	 	return;//递归截止条件
	 
	 // 按照基准值对a数组的 [left, right]区间中的元素进行划分
	 int keyi= partion(a, left, right);
	 
	 // 划分成功后以keyi为边界形成了左右两部分 [left, keyi-1] 和 [keyi+1, right]
	 // 递归排[left, keyi-1]
	 QuickSort(a, left, keyi-1);
	 
	 // 递归排[keyi+1, right]
	 QuickSort(a, keyi+1, right);

  • . 递归框架写完了接下来就差partion函数的实现也就是快排的灵魂,去每一次找基准值。那么一共有三种写法如下:

  • hoare版本

    1. 首先就是要找基准值,这里你可以选最左边或最右边的值(图中是6)
    1. 两个指针指向头(这里选左为基准值,头指针指向第二个)和尾,基准值选左,则右指针先走,反之左指针先走。
    1. 左指针找到比基准值大的停下,右指针找比基准值小的停下,交换左右指针指向值
    1. 重复2.3动作,直到左右指针相遇,交换左指针值和基准值
  • 左值为基准,右指针先走找比6小的:

  • 左值为基准,右指针先走找比6小的:

  • 交换:



最终效果:相遇交换左指针和基准值,保证了6的左边都比6小,右边比6大。

  • 并且除此之外,由于我们看到这种算法类似于二叉树的思想排好中间再排左右子树,因此我要保证选取的随机值尽量位与中位数。所以我们采取三数取中的方法。(选取最左值最右最中间的数的中位数)效率是可以提升5%到10%的。
//三数取中
int GetMidIndex(int* a, int left, int right)

	//int mid = (left + right) / 2;
	//int mid = left + (right - left) / 2;
	int mid = left + ((right - left)>>1);
	if (a[left] < a[mid])
	
		if (a[mid] < a[right])
		
			return mid;

		
		else if (a[left] > a[right])
		
			return left;
		
		else 
		
			return right;
		
	
	else//a[left] > a[mid]
	
		if (a[mid] > a[right])
		
			return mid;

		
		else if (a[left] < a[right])
		
			return left;
		
		else
		
			return right;
		
	


int Partion(int* a, int left,int right)

	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);
	
	int keyi = left;
	while (left < right)
	
		while (left < right && a[right] >= a[keyi])
		
			right--;
		
		while (left < right && a[left] <= a[keyi])
		
			left++;
		
		Swap(&a[left], &a[right]);
	Swap(&a[left], &a[keyi]);
	return left;

  • 挖坑法

  • 挖坑法就是对hoare版本的一种变形,过程如下:
    初始如下:先保存基准值,基准值形成一个坑位!

  • 左为基准,右指针先走,找到小的送到坑位,那么此刻右指针形成了新的坑位

  • 左指针出动,找到大的继续送到坑位,左指针形成了新的坑位



  • 指针相遇,把6写入。也保证左边比6小,右边比6大。代码如下:

//挖坑法
int Partion2(int* a, int left, int right)

	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);

	int key = a[left];
	int pivot = left;
	while (left < right)
	
		//右边先找小
		while (left< right && a[right] >= key)
		
			--right;
		
		a[pivot] = a[right];
		pivot = right;
		while (left < right && a[left] <= key)
		
			++left;
		
		a[pivot] = a[left];
		pivot = left;
	
	a[pivot] = key;
	return pivot;

  1. 前后指针版本
  • 顾名思义,使用两个指针,这里选取左为基准值为例,两个指针从左开始出发一个cur,一个prev。
  • 要求:
    cur指针先走,一旦找到比基准值小的就停下,++prev,并交换。
    cur指针一直到头为止,最后交换prev指向值和基准值



  • 1和2都比6小cur走一步停一步,prev++并交换,指向相等。

  • cur越过7和9去找小的3,此时停下,prev++指向7交换。(我们注意到prev和cur不等时prev永远是去找大的,cur是找小的,因此交换就做到把cur指向的小的往前扔,大的往后仍,)



  • 整个过程如上,代码:
//前后指针法
int Partion3(int* a, int left, int right)

	int mini = GetMidIndex(a, left, right);
	Swap(&a[mini], &a[left]);
	int prev = left, cur = left+1;
	int keyi = left;
	while (cur<=right)
	
		if (a[cur] < a[keyi] 八大排序算法(C语言实现)

C语言实现八大排序算法

数据结构c语言版八大算法(上)图文详解带你快速掌握——希尔排序,堆排序,插入排序,选择排序,冒泡排序!

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

八大排序 (万字总结)(详细解析,建议收藏!!!)

[八大排序]0基础C语言实现八大排序,详解快排,归并,希尔