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

news2025/1/9 1:59:59

文章目录

  • 排序算法
    • 稳定性
    • 1. 插入排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
    • 2. 希尔排序
      • 原理
      • 排序过程
      • 关于增量取值
      • 代码实现
      • 性能分析
    • 3. 选择排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
    • 4. 堆排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
    • 5. 冒泡排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
    • 6. 快速排序
      • 原理
      • Hoare法
      • 挖坑法
      • 前后指针法
      • 快排的优化
        • 三数取中
        • 区间优化
      • 非递归实现快速排序
      • 性能分析
    • 7. 归并排序
      • 原理
      • 排序过程
      • 递归代码实现
      • 非递归代码实现
      • 性能分析
    • 8. 计数排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
    • 9. 基数排序
      • 原理
      • 排序过程
      • 代码实现
      • 性能分析
      • 性能分析


排序算法

稳定性

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

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

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

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

在这里插入图片描述

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_{3}n*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_{2}n) O(log2n)

  • 时间复杂度
    • O ( n ∗ l o g 2 n ) O(n*log_{2}n) 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为一趟排序,将待排序元素分割成独立的两个部分,其中一部分记录的元素大小要比另外一部分的元素小。

  • 从待排序区间选一个元素作为基准
  • 重新排序数列,比基准小(或者大)的放在基准左边,比基准大(或者小)的放在基准右边(相同的数无所谓放哪边),这一次分区被称为一趟快速排序,此时基准左边的数一定是小于(或大于)基准的,而右边则是大于(或小于基准的)
  • 再采用分支思想,对基准的左右两个区间按照同样的方式进行处理,直到区间大小变成1或者0,说明此区间已经有序,就停止递归。

Hoare法

Hoare法是快速排序的一种实现方法,它的实现思路为

  • 定义left和right变量记录区间
  • 每次取left为基准key(也可以是左边)
  • 然后让right从右往左找比key大的值(如果是以left取基准,一定要让right先走)
  • 然后让left从左往右找比基准key大的值
  • 再让left和right的值进行交换
  • 等left和right相遇之后,就把相遇位置的值和基准key的值进行交换

在这里插入图片描述

代码实现

// 快速排序
void QuickSort(int* arr, int n)
{
	Partition(arr, 0, n - 1);
}
// 快排Hoare法
void Partition(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int keyIndex = begin;
	int left = begin;
	int right = end;
	while (left < right)
	{
		// 从右往左找比基准小的元素(一定是严格小于)
		while (left < right && arr[right] >= arr[keyIndex])
		{
			--right;
		}
		// 从左往右找严格大于基准的元素
		while (left < right && arr[left] <= arr[keyIndex])
		{
			++left;
		}
		Swap(&arr[left], &arr[right]);
	}
	// 将基准和相遇点的值交换
	Swap(&arr[keyIndex], &arr[left]);

	// 递归左区间
	Partition(arr, begin, left - 1);
	// 递归右区间
	Partition(arr, left + 1, end);

}

挖坑法

挖坑法其实和Hoare方法类似,它的思想是:

  • 定义left和right记录区间开始和结束
  • 每次拿key记录区间起始位置的值(left位置的值)叫做挖坑
  • 然后right从右往左找一个比key小的元素放到坑位中,此时right产生了一个新的坑位
  • 此时让left从左往右找比key大的值放到right位置的坑位
  • 就这样不断挖坑填坑,直到left和right相遇就把记录的key值放到相遇的位置
  • 接着递归相遇点的左右区间

在这里插入图片描述

代码实现

// 快速排序
void QuickSort(int* arr, int n)
{
	QuickInternal(arr, 0, n - 1);
}
// 快排挖坑法
void QuickInternal(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	// 记录基准值(挖坑)
	int key = arr[begin];
	int left = begin;
	int right = end;
	while (left < right)
	{
		// 从右往左找比基准值到小的元素去填坑
		while (left < right && arr[right] >= key)
		{
			--right;
		}
		// 填坑
		arr[left] = arr[right];
		// 左往右找比基准值大的元素(挖坑)
		while (left < right && arr[left] <= key)
		{
			++left;
		}
		//填坑
		arr[right] = arr[left];

	}
	// 把记录的基准值放到相遇点
	arr[left] = key;

	// 递归左区间
	QuickInternal(arr, begin, left - 1);
	// 递归右区间
	QuickInternal(arr, left + 1, end);
	
}

