【数据结构&C语言】排序大汇总

排序汇总

一、常见排序算法

二、常见排序的实现

插入排序

思想:

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

1、直接插入排序

这里以升序为列子,从前往后依次找小于第一个数组元素的数值,当条件成立时,则插入前面。条件不成立时,依次往后寻找,直到找到或超出数组小标为止。

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//插入排序
void insertion_sort(int* arr, int n)
{
	for (int 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;
	}
}

int main()
{
	int arr[] = { 9,6,3,7,4,0,7,4,2,6,1 };
	int len = sizeof(arr) / sizeof(int);

	insertion_sort(arr, len);
	PrintSort(arr, len);

	return 0;
}

直接排序的特性总结

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)

2、希尔排序

(1)希尔排序的概思想

思想:先选定一个整数(gap = n / 3 + 1),把待排序的元素分成各组,并分别进行插入排序,然后gap = gap / 3 + 1得到下一个整数,再分成各组进行插入排序,当gap = 1时,就相当于插入排序,然后就退出循环

希尔排序时再直接插入排序算法的基础上进行改进而来的,总和来说它的效率高于直接插入排序算法。

希尔排序的特性总结

  1. 希尔排序是对直接排序的优化
  2. gap > 1时都是预排序,目的时让数组更接近于有序。当gap = 1时,数组已经接近有序,可以达到优化的效果。
  3. 希尔排序的方法有两种,第一种是一组一组走,第二种是多组并走
(2)gap组实现
C 复制代码
int gap = n;
while (gap > 1)
{
	//分成三组,排序的效率最大
	gap = gap / 3 + 1;
}
(3)插入排序

这里相对于直接插入排序,只是把-1的部分换成了-gap

C 复制代码
int gap = 3
for (int i = 0; i < n - gap; i++)
	{
		int end = i;
		int tmp = arr[end + gap];
		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + gap] = arr[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		arr[end + gap] = tmp;
	}
(4)合并代码实现:
C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//插入排序
void insertion_sort(int* arr, int n)
{
	for (int 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;
	}
}

//希尔排序(多组并行)
void Shell_sort01(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;

		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (tmp < arr[end])
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

//希尔排序(一组一组排序)
void Shell_sort02(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;

		for (int i = 0; i < gap; i++)
		{
			for (int j = i; j < n - gap; j += gap)
			{
				int end = j;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (tmp < arr[end])
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				arr[end + gap] = tmp;
			}
		}
	}
}

int main()
{
	int arr[] = { 9,6,3,7,4,0,7,4,2,6,1 };
	int len = sizeof(arr) / sizeof(int);

	Shell_sort01(arr, len);
	PrintSort(arr, len);

	Shell_sort02(arr, len);
	PrintSort(arr, len);

	return 0;
}

希尔排序有两种写法,并且两种写法的时间复杂度一致,运行上并无区别。

选择排序

  1. 在元素集合array[i] -- array[n - 1]中选择最大/最小的元素
  2. 若它不是这组元素中的最后一个元素,则将它与这组元素中的最后一个元素交换
  3. 重复上述步骤,直到集合剩余一个元素
C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void Swap(int* x, int* y)
{
	int* tmp = *x;
	*x = *y;
	*y = tmp;
}

void selection_sort(int* arr, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i < end; i++)
		{
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
		}
		Swap(&arr[begin], &arr[mini]);
		//问题
		if (begin == maxi)
			maxi = mini;
		Swap(&arr[end], &arr[maxi]);
		begin++;
		end--;
	}
}

int main()
{
	int arr[] = { 9,4,8,5,1,0,5,1,4 };
	int len = sizeof(arr) / sizeof(int);

	selection_sort(arr, len);
	PrintSort(arr, len);

	return 0;
}

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

  1. 直接选择排序效率很低,实际中不常用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)

1、堆排序

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

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
  • 升序:建大堆
  • 降序:建小堆
  1. 利用堆删除思想来进行排序

需要包含实现堆的文件、在test文件中这样写:

C 复制代码
// 堆排序    O(N*logN)
// 冒泡排序  O(N^2)
void HeapSort(int* a, int n)
{
	// 降序,建小堆
	// 升序,建大堆
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	for (int i = (n-1-1)/2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

void TestHeap2()
{
	int a[] = { 4,2,8,1,5,6,9,7,2,7,9};
	HeapSort(a, sizeof(a) / sizeof(int));
}

int main()
{
	TestHeap2();
	return 0;
}

交换排序

1、冒泡排序

因为冒泡排序很好理解,它的教学意义很大。但是在运行效率上不高,所以实践意义不大。

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void bubble_sort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		for (int 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;
			}
		}
	}
}


int main()
{
	int arr[] = { 9,4,8,5,1,0,5,1,4 };
	int len = sizeof(arr) / sizeof(int);

	bubble_sort(arr, len);
	PrintSort(arr, len);

	return 0;
}

冒泡排序的特性总结

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

2、快速排序

