C语言实现九大排序算法(建议收藏!)

Posted 爱敲代码的三毛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言实现九大排序算法(建议收藏!)相关的知识,希望对你有一定的参考价值。

文章目录


排序算法

稳定性

如何判断一个排序算法是否稳定?

两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则称该算法是具备稳定性的排序算法。

一个排序是否发生跳跃式的交换也是判断是否稳定的一个技巧。

  • 一个稳定的排序算法可以变成一个不稳定的排序算法
  • 一个本身就不稳定的排序算法是不可以变成一个稳定的排序算法的

1. 插入排序

直接插入排序是一种最简单的排序方法,它的基本操作是将一个记录插入到已排好的有序列表中,从而得到一个新的有序列表。

原理

把待排序的区间分为

  • 有序区间
  • 无序区间

每次选择无序区间的第一个元素,从有序区间的后面向前比较,在有序区间内选择符合要求的位置将元素插入。

直接插入排序的基本思想就是默认下标为0的元素是一个有序区间,让后从下标为1的位置开始向前插入元素,每插入一个元素有序区间的大小就加上1,直到把最后一个元素插入到有序区间内。

这就和平时的玩斗地主类似,每摸一张牌就会把牌插到对应的位置。

排序过程

记录无序区间的第一个数,拿它和有序区间的数从后往前逐个比较如果有序区间的数大于这个数,就将有序区间的比它大的数字往后移动一个位置,直到下标到-1或者不小于有序区间的数,然后将无序区间的数字插入到对应位置。假设要排序的数据是 24 , 19 , 32 , 48 , 38 , 6 , 13 , 24 24,19,32,48,38,6,13,24 24,19,32,48,38,6,13,24

代码实现

  • tmp记录无序区间的第一个数字
  • 从有序区间最后一个元素和tmp做比较
  • 注意比较完后,此时的下标end是在要插入位置的前一个位置,所以要加一
// 直接插入排序
void InsertSort(int* arr, int n)

	int i = 0;
	for (i = 0; i < n-1; ++i)
	
		int end = i;
		int tmp = arr[end + 1];//记录无序区间的第一个元素
		while (end >= 0)
		
			// 拿要插入的元素和有序区间的元素比较
			if (tmp < arr[end])
			
				arr[end + 1] = arr[end];
				end--;
			
			else
			
				break;
			
		
		//插入到有序区间
		arr[end + 1] = tmp;
	

性能分析

先然这里是两层循环嵌套,最坏情况就是当数据是逆序(或者接近逆序)的时候,最好情况当然是已经是有序的时候,这里没有用任何额外空间。开头说过判断稳定性就是一组数据里有相同的元素,如果排序前和排序后,这两个相同的元素的前后关系没有发生变化那么这个排序就是稳定的。

  • 时间复杂度
    • 最好情况: O ( n ) O(n) O(n)
    • 最坏情况: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性
    • 稳定的排序

注意:一组数据的元素越区间于有序,直接插入排序的效率越高。

2. 希尔排序

我们直到直接插入排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2),那么当排序数据非常大的时候,就比较慢了。

假设要排序1万个无序的数据,那么直接插入排序的时间复杂度就是 1000 0 2 = 1 亿 10000^2 = 1亿 100002=1亿,如果采用一种分组的思想,把1万个数据分为100组一组100个,对每组进行直接插入排序,那么每组是有序的整体也就趋近于有序,那么再来排序时间复杂就会大大的降低,这就是希尔排序的思想。

原理

希尔排序又叫做“缩小增量排序”,它也是插入排序的一种。它的基本思想是:先将整个待排序序列通过增量分为若干个子序列分别进行直接插入排序,待整个序列的记录趋近于有序时,再对整体进行一次直接插入排序。

排序过程

假设我们要排序的数据是 24 , 19 , 32 , 48 , 38 , 6 , 13 , 24 , 22 , 2 24,19,32,48,38,6,13,24,22,2 24,19,32,48,38,6,13,24,22,2