前后指针法

前后指针法稍微和前面两有一点不同,它的基本思想是:

  • 拿一个key记录待排序区间起始位置的值
  • 再定义prev指向起始位置,cur指向起始位置的后一个位置
  • 然后cur从前向后找比key小的值,找到小于key的值后prev往后走一步
  • 然后把cur和prev位置的值进行交换
  • 直到cur超出数组长度,然后把prev和key的值进行交换,就完成了一趟快速排序
  • 此时prev左边的数要小于prev右边的数
  • 然后递归左右区间,最后完成整个数组的排序

手绘一趟排序的gif图

在这里插入图片描述

代码实现

循环里的++prev != cur是当prev和cur相遇是没有必要交换的

// 快排前后指针法
void QuickPtr(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int prev = begin;
	int cur = prev + 1;
	int keyIndex = prev;

	while (cur <= end)
	{
		// 当找到了小于key的值
		if (arr[cur] < arr[keyIndex] && ++prev != cur)
		{
			//小于key就让prev往后走一步再交换
			++prev;
			Swap(&arr[prev], &arr[cur]);
		}
		++cur;
	}
	// 交换prev和key的值
	Swap(&arr[keyIndex], &arr[prev]);

	// 递归左边区间
	QuickPtr(arr, 0, prev - 1);
	// 递归右边区间
	QuickPtr(arr, prev + 1, end);
}

快排的优化

前面三种写法的快速排序无论是哪一种,好像排序的时间复杂度是 O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n),当如果是极端情况数据已经是有序的化,那么它的时间复杂度将会上升到 O ( n 2 ) O(n^{2}) O(n2),递归的层次如果太深就可能会出现栈溢出的情况,所以要对其进行优化。优化代码基于挖坑法

三数取中

在选择基准key的时候,我们永远选择的是最左边也就是第一个元素,起始这并不理想,就比如数据已经有序的情况下就会让时间复杂度变成 O ( n 2 ) O(n^{2}) O(n2),所以key的取值是非常关键的,这就可以使用三数取中法,取3个数的中位数,如果key的取值越接近一组数据的中位数,那么快排的效率也就越高。

所以要在left、right和它们的中间下标mid中取一个中位数来做基准,将这个中位数和left的值进行交换。

这里来以挖坑法为例,优化代码:

// 三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (left + right) >> 1;
	if (arr[mid] > arr[left])
	{
		if (arr[mid] > arr[right])
		{
			if (arr[left] > arr[right])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else 
		{
			if (arr[left] > arr[right])
			{
				return right;
			}
			else
			{
				return left;
			}
		}
	}
}

// 快排挖坑法
void QuickInternal(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	// 三数取中
	int mid = GetMidIndex(arr, begin, end);
	int tmp = arr[mid];
	arr[mid] = arr[begin];
	arr[begin] = tmp;
	// 记录基准值(挖坑)
	int key = arr[begin];
	int left = begin;
	int right = end;
	while (left < right)
	{
		// 从右往左找比基准值到小的元素去填坑
		while (left < right && arr[right] >= key)
		{
			--right;
		}
		// 填坑
		arr[left] = arr[right];
		// 左往右找比基准值大的元素(挖坑)
		while (left < right && arr[left] <= key)
		{
			++left;
		}
		//填坑
		arr[right] = arr[left];
		
	}
	// 把记录的基准值放到相遇点
	arr[left] = key;

	// 递归左区间
	QuickInternal(arr, 0, left - 1);
	// 递归右区间
	QuickInternal(arr, left + 1, end);
}

区间优化