(1)算法步骤
    1. 终止条件 :首先检查left(左边界)是否大于等于right(右边界),如果是,则当前子数组的长度小于或等于1,不需要排序,直接返回
  1. 选择基准元素:从列表中选择一个元素作为基准(一般是选第一个元素)。
  2. 分区:小于基准元素的元素排在基准元素左侧,大于的则排右侧
  3. 递归排序:对基准元素的左侧和右侧的子列表分别进行递归
  4. 递归结束后整个列表已有序
(2)实例(无优化版)
C 复制代码
void quick_sort(int* arr, int left, int right)
{
	if (left >= right)
		return;
		
		int keyi = left;
		int begin = left, end = right;
		while (begin < end)
		{
			//左边做key,先让右边先走,再让左边走(可以保持key比停下的位置的元素大)
			//右边做key,先让左边先走,再让右边走(可以保持key比停下的位置的元素小)
			//结论:一边做key,让另一边先走
			while (begin < end && arr[end] >= arr[keyi])
			{
				end--;
			}

			while (begin < end && arr[begin] <= arr[keyi])
			{
				begin++;
			}
			Swap(&arr[begin], &arr[end]);
		}
		Swap(&arr[keyi], &arr[begin]);
		keyi = begin;

		quick_sort(arr, left, keyi - 1);
		quick_sort(arr, keyi + 1, right);
	}
	
}
(3)快排的优化
(3.1)三数取中

避免有序情况下,效率退化

C 复制代码
//三数取中
int GetMidi(int* arr, int left, int right)
{
	int mid = (right + left) / 2;
	if (arr[mid] < arr[left])
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if (arr[left] < arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if (arr[right] < arr[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}
C 复制代码
//三数取中
int midi = GetMidi(arr, left, right);
Swap(&arr[left], &arr[midi]);
(3.2)小区间优化

小区间优化,不再递归分割排序,减少递归的次数

C 复制代码
if ((right - left + 1) < 10)
{
	selection_sort(arr + left, right - left + 1);
}
(4)hoare版本
C 复制代码
void Swap(int* x, int* y)
{
	int* tmp = *x;
	*x = *y;
	*y = tmp;
}

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

//避免有序情况下,效率退化
//三数取中
void quick_sort(int* arr, int left, int right)
{
	if (left >= right)
		return;

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

		int keyi = left;
		int begin = left, end = right;
		while (begin < end)
		{
			//左边做key,先让右边先走,再让左边走(可以保持key比停下的位置的元素大)
			//右边做key,先让左边先走,再让右边走(可以保持key比停下的位置的元素小)
			//结论:一边做key,让另一边先走
			while (begin < end && arr[end] >= arr[keyi])
			{
				end--;
			}

			while (begin < end && arr[begin] <= arr[keyi])
			{
				begin++;
			}
			Swap(&arr[begin], &arr[end]);
		}
		Swap(&arr[keyi], &arr[begin]);
		keyi = begin;

		quick_sort(arr, left, keyi - 1);
		quick_sort(arr, keyi + 1, right);
	}
	
}
  • 时间复杂度:

    • 分解:每次将列表分成两半,需要O(log n)层递归

    • 和并:每层递归需要O(n)的时间来合并子列表

    • 总时间复杂度:O(n*log n)

  • 优点

    • 平均时间复杂度为 O(n log n),在大多数情况下非常高效。

    • 原地排序(in-place)算法,只需 O(log n) 额外空间(递归栈)。

    • 适合内存中的大规模数据排序,常用于通用排序函数(如 std::sort)。

  • 缺点

    • 不稳定排序,相同元素的相对顺序可能改变。

    • 最坏时间复杂度为 O(n²)(如数组已近乎有序时),但可通过随机化或"三数取中"优化。

    • 小规模数据性能较差,不如插入排序等简单算法。

(5)挖坑法
  1. 使用两个指针beginend,分别初始化leftright。两指针从数组两端向中间扫描
  2. end指针从右向左移动,寻找第一个小于基准值tmp的元素。找到后,将该元素的值放到begin指针所指的"坑"中
  3. 然后,begin指针从左向右移动,寻找第一个大于基准值tmp的元素。找到后,将该元素的值放到end指针所指的"坑"中(注意,此时end已经向左移动过,所以不会覆盖之前放置的值)
  4. 直到beginend相遇或交错。此时,begin所在的位置就是基准值tmp应该放置的位置
C 复制代码
// 快速排序挖坑法
void PartSort2(int* a, int left, int right)
{
	if (left >= right)//只有一个元素或不存在,停止递归
		return;

	int keyi = left;
	int key = a[left];//先将基准值存下来

	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && key <= a[end])//右找大,找到停下来,填到左边的坑
		{
			end--;
		}
		if (begin < end)
			a[begin] = a[end];

		while (begin < end && key >= a[begin])//左找小,找到停下来,填到右边的坑
		{
			begin++;
		}
		if (begin < end)
			a[end] = a[begin];
	}
	a[begin] = key;//相遇之后,将保存的值填到相遇位置
	keyi = begin;

	PartSort2(a, left, keyi - 1);
	PartSort2(a, keyi + 1, right);
}
(6)前后指针法
  • prev(前指针)初始化为left,它用于记录小于基准值的最后一个元素的位置。
  • cur(后指针)初始化为left + 1,它用于遍历数组中的元素,寻找小于基准值的元素。
  • 使用while循环,当cur小于等于right时执行循环体。
  • 在循环体内,如果a[cur]小于基准值a[keyi],并且prev没有指向当前cur的位置(即prev++ != cur,这是为了避免自身与自身的无用交换),则将a[prev]a[cur]的值交换。然后,prev向前移动一步(prev++在条件判断之后执行,确保只有在需要时才进行交换和移动)。
  • cur始终向前移动,直到遍历完整个子数组。
  • 将基准值a[keyi]a[prev]交换,这样基准值就被放到了它最终应该在的位置。
  • 由于基准值已经与a[prev]交换,所以将keyi更新为prev,以反映基准值的新位置。
