【数据结构】排序算法(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序)

小编主页详情<-请点击
小编gitee代码仓库<-请点击


本文主要介绍了排序算法(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序),内容全由作者原创(无AI),同时深度解析了每个排序算法的具体实现和拓展,并带有配图帮助博友们更好的理解,点个关注不迷路,下面进入正文~~


目录

前言:

1.插入排序

1.1直接插入排序

1.2希尔排序

2.选择排序

2.1选择排序

2.2堆排序

3.交换排序

3.1冒泡排序

3.2快速排序

3.2.1hoare版本

3.2.2前后指针版本

3.2.3非递归实现

4.归并排序

4.1递归实现

4.2非递归实现

5.计数排序

结语:


前言:

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

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

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

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

1.插入排序

1.1直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

当插入第i(i>=1)个元素时,前面的a[0],a[1],...,a[i-1]已经排好序,此时用a[i]的排序码与a[i-1],

a[i-2],...的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

下面是直接插入排序代码的实现:

cpp 复制代码
void InsertSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)
	{
		// [0, n-2]是最后一组
		// [0,end]有序 end+1位置的值插入[0,end],保持有序
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高

  2. 时间复杂度:O(N^2)

  3. 空间复杂度:O(1),它是一种稳定的排序算法

  4. 稳定性:稳定

1.2希尔排序

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

简单来说,希尔排序分为两步:1.预排序 2.直接插入排序

其中预排序其实也是直接插入排序,预排序可以让数组变得更有序,那为什么预排序可以提高排序的效率呢?

那么为什么要gap = gap / 3 + 1呢?为了保证gap最后是等于1的。

整体的逻辑是gap = gap / 3 + 1,gap > 1时是预排序,gap == 1时是插入排序。

从i=0开始遍历到 i < n - gap,每个比较的数据间隔都是gap,直到end小于0停止比较。

cpp 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		// +1保证最后一个gap一定是1
		// gap > 1时是预排序
		// gap == 1时是插入排序
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
	
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。

  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap ==1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,通常按O(n^1.3)估算。

  4. 稳定性:不稳定

2.选择排序

2.1选择排序

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

在元素集合a[i]--a[n-1]中选择关键码最大(小)的数据元素。若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换,在剩余的a[i]--a[n-2]

(a[i+1]--a[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

这里我们采用优化后的代码,在遍历一次数组后,同时找到数组中的最大值和最小值

下面是优化后的完整代码:

cpp 复制代码
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[mini] > a[i])
			{
				mini = i;
			}
			if (a[maxi] < a[i])
			{
				maxi = i;	
			}
		}
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[mini], &a[begin]);
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}

这里需要注意,当maxi == begin时,如果交换了mini和begin的值,那么原来本是最大值位置的begin变成的最小值,而交换后的mini反而成为的最大值的位置。因此若maxi == begin,我们要将maxi = mini。

直接选择排序的特性总结:

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

  2. 时间复杂度:O(N^2)

  3. 空间复杂度:O(1)

  4. 稳定性:不稳定

2.2堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

详细推导过程请查看:【数据结构】二叉树-堆(树的概念、二叉树的概念、顺序结构的结构及实现、堆的实现、堆排序、TopK问题)

下面是堆排序的完整代码:

cpp 复制代码
void AdjustDown(int* a, int n, int parent)
{
	// 先假设左孩子小
	int child = parent * 2 + 1;

	while (child < n)  // 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)
{
	// 向下调整建堆 O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

堆排序特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。

  2. 时间复杂度:O(N*logN)

  3. 空间复杂度:O(1)

  4. 稳定性:不稳定

3.交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.1冒泡排序

冒泡排序比较简单,这里不做过多讨论,下面是冒泡排序的完整代码:

cpp 复制代码
// 适合教学,实践中没啥价值
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int flag = 0;
		for (int j = 0; j < n - i - 1; j++)
		{
			
			if (a[j] > a[j + 1])
			{
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
				flag = 1;
			}
			
		}
		if (flag == 0)
		{
			break;
		}
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序

  2. 时间复杂度:O(N^2)

  3. 空间复杂度:O(1)

  4. 稳定性:稳定

3.2快速排序

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

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

  1. hoare版本

  2. 挖坑法(这里不做分析)

  3. 前后指针版本

3.2.1hoare版本

我们先假设最左边的值为keyi,当begin<end时,右边找小,左边找到,然后交换。当begin=end时,他们相遇的位置一定是比keyi小的值,这个时候我们再交换keyi和begin的值。此时keyi左边全部是小于等于keyi的值,keyi右边全部是大于等于keyi的值,区间被划分成->

left, keyi-1\] keyi \[keyi+1, right

如果[left, keyi-1]和[keyi+1, right]都有序了,那么整个数组就有序了。

因此我们可以递归调用QuickSort(a, left, keyi - 1)和QuickSort(a, keyi + 1, right),这样全部数据就都有序了。

下面是完整代码:

cpp 复制代码
// hoare
int PartSort1(int* a, int left, int right)
{
	int begin = left;
	int end = right;
	int keyi = left;

	while (begin < end)
	{
		// 右边找小
		while (begin < end && a[end] >= a[keyi])
		{
			end--;
		}
		// 左边找大
		while (begin < end && a[begin] <= a[keyi])
		{
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[keyi], &a[begin]);
	keyi = begin;
	return keyi;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort1(a, left, right);
	// [left, keyi-1] keyi [keyi+1, right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
	
}

这个代码还可以在做几个优化,例如:

三数取中:如果这个数组本身就是有序的,那么递归调用的深度会很深。为了避免这种情况,我们可以用随机数选keyi或者三数取中,让数组最前面、中间、最后面大小为中间值的数做keyi,及将中间值交换到最左边的位置做keyi。

小区间优化:我们可以假设keyi每次都选到了中间值,那么快速排序的递归调用就类似于完全二叉树,而完全二叉数的最后一层占整个数的一半,倒数第二层又占整棵树的四分之一,数据很少却又又要大量递归调用,很浪费时间。因此,我们可以当数组内小于十个数时就是用插入排序,这样可以大量减小递归调用的次数。

下面时优化后的完整代码:

cpp 复制代码
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		InsertSort(a+left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int keyi = left;
		int begin = left, end = right;
		while (begin < end)
		{
			// 右边找小
			while (begin < end && a[end] >= a[keyi])
			{
				--end;
			}

			// 左边找大
			while (begin < end && a[begin] <= a[keyi])
			{
				++begin;
			}

			Swap(&a[begin], &a[end]);
		}

		Swap(&a[keyi], &a[begin]);
		keyi = begin;
		// [left, keyi-1] keyi [keyi+1, right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  2. 时间复杂度:O(N*logN)

  3. 空间复杂度:O(logN)

  4. 稳定性:不稳定

3.2.2前后指针版本

前后指针版本相对比较简单。初始时,prev指向序列开头,cur指向prev指针的下一个位置。然后判断cur是否小于keyi,如果小于,就++prev,再交换prev和cur的值,最后再cur++;如果大于,就直接cur++。直到cur>right,及cur越界时跳出循环。此时再交换prev和keyi的值,这样keyi左边全部是小于等于keyi的值,keyi右边全部是大于等于keyi的值

详细代码如下:

cpp 复制代码
// 前后指针
int PartSort2(int* a, int left, int right)
{
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	int prev = keyi;
	int cur = keyi + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort1(a, left, right);
	// [left, keyi-1] keyi [keyi+1, right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
	
}

3.2.3非递归实现

非递归的实现我们可以借助来实现。核心思路就是把需要排序的区间入栈,要注意这里入栈要先入right再入left,因为我们我们才能先取到left,后面的入栈也是同理。

详细代码如下:

cpp 复制代码
#include "Stack.h"

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);
	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);
		int keyi = PartSort1(a, begin, end);
		// [begin, keyi-1] keyi [keyi+1, end]
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}

4.归并排序

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

4.1递归实现

核心思路:如果左区间和右区间都有序,那么就可以合并。我们先递归调用_MergeSort(a, tmp, begin1, end1)和 _MergeSort(a, tmp, begin2, end2)使左区间和右区间都有序,接着开始合并。我们将合并后的新数组先存储在tmp数组中,最后再用memcpy将tmp数组拷贝到原数组中。

需要注意的是区间不能是[begin, mid-1][mid, end],这样会导致死循环的问题。区间应是

begin, mid\]\[mid+1, end

还需要注意的是memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int)),拷贝的起始位置要加入begin。

下面是归并排序的完整代码:

cpp 复制代码
_MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	_MergeSort(a, tmp, begin1, end1);
	_MergeSort(a, tmp, begin2, end2);
	int i = begin;
	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)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);
	free(tmp);
	tmp = NULL;
}

归并排序特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序更多解决在磁盘中的外排序问题。

  2. 时间复杂度:O(N*logN)

  3. 空间复杂度:O(N)

  4. 稳定性:稳定

4.2非递归实现

核心思路:先11归并,再22归并,再44归并,直到每组归并数据的数据个数小于n。

需要注意的是这样写会有越界的风险

如果越界的情况是第二种,说明其实数据就只有1组,不需要归并,直接break。

如果越界的情况是第一种,我们就要修正end2的值为n-1。

归并的步骤和递归相同

下面是非递归实现归并排序的完整代码:

cpp 复制代码
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}

	int gap = 1;
	// gap每组归并数据的数据个数
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i+ gap -1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 第二组都越界不存在,这一组就不需要归并
			if (begin2 >= n)//要写等号
			{
				break;
			}

			// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			int j = i;
			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, (end2 - begin1 + 1) * sizeof(int));
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

5.计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

操作步骤:

  1. 统计相同元素出现次数

  2. 根据统计的结果将序列回收到原来的序列中

下面为计数排序的详细代码:

cpp 复制代码
// 时间复杂度:O(N+range)
// 只适合整数/适合范围集中
// 空间范围度:O(range)
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*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}

	free(count);
}

计数排序的特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

  2. 时间复杂度:O(MAX(N,范围))

  3. 空间复杂度:O(范围)

  4. 稳定性:稳定

结语:

这篇文章全文由作者手写,图片由画图软件所制,无AI制作,希望各位博友能有所收获

欢迎各位博友的讨论,觉得不错的小伙伴,别忘了点赞关注哦~

相关推荐
MediaTea2 小时前
ML:逻辑回归的基本原理与实现
人工智能·算法·机器学习·数据挖掘·逻辑回归
邪修king2 小时前
UE5:C++ 实现 游戏逻辑 ↔ UI 双向联动
c++·游戏·ue5
辛苦才能2 小时前
数据结构--排序--插入排序(C语言,重点排序面试和比赛都会考察)
c语言·数据结构·面试
怪兽软家2 小时前
AutoCAD 2027安装教程及下载
windows·经验分享·生活
SuperByteMaster10 小时前
keil 工程 .gitignore配置文件
c语言
超级码力66610 小时前
【Latex文件架构】Latex文件架构模板
算法·数学建模·信息可视化
穿条秋裤到处跑10 小时前
每日一道leetcode(2026.04.29):二维网格图中探测环
算法·leetcode·职场和发展
Merlos_wind11 小时前
HashMap详解
算法·哈希算法·散列表
汉克老师11 小时前
GESP2025年3月认证C++五级( 第三部分编程题(1、平均分配))
c++·算法·贪心算法·排序·gesp5级·gesp五级