快排在递归的过程中递归的次数每一层都是上一层的2倍数,如果当递归达到了一个比较深的层次,每继续往下一层都是一个比较大的增加递归次数,此时就可以做一个优化,就是在区间缩小到到一定大小时使用直接插入排序,当然也可以时堆排序,来减少递归次数。虽然优化在有些场景作用微乎其微,但在有些场景比如有千万数据的时候,这个区域的取值越大可能效果就越明显。

// 快排挖坑法
void QuickInternal(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	// 插入排序优化
	if (end - begin > 30)
	{
		// 三数取中
		int mid = GetMidIndex(arr, begin, end);
		Swap(&arr[mid], &arr[begin]);
		// 记录基准值(挖坑)
		int key = arr[begin];
		int left = begin;
		int right = end;
		while (left < right)
		{
			// 从右往左找比基准值到小的元素去填坑
			while (left < right && arr[right] >= key)
			{
				--right;
			}
			// 填坑
			arr[left] = arr[right];
			// 左往右找比基准值大的元素(挖坑)
			while (left < right && arr[left] <= key)
			{
				++left;
			}
			//填坑
			arr[right] = arr[left];

		}
		// 把记录的基准值放到相遇点
		arr[left] = key;

		// 递归左区间
		QuickInternal(arr, begin, left - 1);
		// 递归右区间
		QuickInternal(arr, left + 1, end);
	}
	else
	{
		// 调用直接插入排序
		InsertSort(arr + begin, end-begin+1);
	}
	
	
}

非递归实现快速排序

非递归的快排只能通过栈来模拟递归实现,而且C语言没有栈,所以可以使用自己实现的栈或者使用C++的STL。

这里我使用自己实现的栈来实现非递归挖坑版快排。

  • 首先将左边界下标入栈
  • 再将右边界入栈
  • 然后每次拿出两个边界,进行挖坑填坑
  • 一次快排完成后,判断左右区间是否还有元素,有的化就继续入栈
  • 当栈为空时就完成了
// 非递归快排(挖坑法)
void NoRecursiveQuick(int* arr, int n)
{
	Stack stack;
	StackInit(&stack);
	StackPush(&stack, 0);
	StackPush(&stack, n-1);
	while (!StackEmpty(&stack))
	{
		int end = StackTop(&stack);// 获取栈顶元素
		StackPop(&stack);
		int begin = StackTop(&stack);
		StackPop(&stack);

		int left = begin;
		int right = end;
		int key = arr[left];
		
		while (left < right)
		{
			// 从右往左找数坑坑
			while (left < right && arr[right] >= key)
			{
				--right;
			}
			arr[left] = arr[right];
			// 从左往右找数填坑
			while (left < right && arr[left] <= key)
			{
				++left;
			}
			arr[right] = arr[left];
			
		}
		// 把key值放到相遇点
		arr[left] = key;

		// 左区间
		if (left - 1 > begin)
		{
			StackPush(&stack, begin);
			StackPush(&stack, left - 1);
			
			
		}
		// 右区间
		if (left + 1 < end)
		{
			StackPush(&stack, left+1);
			StackPush(&stack, end);
		}

	}

}

性能分析

快排的递归的执行其实就是一颗二叉树,每一层也就是执行 n n n次,树的高度为 l o g 2 n log_{2}n log2n,空间复杂度也是同理。

  • 时间复杂度
    • O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)
    • O ( n 2 ) O(n^{2}) O(n2)(如果没有优化,数据已经有序的情况)
  • 空间复杂度
    • O ( l o g 2 n ) O(log_{2}n) O(log2n)
    • 最坏情况 O ( n 2 ) O(n^{2}) O(n2)
  • 稳定性
    • 不稳定(跳跃式交换)

7. 归并排序

归并排序又是一类不同与前面的排序,归并的意思就是将两个或者两个以上的数组,将它们合并成一个新的有序数组,这也是分治算法的典型应用,将有序的子序列合并,得到完全有序的序列,先让每个子序列有序,再让子序列有序,在使子序列段间有序。若将两个有序表合成一个有序表,称为二路归并。