我们假设第一次增量为 3 3 3,增量可以理解为每一组元素之间的间隔,也就是通过增量将这组元素分为了3组,对这三组元素分别进行直接插入排序,再把增量设置为2再对这两组进行直接插入排序,经过两轮分组排序之后我们发现,序列中的元素已经整体趋近于有序了,所以再整体进行一次插入排序即可,而越是趋近于有序的数据,直接插入排序的效率也就越高。

关于增量取值

在清华大学严蔚敏的《数据结构C语言版》上右这么一段话,就是关于增量序列的问题,目前尚求得最后的增量序列,但是需要注意的是:应使增量序列中的值没有除1之外的公因子,并且最后一个增量必须是1

  • 增量越大:大的和小的数可以更快的移动到对应的方向,且越不接近于有序
  • 增量越小:大的和小的数可以更慢移动到对应的方向,且越接近于有序

代码实现

这里的代码是通过一次就把所有元素排序完成。

  • gap的取值保证最后为1就可以了
  • gap不等于1之前其实都是预排序,让数据趋近于有序。
// 希尔排序
void ShellSort(int* arr, int n)

	int gap = n;
	while (gap > 1)
	
		gap = gap / 3 + 1;//保证最后gap为1就可以了
		int i = 0;
		for (i = 0; i < n - gap; ++i)//多组元素同时进行插入排序
		
			int end = i;
			int tmp = arr[end+gap];
			while (end >= 0)
			
				if (arr[end] > tmp)
				
					arr[end+gap] = arr[end];
					end -= gap;
				
				else
				
					break;
				
			
			arr[end + gap] = tmp;
		
	


性能分析

希尔排序的时间复杂是不太好计算的,它取决于增量的取值,在严蔚敏的数据结构书上也有说到涉及到数学上尚未解决的问题。假设gap的取值是3,那么每一次循环的次数就是 n / 3 / 3 / 3... / 3 = 1 n/3/3/3.../3 = 1 n/3/3/3.../3=1,那就是 3 x = n 3^x=n 3x=n,然后插入排序本来时间复杂度应该是 O ( n 2 ) O(n^2) O(n2),当这里进行了多次预排序,数组已经很接近有序了,所以这里的插入排序可以认为是 O ( n ) O(n) O(n)

  • 时间复杂度

    • 最坏的时间复杂度 O ( l o g 3 n ∗ n ) O(log_3n*n) O(log3nn)(针对我这里的代码)

    • 希尔排序的时间复杂为 O ( n 1.3 到 1.5 ) O(n^1.3到1.5) O(n1.31.5)

  • 空间复杂度

    • O ( 1 ) O(1) O(1)
  • 稳定性

    • 不稳定的排序(发生跳跃式交换)

3. 选择排序

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

原理

每次从无序区间中选取一个最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的元素排序完。

  • 在元素集合中 a r r [ i ]   a r r [ n − 1 ] arr[i]~arr[n-1] arr[i] arr[n1]中选择最大(最小的元素)
  • 若它不是这组元素中的最后一个(第一个元素),则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的 a r r [ i ]   a r r [ n − 2 ] arr[i]~arr[n-2] arr[i] arr[n2]集合中,重复此步骤,直到集合中剩余一个元素为止

排序过程

直接选择排序比较简单,就是从待排序区间找最大或者最小的数放到排序完成的区间,每一次都排序都能确定一个排序好的元素。

代码实现

每一次遍历待排序区间都记录最小元素的下标然后和排序完成的区间的后一个位置的元素进行交换。

// 选择排序
void SelectSort(int* arr, int n)

	int i = 0;
	for (i = 0; i < n - 1; ++i)
	
		int minIndex = i;//记录待排序区间最小元素下标
		int j = 0;
		for (j = i + 1; j < n; ++j)
		
			if (arr[minIndex] > arr[j])
			
				minIndex = j;
			
		
		int tmp = arr[minIndex];
		arr[minIndex] = arr[i];
		arr[i] = tmp;
	