C 复制代码
// 快速排序前后指针法
void PartSort3(int* a, int left, int right)
{
	if (left >= right)//只有一个元素或不存在,停止递归
		return;

	int keyi = left;

	int prev = left;//前指针
	int cur = prev + 1;//后指针
	while (cur <= right)//cur越界结束循环
	{
		//cur一直向后走,遇到小的值就停下,让prev向前走一步并交换
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

			cur++;
	}
	Swap(&a[keyi], &a[prev]);//出循环,将prev停留位置与keyi位置进行交换
	keyi = prev;

	PartSort3(a, left, keyi - 1);
	PartSort3(a, keyi + 1, right);
}
(7)非递归法

当递归的深度过深时,有可能会导致栈的溢出(纵向),这时可以使用非递归法,在堆里面进行排序(横向)。跟递归的思路很像,但逻辑不同,可以当成递归来写。

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"

void PrintSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int PartSort(int* arr, int left, int right)
{
	if (left >= right)
		return;

	int keyi = left;
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}

		cur++;
	}
	Swap(&arr[prev], &arr[keyi]);

	return prev;
}

void QuickSortNonR(int* arr, 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 = PartSort(arr, begin, end);

		//[begin, keyi - 1] keyi [keyi + 1, end]
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}

		if (keyi - 1 > begin)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
}

3、合并排序

(1)合并排序

合并排序是一种后序的思路,先排完序,再去克隆到原数组。先申请数组空间,取中间值,分两边递归,再排序。

C 复制代码
void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
		return;

	//取中间值,分两边递归
	int mid = (end + begin) / 2;
	//递归排序
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	//设立新变量,更易于比较
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;

	//设置下标,归并
	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++];
	}

	//将数据拷贝给a数组中
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* a, int n)
{
	//思路:将获取的元素放进新的数组中,然后拷贝到a数组中
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	//设立子函数,将数据放进里面递归排序
	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
}

归并排序特性总结:

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(n)
(2)合并排序(非递归)

合并排序的非递归可以使用循环来实现,本质上也是一种两路递归的思想

C 复制代码
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			// [begin1, end1][begin2, end2]
			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, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}

计数排序

  • 统计相同元素出现次数
  • 根据统计的结果将序列回收到原来的序列中
C 复制代码
void CountSort(int* arr, int n)
{
	//循环遍历找最值
	int min = arr[0], max = arr[0];
	for (int i = 0; i < n; i++)
	{
		if (arr[i] < min)
		{
			min = arr[i];
		}

		if (arr[i] > max)
		{
			max = arr[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[arr[i] - min]++; 
	}

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

	free(count);
	count = NULL;
}

计数排序的特性:

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

时间复杂度:O(N+range)

空间复杂度:O(range)

稳定性:稳定

排序算法复杂度及稳定性

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性
冒泡排序 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) O(n^2) O(1) 稳定
希尔排序 O(nlogn)~O(n^2) O(n^1.3) O(n^2) O(1) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn)~O(n) 不稳定

稳定性验证案例

直接选择排序:5 8 5 2 9

希尔排序:5 8 2 5 9

堆排序:2 2 2 2

快速排序:5 3 3 4 3 8 9 1 0 1 1

相关推荐
间彧1 小时前
Docker 数据持久化完全指南:四种挂载方式详解与实战
后端
IT_陈寒1 小时前
SpringBoot 3.2 性能优化全攻略:7个让你的应用提速50%的关键技巧
前端·人工智能·后端
做怪小疯子2 小时前
LeetCode 热题 100——普通数组——除自身以外数组的乘积
数据结构·算法·leetcode
明洞日记2 小时前
【数据结构手册001】从零构建程序世界的基石
数据结构·c++
雨落在了我的手上2 小时前
C语言入门 (二十):指针(6)
c语言
❀͜͡傀儡师2 小时前
springboot集成mqtt服务,自主下发
java·spring boot·后端·mqtt·netty
火车叼位2 小时前
兼容命令行与 Android Studio 的 JDK 策略:从踩坑到方案
后端
IMPYLH2 小时前
Lua 的 pairs 函数
开发语言·笔记·后端·junit·单元测试·lua
用户345848285052 小时前
什么是 Java 内存模型(JMM)?
后端