原理

递归把一个数组所有元素不断对半拆分,直到拆分成一个元素,然后在两两开始合并,直到所有元素最后合并成一个有序数组。

排序过程

在这里插入图片描述

在这里插入图片描述

递归代码实现

  • 如果区间元素大于1就需要对半分开递归
  • 递归完后开始返回,每次返回就是在合并有序数组
  • 合并完后再把临时数组拷贝到原数组里
// 归并排序
void MergeSort(int* arr, int n)
{
	int* newArr = (int*)(malloc(sizeof(int)*n));
	Merge(arr, 0, n - 1,newArr);


	free(newArr);
}

// 归并排序的分解合并
void Merge(int* arr, int left, int right, int* newArr)
{
	if (left >= right)// 超过1个元素才分解
	{
		return;
	}

	int mid = (left + right) >> 1;
	Merge(arr, left, mid, newArr);//递归左边
	Merge(arr, mid + 1, right, newArr);//递归右边

	int start1 = left;
	int end1 = mid;
	int start2 = mid + 1;
	int end2 = right;
	// 合并两个有序数组
	int index = left;// 注意开始位置
	while (start1 <= end1 && start2 <= end2)
	{
		if (arr[start1] <= arr[start2])
		{
			newArr[index] = arr[start1];
			++index;
			++start1;
		}
		else
		{
			newArr[index] = arr[start2];
			++index;
			++start2;
		}
	}
	// 把剩下的元素放到数组中

	while (start1 <= end1)
	{
		newArr[index] = arr[start1];
		++index;
		++start1;
	}

	while (start2 <= end2)
	{
		newArr[index] = arr[start2];
		++index;
		++start2;
	}

	// 把临时数组复制到原数组
	while (left <= right)
	{
		arr[left] = newArr[left];
		++left;
	}

}

非递归代码实现

非递归的实现也是将元素先将单个元素合并成两个有序的元素,再将两个合并成4个,直到整体合并成一个有序数组。

  • 以gap为间隔将元素分组,也可以把gap理解为每个待合并区间元素个数
  • gap每次以2倍增长,因为每次都是两组合并,合并之后元素个数就会翻倍
  • 需要注意就是后面区间的越界处理。

在这里插入图片描述

如果元素个数不是那么均匀

  • 最后一组归并时,第二个区间不存在
  • 最后一组归并时,第一个小区间不够gap个
  • 对与这两种情况,我这里简单进行处理int end2 = (start2 +gap-1) >= n ? (n-1) : (start2+gap-1);
  • 第二个区间的末尾,如果越界就把它赋值成数组的最后一个元素

在这里插入图片描述

代码实现

// 归并排序非递归实现
void MergeSortNonR(int* arr, int n)
{
	// 临时数组
	int* tmp = (int*)(malloc(sizeof(int) * n));

	int gap = 1; // 每个待合并的区间元素个数
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2*gap) // 多少组进行合并,每次跳过已经合并的两个区间
		{
			int start1 = i;
			int end1 = i + gap - 1;
			int start2 = i + gap;
			// 需要考虑特殊情况下的越界
			int end2 = (start2 +gap-1) >= n ? (n-1) : (start2+gap-1);

			// 合并有序数组
			int index = start1;
			while (start1 <= end1 && start2 <= end2)
			{
				if (arr[start1] <= arr[start2])
				{
					tmp[index] = arr[start1];
					++start1;
					++index;
				}
				else
				{
					tmp[index] = arr[start2];
					++start2;
					++index;
				}
			}

			// 将剩下的元素放到临时数组中
			while (start1 <= end1)
			{
				tmp[index] = arr[start1];
				++start1;
				++index;
			}

			while (start2 <= end2)
			{
				tmp[index] = arr[start2];
				++start2;
				++index;
			}

			// 将临时数组的元素拷贝到原数组
			index = i;
			while (index <= end2)
			{
				arr[index] = tmp[index];
				++index;
			}
		}

		// 改变gap大小
		gap *= 2;
	}
	free(tmp);
}