性能分析

选择排序是一种简单直观的排序算法,无论什么数据进行排序都是 O ( n 2 ) O(n^2) O(n2),如果要用选择排序,数据规模越小是越好的。

  • 时间复杂度
    • 最好最坏都是 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度
    • O ( 1 ) O(1) O(1)
  • 稳定性
    • 不稳定(发生跳跃式交换)

4. 堆排序

堆排序是利用数据结构堆的特性来进行排序,它也是一种选择排序,它通过堆来选取数据。排升序建大堆,排降序建小堆。
堆的详细介绍可以看这一篇文章数据结构堆的详解

原理

每次将堆顶元素和最后一个元素进行交换,再进行向下调整,然后缩小待排序区间,直到数据有序,因为堆顶的元素一定是一组数据中的最大或者最小值。

注意:向下调整的前提是,这个根节点的左右子树一定要是一个堆(大堆或小堆)

排序过程

代码实现

  • 首先要将数组通过向下调整算法建立成逻辑上的堆
  • 然后将堆顶元素和待排序区域最后一个元素进行交换,然后从堆顶开始向下调整
  • 因为堆顶的元素一定是最大或者最小的,每次交换都会确定一个元素排序成功。
// 向下调整(建大堆)
void AdjustDown(int* arr, int n, int index)

	int parent = index;
	int child = parent*2+1;// 左孩子下标

	while (parent < n)
	
		// 找出左右孩子中较大的那一个
		if (child < n && child + 1 < n && arr[child] < arr[child + 1])
		
			++child;
		
		//和父亲比较
		if (child < n && arr[child] > arr[parent])
		
			int tmp = arr[child];
			arr[child] = arr[parent];
			arr[parent] = tmp;
			parent = child;//让调整的位置成为新的父节点
			child = parent*2 + 1;
		
		else
		
			// 说明无需调整
			break;
		
	


// 堆排序
void HeapSort(int* arr, int n)

	//建堆
	int i = 0;
	for (i = (n - 2) / 2; i >= 0; --i)
	
		AdjustDown(arr, n, i);
	
	// 排序
	int end = n-1;
	while (end > 0)
	
		// 堆顶元素和最后一个待排序元素交换
		int tmp = arr[0];
		arr[0] = arr[end];
		arr[end] = tmp;
		// 向上调整
		AdjustDown(arr, end, 0);
		--end;
	

性能分析

建堆的时间复杂度为 O ( n ) O(n) O(n),向下调整的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)

  • 时间复杂度
    • O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n)
  • 空间复杂度
    • O(1)
  • 稳定性
    • 不稳定(明显的跳跃式交换)

5. 冒泡排序

原理

冒泡排序也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

排序过程

每一趟冒泡排序都能确认一个元素最终的位置,这是一趟冒泡排序的过程

代码实现

当遍历了一遍待排序区间没有发生交换时,说明数组已经有序无需再排序了。

// 冒泡排序
void BubbleSort(int* arr, int n)

	int i = 0;
	for (i = 0; i < n - 1; ++i) // 冒泡排序趟数
	
		int j = 0;
		int flag = 1;
		for (j = 0; j < n - i - 1; ++j) // 待排序区间进行比较交换
		
			if (arr[j] > arr[j + 1])
			
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 0;
			
		
		if (flag == 1)
		
			// 说明已经有序
			break;
		
	

性能分析

  • 时间复杂度
    • O ( n 2 ) O(n^2) O(n2)
    • 只有当数据时有序的时候才为 O ( n ) O(n) O(n)
  • 空间复杂度
    • O ( 1 ) O(1) O(1)
  • 稳定性
    • 稳定的排序
    • 很明显这是一个稳定的排序,没有发生跳跃式交换且交换的都是相邻的元素

6. 快速排序

原理

快速排序的基本思想是,通过选取一个关键元素key为一趟排序,将待排序元素分割成独立的两个部分,其中一部分记录的元素大小要比另外一部分的元素小。