性能分析

归并排序使用递归进行分解,是一棵树型结构,而每一层都有 n n n个元素,归并排序还使用了额外的数组辅助。

  • 时间复杂度
    • O ( m = n l o g 2 n ) O(m=nlog_{2}{n}) O(m=nlog2n)
  • 空间复杂度
    • O ( n ) O(n) O(n)
  • 稳定性
    • 稳定的排序
    • 没有发生跳跃式的交换,比较的是相邻的元素,所以是稳定的

8. 计数排序

原理

计数排序是一种非比较的排序,也就是它不需要拿一组数据中的元素进行比较排序,它是一种类似于哈希表的变形,记录一组数据中每个数据出现的次数,然后直接放到原数组中。

排序过程

假设有一组数据 5 , 4 , 4 , 3 , 8 , 7 , 8 , 6 , 3 , 1 {5,4,4,3,8,7,8,6,3,1} 5,4,4,3,8,7,8,6,3,1

在这里插入图片描述

在这里插入图片描述

代码实现

实现计数排序不能使用绝对映射,而要使用相对映射

  • 绝对映射

    比如有5个数据 900 , 901 , 902 , 903 , 904 , 905 {900,901,902,903,904,905} 900,901,902,903,904,905,如果采用绝对映射就是开一个大小为906的数组,来计数元素的出现次数,显然出现了大量的空间浪费

  • 相对映射

    而相对映射就不会出现这种情况,同样是数据 900 , 901 , 902 , 903 , 904 , 905 {900,901,902,903,904,905} 900,901,902,903,904,905,我们开的数据大小就是一组元素的最大值减去减去最小值再加上1( m a x − m i n + 1 max-min+1 maxmin+1),也就是6。然后记录出现次数就是 a r r [ 元素 − m i n ] arr[元素-min] arr[元素min],取元素就是 i + m i n i+min i+min就好了,这样大大减少了空间的浪费。

显然计数排序适合数据比较集中的情况。

// 计数排序
void CountSort(int* arr, int n)
{
	
	
	// 记录最大最小值
	int min = arr[0];
	int max = arr[0];
	
	// 统计出最大最小值
	int i = 0;
	for (i = 0; i < n; ++i)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if (arr[i] < min)
		{
			min = arr[i];
		}
	}
	// 计算元素范围
	int range = max - min + 1;
	// 开辟空间记录数字出现次数
	int* countArr = (int*)(malloc(sizeof(int) * range));
	memset(countArr, 0, sizeof(int) * range);
	// 记录元素出现次数
	for (i = 0; i < n; ++i)
	{
		++countArr[arr[i] - min];
	}
	// 将元素放进原数组
	int index = 0;
	for (i = 0; i < range; ++i)
	{
		int count = countArr[i];
		while (count)
		{
			arr[index] = i + min;
			--count;
			++index;
		}
		
	}

	free(countArr);

}

性能分析

  • 时间复杂度
    • O ( n + r a n g e ) O(n+range) O(n+range)
  • 空间复杂度
    • O ( r a n g e ) O(range) O(range)
  • 稳定性
    • 显然是稳定的排序

9. 基数排序

原理

基数排序也不是基于比较的排序,基数排序是一种借助多关键字排序的思想。假设要排序一组数字,它一次排序都是先比较每个数字的个位数进行比较排序,再拿十位比较排序、依次类推。

排序过程

在这里插入图片描述

代码实现

  • 通过队列来先进先出的性质,来用十个队列当做桶
  • 循环最大元素的位数次,依次按十位、百位…依次入队列
  • 从0号队列开始出队列到数组
  • 直到所有循环结束,元素就有序了。
// 获取位
int GetKey(int value, int k)
{
	int key = 0;
	while (k >= 0)
	{
		key = value % 10;
		value /= 10;
		--k;
	}

	return key;
}

// 按位数入桶
void Distribute(int* arr, int left, int right, int k,Queue* bucket)
{
	int i = 0;
	for (i = left; i < right; ++i)
	{
		int key = GetKey(arr[i], k);
		QueuePush(&bucket[key], arr[i]);
	}
}

// 把桶中的元素放回数组
void Collect(int* arr,Queue* bucket)
{
	int k = 0;
	int i = 0;
	for (i = 0; i < 10; ++i)
	{
		while (!QueueEmpty(&bucket[i]))
		{
			arr[k] = QueueFront(&bucket[i]);
			QueuePop(&bucket[i]);
			++k;
		}
	}
}

// 基数排序
void RadixSort(int* arr, int n)
{
	// 初始化队列
	Queue bucket[10];
	int i = 0;
	for (i = 0; i < 10; ++i)
	{
		Queue q;
		QueueInit(&q);
		bucket[i] = q;
	}
	// 计算最大位数
	int maxCount = 0;
	for (i = 0; i < n; ++i)
	{
		int count = 0;
		int tmp = arr[i];
		while (tmp)
		{
			++count;
			tmp /= 10;
		}
		maxCount = maxCount >= count ? maxCount : count;
	}
	
	// 循环最大位数次
	for (i = 0; i < maxCount; ++i)
	{
		Distribute(arr, 0, n,i,bucket);
		Collect(arr, bucket);
	}

}

性能分析

这里计算位数是 O ( n ) O(n) O(n),多次基数排序 m a x C o u n t ∗ n maxCount*n maxCountn,这里使用了10个队列,额外的空间

  • 时间复杂度
    • O ( n ∗ m a x C o u n t ∗ n ) O(n*maxCount*n) O(nmaxCountn)
  • 空间复杂度
    • O ( n ) O(n) O(n)
  • 稳定性
    • 显然是一个稳定的排序

从0号队列开始出队列到数组

  • 直到所有循环结束,元素就有序了。
// 获取位
int GetKey(int value, int k)
{
	int key = 0;
	while (k >= 0)
	{
		key = value % 10;
		value /= 10;
		--k;
	}

	return key;
}

// 按位数入桶
void Distribute(int* arr, int left, int right, int k,Queue* bucket)
{
	int i = 0;
	for (i = left; i < right; ++i)
	{
		int key = GetKey(arr[i], k);
		QueuePush(&bucket[key], arr[i]);
	}
}

// 把桶中的元素放回数组
void Collect(int* arr,Queue* bucket)
{
	int k = 0;
	int i = 0;
	for (i = 0; i < 10; ++i)
	{
		while (!QueueEmpty(&bucket[i]))
		{
			arr[k] = QueueFront(&bucket[i]);
			QueuePop(&bucket[i]);
			++k;
		}
	}
}

// 基数排序
void RadixSort(int* arr, int n)
{
	// 初始化队列
	Queue bucket[10];
	int i = 0;
	for (i = 0; i < 10; ++i)
	{
		Queue q;
		QueueInit(&q);
		bucket[i] = q;
	}
	// 计算最大位数
	int maxCount = 0;
	for (i = 0; i < n; ++i)
	{
		int count = 0;
		int tmp = arr[i];
		while (tmp)
		{
			++count;
			tmp /= 10;
		}
		maxCount = maxCount >= count ? maxCount : count;
	}
	
	// 循环最大位数次
	for (i = 0; i < maxCount; ++i)
	{
		Distribute(arr, 0, n,i,bucket);
		Collect(arr, bucket);
	}

}

性能分析

这里计算位数是 O ( n ) O(n) O(n),多次基数排序 m a x C o u n t ∗ n maxCount*n maxCountn,这里使用了10个队列,额外的空间

  • 时间复杂度
    • O ( m a x C o u n t ∗ n ) O(maxCount*n) O(maxCountn)
  • 空间复杂度
    • O ( n ) O(n) O(n)
  • 稳定性
    • 显然是一个稳定的排序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/118579.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Easy App Locker - 给你的 mac 应用加锁保护你的隐私

Easy App Locker - 给你的 mac 应用加锁保护你的隐私 Easy App Locker可以对Mac上的单个应用进行密码保护。维护Mac上的隐私。 像如果你的某个应用存在隐私数据就可以使用该软件将此应用上锁&#xff0c;这样当你的朋友使用你的 mac 时你就不用担心你的隐私被泄露了&#xff0…

Java中创建线程的五种方式

目录&#xff1a; 前言 1.进程与线程的区别&#xff1f; 2.进程是操作系统进行资源分配的基本单位&#xff0c;而操作系统是以线程为单位进行调度的。 3. Java操作多线程&#xff0c;依赖最核心的类Thread。 4.关于start和run的区别&#xff1f; 5.使用JDK自带的工具jcon…

ArcGIS基础实验操作100例--实验7分割多部分要素

本实验专栏来自于汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 基础编辑篇--实验7 分割多部分要素 目录 一、实验背景 二、实验数据 &#xff08;1&#xff09;查看多…

第05讲:Redis主从复制

一、关于主从复制 1.1、什么是主从复制 主机数据更新后根据配置和策略&#xff0c; 自动同步到备机的master/slaver机制&#xff0c;Master以写为主&#xff0c;Slave以读为主 1.2、主从复制的作用 读写分离&#xff0c;性能扩展容灾快速恢复 二、一主多从的实验 2.1、原…

GitHub+HEXO博客设置主题

文章目录安装主题修改站点配置文件修改主题配置文件部署到github补充由于之前使用HexoGitHub搭建了个人博客用的是最原始的主题&#xff0c;丑的一批QAQ 用一下Github上面Star最高Next主题look look&#xff01; 上节博客&#xff1a;HexoGitHub搭建个人博客 主题选取网站&a…

CnOpenData中国工业企业基本信息扩展数据

一、数据简介 拉动中国经济的三个产业中&#xff0c;工业企业占有特殊的地位&#xff0c;是推动国内经济发展的重要产业。工业是最主要的物质生产部门&#xff0c;为居民生活、各行业的经济活动提供物质产品&#xff0c;这一重要作用是其他任何产业部门都无法替代的。工业企业为…

拆串后结构化,其中按行对齐

【问题】 I have a bit weired scenario where i need to fetch data i have following three products product1 product2 product3and each product has different ids(e.g. p1345,p3453,p2345) and then each froduct have different options which are having different…

算法题刷累了就来试试游戏吧----2048小游戏----C语言实现

目录 1. 代码前的准备 2. 游戏思路及代码分析 2.1 game.h 代码分析 2.2 test.cpp代码分析 3. 完整代码 3.1 game.h 3.2 game.cpp 3.3 test.cpp 嘿嘿嘿&#xff0c;写游戏还是挺高兴的撒&#xff0c;如果你还不知道2048这个小游戏的规则&#xff0c;那么快去试试吧。不然…

项目管理误区:项目不确定性≠项目风险

项目失败的原因千千万万&#xff0c;罪魁祸首肯定跟“它”脱不了关系&#xff01; 前段时间&#xff0c;偶然看到一个求助贴引发了各路网友&#xff08;项目经理&#xff09;的热议。求助人的问题是&#xff1a;“如何管理项目中的不确定性&#xff1f;” 下面的回复已然偏离答…

Linux系统运行时参数命令--文件IO性能监控

目录 4 文件IO性能监控 4.1 I/O 的两种方式(缓存 I/O 和直接 I/O) 1 缓存 I/O 2 直接 I/O 4.2 监控磁盘I/O的命令 1 iostat IO状态 2 swapon查看分区使用情况 3 df硬盘使用情况 4 du目录文件大小 4.3 文件IO写入频繁案例分析 C/CLinux服务器开发/后台架构师【零声教育…

IU8689+IU5706 单声道100W/立体声60W同步升压+功放IC大功率拉杆音箱应用组合方案

引言 目前中大功率拉杆音箱主要采用12V铅酸电池为供电电源&#xff0c;在电源直供的时候&#xff0c;一般的功放芯片输出功率在20W左右&#xff08;喇叭为4欧、THD10%&#xff09;。超过50W的功率现阶段市场上主要采用升压芯片TPA3116的组合解决方案。 随着竞争的加剧&#x…

如何进行数据可视化图表设计?

如何进行数据可视化图表设计&#xff1f; 对数据人来说&#xff0c;数据可视化是分析理解数据&#xff0c;并最终呈现数据的必修课。本文从以下几个点来说明&#xff0c;如何进行数据可视化图表设计。1、数据背后的故事2、充分理解数据3、多种图表类型设计指南 1数据背后的故…

安克创新能否锚定全球家用储能市场 隆起新的增长极?

提到能源储存&#xff0c;似乎应该是涉及一个国家或者地区的宏大概念。但事实上&#xff0c;储能正在走向家用领域。 近年来&#xff0c;全球能源价格持续高涨&#xff0c;但家用储能的成本却随着锂电等新能源技术的发展在逐渐下降&#xff0c;经济性开始凸显。家用储能在海外…

用HTML制作独一无二的2022回忆旋转相册

目录 前言 效果展示 流程 前言 元旦即将来临&#xff0c;展望2022&#xff0c;我们可以制作一个自己的2022回忆的旋转相册&#xff0c;通过下面的方法来学习吧 效果展示 制作好后&#xff0c;十张相册会在下面旋转&#xff0c;可以改为自己想要放的照片和音乐&#xff0c;制…

史上最简单的推箱子(AS3.0版)

我最熟悉的语言是 AS3.0&#xff0c;现在主要用C&#xff0c;还想学学Python&#xff0c;因此&#xff0c;最近一段时间先用AS3.0实现了最简版的推箱子、贪吃蛇和俄罗斯方块&#xff0c;然后换Python实现&#xff0c;算是熟悉了一下Python的基本用法&#xff0c;最后用C实现&a…

微信小程序 Spdier - OfferShow 反编译逆向(一)

微信小程序 Spdier - OfferShow 反编译逆向&#xff08;一&#xff09; 文章目录微信小程序 Spdier - OfferShow 反编译逆向&#xff08;一&#xff09;前言一、任务说明1.尝试反编译分析出js_code参数的生成方式&#xff0c;用来获取token2.将小程序搜索出来的数据保存至本地e…

【Java开发】Spring Cloud 01 :微服务前提精要

算是新开了一个 Spring Cloud 的坑&#xff0c;本文来源于姚半仙的《Spring Cloud 微服务项目实战》课程&#xff0c;大部分文字内容基于该课程&#xff0c;我的工作可能就是梳理归纳和拓展&#xff0c;希望尽快搞懂相对来说较为简单的 Spring Cloud Alibaba 微服务框架&#x…

决策树及分类原理与划分依据:信息熵、信息增益、信息增益率、基尼值和基尼指数

一、决策树及分类原理 决策树&#xff1a;是一种树形结构&#xff0c;其中每个内部节点表示一个属性上的判断&#xff0c;每个分支代表一个判断结果的输出&#xff0c;最后每个叶节点代表一种分类结果&#xff0c;本质是一颗由多个判断节点组成的树 熵(Entropy) &#xff1a;…

PostgreSQL数据库查询执行——SeqScan节点执行

SeqScan节点代码处于src/backend/executor/nodeSeqscan.c文件中&#xff0c;包含了4个重要函数&#xff1a;ExecInitSeqScan、ExecSeqScan、ExecReScanSeqScan和 ExecEndSeqScan。 ExecInitSeqScan src/backend/executor/nodeSeqscan.c文件中的ExecInitSeqScan函数&#xff0c…

数智化转型给企业带来了什么?

数智化转型的核心价值之一在于对企业创智型活动的赋能&#xff0c;从用户需求趋势预测到创意快速验证&#xff0c;数智化应用具有巨大的价值前景。 因此&#xff0c;尽管目前数智化研发还存在着一定程度的复杂性高、不确定性大等问题&#xff0c;但因数智化研发所具有极大的潜…