【数据结构】排序

🐇
🔥博客主页: 云曦
📋系列专栏: 数据结构

💨吾生也有涯,而知也无涯 💛 感谢大家👍点赞 😋关注📝评论


文章目录

  • 前言
  • 一、排序的概念及运用
  • 二、常见排序算法的实现
    • [📙2.1 插入排序](#📙2.1 插入排序)
      • [📓2.1.1 直接插入排序](#📓2.1.1 直接插入排序)
      • [2.1.2 📓希尔排序](#2.1.2 📓希尔排序)
    • [📙2.2 选择排序](#📙2.2 选择排序)
      • [📓2.2.1 直接选择排序](#📓2.2.1 直接选择排序)
      • [📓2.2.2 堆排序](#📓2.2.2 堆排序)
    • [📙2.3 交换排序](#📙2.3 交换排序)
      • [📓2.3.1 冒泡排序](#📓2.3.1 冒泡排序)
      • [📓2.3.2 快速排序](#📓2.3.2 快速排序)
        • [📄2.3.2.1 hoare版本](#📄2.3.2.1 hoare版本)
        • [📄2.3.2.2 挖坑法](#📄2.3.2.2 挖坑法)
        • [📄2.3.2.3 前后指针版本](#📄2.3.2.3 前后指针版本)
        • [📄2.3.2.4 快速排序(非递归实现)](#📄2.3.2.4 快速排序(非递归实现))
    • [📙2.4 归并排序](#📙2.4 归并排序)
      • [📓2.4.1 归并排序](#📓2.4.1 归并排序)
      • [📓2.4.2 归并排序(非递归实现)](#📓2.4.2 归并排序(非递归实现))
    • [📙2.5 非比较排序](#📙2.5 非比较排序)
      • [📓2.5.1 计数排序](#📓2.5.1 计数排序)
  • [三、 排序算法的复杂度和稳定性](#三、 排序算法的复杂度和稳定性)
  • 四、本篇章的代码
    • [📙4.1 **<font color=Orange>Sort.h**](#📙4.1 Sort.h)
    • [📙4.2 **<font color=Orange>Sort.c**](#📙4.2 Sort.c)
    • [📙4.3 **<font color=Orange>Test.c**](#📙4.3 Test.c)

前言

在上期我们讲解完了普通二叉树,这期又是一期新的知识点(排序)。

一、排序的概念及运用

  • 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
  • 排序的运用:
    排序在生活中运用的场景很多,例如:我们想知道一部手机的价格、性能、品牌等信息的排名时,排序的作用就发挥出来了

二、常见排序算法的实现

📙2.1 插入排序

📓2.1.1 直接插入排序

  • 插入排序是一种很简单的排序法,其思想就是:
  • 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
  • 其次排序与之前的数据结构不同,我们实现的时候一般先实现单趟排序,再去实现多趟排序。
  • 单趟的思路:
  1. 定义临时变量(tmp)来保存要插入的数据
  2. 然后与前面的数比较,大于tmp就往后挪
  3. 小于tmp,就把tmp赋值给这个元素的后一位
  • 单趟实现:
c 复制代码
void InsetionSort(int* arr, int n)
{
	int end = i;
	int tmp = arr[end + 1];
	while (end >= 0)
	{

		if (tmp < arr[end])
		{
			arr[end + 1] = arr[end];
		}
		else
		{
			break;
		}

		end--;
	}
	arr[end + 1] = tmp;
	
}
  • 多趟思路:
  • 在套一层循环
  • 需要注意的是:end最后落到的位置是n-2的位置,n-1的话会把下标为n的数据插入进去导致越界访问
c 复制代码
void InsetionSort(int* arr, int n)
{
	int i = 0;
	//end最后停留的位置是n-2的位置
	//所以i要小于n-1
	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];
			}
			else
			{
				break;
			}

			end--;
		}
		arr[end + 1] = tmp;
	}
	
}
  • 复杂度:
  1. 时间复杂度:
    最坏情况(要升序,而数组是逆序):O(N^2)
    最好情况(要升序,而数组是顺序有序):O(N)
  2. 空间复杂度:O(1)

2.1.2 📓希尔排序

  • 希尔排序是在直接插入排序的基础上变形的一种排序。
  • 希尔排序分为:
  1. 预排序
    预排序是分组排序,间隔gap的为一组,
  2. 直接插入排序
  • 预排序的意义:大的数更快的到后面,小的数更快的到前面,
    gap越大跳得越快,越不接近有序
    gap越小跳得越慢,越接近有序,当gap==1时,就是直接插入排序
    但gap == 1时,就是直接插入排序了
  • 单组gap的单次执行思路:
  1. 首先假设gap==3,end从下标0开始,tmp记录end+gapx位置的元素
  2. 然后判断tmp记录的元素是否小于end位置的元素,小于就把end位置的元素往后挪到end+gap的位置上,大于就break跳出循环,叫tmp赋值给end+gap的位置上
  • 这个思路就类似直接插入排序,就是把加1的位置变成了加gap
c 复制代码
void ShellSort(int* arr, int n)
{
	int gap = 3;
	int end = 0;
	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;
	
}
  • 单组gap的执行思路:

上面的过程,只是执行了一组内一次的过程,要一组都执行完的话,还要加一层循环

  1. 定义i从0开始小于n-gap,每次循环上来减等gap
c 复制代码
void ShellSort(int* arr, int n)
{
	int gap = 3;
	int i = 0;
	for (i = 0; i < n - gap; i += gap)
	{
		int end = 0;
		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;
	}

}
  • gap组的执行思路:
  • gap组执行,简单嘛,再加一层循环
    循环从0开始,小于n结束
c 复制代码
void ShellSort(int* arr, int n)
{
	int gap = 3;
	int j = 0;
	for (j = 0; j < n; j++)
	{
		int i = j;
		for (i = j; i < n - gap; i += gap)
		{
			int end = 0;
			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;
		}
	}
	
}
  • 上面这部分逻辑有的地方是这样写的:
  1. 多组并排,也就是多组一起排,end最后落的位置是n-gap的位置
    这种写法的效率和上面的效率是一样的,只是思路不一样而已
c 复制代码
void ShellSort(int* arr, int n)
{
	int gap = 3;
	int i = 0;
	for (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;
	
}
  • gap的取值
  • 那么问题来了,gap应该取多少合适呢?
    答案是:把n赋值给gap,然后gap = gap / 2,除2的数可以保证最后一次gap永远是1,就是直接插入排序。
    有人决定gap/2比较慢,就改成了gap/3,也是可以的,但gap/3时要注意一点:gap小于3或其他数时,会除出0来,所以要写成gap/3+1
c 复制代码
void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		//gap = gap / 3 + 1;
		int i = 0;
		for (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;
		}

	}
	
}
  • 测试:
c 复制代码
void TestShellSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int size = sizeof(arr) / sizeof(arr[0]);
	InsertSort(arr, size);
	PrintArray(arr, size);
}
  • 复杂度:
  1. gap/2的话,时间复杂度:O(log₂N)
    gap/3+1的话,时间复杂度:O(log₃N)
    也有些地方通过计算得出时间复杂度:O(N^1.3)
  2. 空间复杂度:O(1)

📙2.2 选择排序

📓2.2.1 直接选择排序

  • 直接插入排序是一个很简单的排序
  1. 遍历找出最小和最大值的下标,然后小的和左边的数交换,大的和右边的数交换
  2. 先找出最大值和最小值的下标
c 复制代码
void SelectSort(int* arr, int n)
{
	int mini = begin;
	int maxi = begin;
	int i = 0;
	for (i = begin+1; i < end; i++)
	{
		if (arr[i] > arr[maxi])
		{
			maxi = i;
		}

		if (arr[i] < arr[mini])
		{
			mini = i;
		}
	}

}
  1. 套上循环,左边从0开始,右边从n-1开始,形成左闭右闭
    每次找到最大和最小的值和左右边上交换,然后左加1,右-1
  • 需要注意的是:maxi和begin有可能在一个位置上,begin和mini先交换,有可能会导致maxi位置上的值变成其他的,这时我们要加上一个判断进行修正
c 复制代码
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
    	//[begin,end] 左闭右闭
		int mini = begin;
		int maxi = begin;
		int i = 0;
		for (i = begin+1; i < end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}

			if (arr[i] < arr[mini])
			{
				mini = i;
			}

		}

		Swap(&arr[begin], &arr[mini]);
		//max如果被换走,就修正一下
		if (maxi == begin)
		{
			maxi = mini;
		}

		Swap(&arr[end], &arr[maxi]);
		++begin;
		--end;
	}

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

📓2.2.2 堆排序

堆排序在二叉树的篇章讲过了,就不在论述了,大家可以去此链接查看:
二叉树讲解堆排序的篇章

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

📙2.3 交换排序

📓2.3.1 冒泡排序

  • 冒泡排序就不用说了,相信大家在学习C语言的语法时,就已经学过了
  • 思路也就是两两比较,小于就交换
c 复制代码
void BubbleSort(int* arr, int n)
{
	int j = 0;
	for (j = 0; j < n; j++)
	{
		int flag = 0;
		int i = 1;
		for (i = 1; i < n-j; i++)
		{

			if (arr[i - 1] > arr[i])
			{
				Swap(&arr[i - 1], &arr[i]);
				flag = 1;
			}

		}

		if (flag == 0)
		{
			break;
		}

	}

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

📓2.3.2 快速排序

📄2.3.2.1 hoare版本
  • 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
  • hoare版本的思想:
  • 单趟思想:
    右边先走,找比kay小的值
    右边找到后,左边再走,找比kay大的值,然后交换左右的值
    当L遇到R时,遇见位置的值一定比kay小,交换一下

c 复制代码
int PartSort1(int* arr, int left, int right)
{
	int kay = arr[left];
	while (left < right)
	{
		//找小
		while (arr[right] > kay)
		{
			--right;
		}

		//找大
		while (arr[left] < kay)
		{
			++left;
		}
		
		Swap(&arr[right], &arr[left]);
	}

	Swap(&arr[left], &kay);
	return left;
}
  • 这样单趟就写好了,不过还没写完,与几处地方有坑
  1. 坑1:会死循环
c 复制代码
int PartSort1(int* arr, int left, int right)
{
	int kay = arr[left];
	while (left < right)
	{
		//找小
		while (arr[right] >= kay)
		{
			--right;
		}

		//找大
		while (arr[left] <= kay)
		{
			++left;
		}
		
		Swap(&arr[right], &arr[left]);
	}

	Swap(&arr[left], &kay);
	return left;
}
  1. 坑2:会导致越界访问
c 复制代码
int PartSort1(int* arr, int left, int right)
{
	int kay = left;
	while (left < right)
	{
		//找小
		while (left < right && arr[right] >= kay)
		{
			--right;
		}

		//找大
		while (left < right && arr[left] <= kay)
		{
			++left;
		}
		
		Swap(&arr[right], &arr[left]);
	}

	Swap(&arr[left], &kay);
	return left;
}
  1. 坑3:交换时,没有交换到数组里的值
c 复制代码
int PartSort1(int* arr, int left, int right)
{
	int kayi = left;
	while (left < right)
	{
		//找小
		while (left < right && arr[right] >= arr[kayi])
		{
			--right;
		}

		//找大
		while (left < right && arr[left] <= arr[kayi])
		{
			++left;
		}
		
		Swap(&arr[right], &arr[left]);
	}

	Swap(&arr[left], &arr[kayi]);
	return left;
}
  • 整体思想:
c 复制代码
void QuickSort(int* arr, int regin, int end)
{
	if (regin >= end)
	{
		return;
	}

	int kayi = PartSort1(arr, regin, end);

	//左区间          分割下标  右区间
	//[regin,kayi-1]    kayi   [kayi+1,end]
	QuickSort(arr, regin, kayi - 1);//递归kayi的左区间
	QuickSort(arr, kayi + 1, end);//递归kayi的右区间
}
  • hoare版本的快排就写好了
  • 复杂度:
    时间复杂度:O(N*logN)
    最坏情况(排升序,而数组是升序),时间复杂度O(N^2)
  • 快排有一个小的优化:
  • 三数取中:
    含义是:左边、中间、右边,三个数,取不是最大也不是最小的那个数的下标返回,返回后与left交换位置
  • 有三数取中的优化,快速排序不会出现最坏情况
c 复制代码
int GetMidi(int* arr, int left, int right)
{
	int mid = (left + right) / 2;
	if (arr[left] < arr[mid])
	{
	
		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if (arr[right] > arr[left])//mid是最大值的下标
		{
			return right;
		}
		else
		{
			return left;
		}

	}
	else   //arr[left] > arr[mid]
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if(arr[right] < arr[left]) //mid是最小值的下标
		{
			return right;
		}
		else
		{
			return left;
		}

	}

}
📄2.3.2.2 挖坑法

想来看看挖坑法的动图

  • 挖坑法单趟思想:
  1. kay记录左边数据,坑位也记录在左边
  2. 右边找小,找到后和坑位的位置进行交换,最后坑位更新到右边
  3. 再是左边找小,找到后和坑位的位置进行交换,最后坑位更新到左边
  4. 当相遇时,把kay保存的数据赋值到坑位的位置上,返回坑位(相遇时坑位是kay应该到的位置(排好序的位置上))
c 复制代码
int PartSort2(int* arr, int left, int right)
{
	int midi = GetMidi(arr, left, right);
	Swap(&arr[midi], &arr[left]);

	int kay = arr[left];
	int hole = left;
	while (left < right)
	{
		//找小,找到后与坑位交换,右边形成新的坑
		while (left < right && arr[right] >= kay)
		{
			--right;
		}
		Swap(&arr[right], &arr[hole]);
		hole = right;

		//找大,找到后与坑位交换,左边形成新的坑
		while (left < right && arr[left] <= kay)
		{
			++left;
		}
		Swap(&arr[left], &arr[hole]);
		hole = left;
	}
	arr[hole] = kay;
	return hole;
}
  • 挖坑法整体思想:
    整体思想还是跟hoare版本的整体思想一样,用递归实现
c 复制代码
void QuickSort(int* arr, int regin, int end)
{
	if (regin >= end)
	{
		return;
	}

	int kayi = PartSort2(arr, regin, end);

	//左区间          分割下标  右区间
	//[regin,kayi-1]    kayi   [kayi+1,end]
	QuickSort(arr, regin, kayi - 1);//递归kayi的左区间
	QuickSort(arr, kayi + 1, end);//递归kayi的右区间
}
📄2.3.2.3 前后指针版本

先看前后指针版本的动图:

  • 前后指针版本的单趟思想:
  1. 用kayi记录左边的下标,prev从左边开始,cur从prev+1开始
  2. cur找小,找到就++prev然后交换
  3. 当cur >= right时(循环的结束条件),循环结束,把kayi位置的值和prev位置的值进行交换,然后返回prev
c 复制代码
int PartSort3(int* arr, int left, int right)
{
	int midi = GetMidi(arr, left, right);
	Swap(&arr[midi], &arr[left]);
	
	int prev = left;
	int cur = prev + 1;
	int kayi = left;
	while (cur <= right)
	{
		if (arr[cur] <= arr[kayi] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[prev], &arr[kayi]);
	return prev;
}
  • 前后指针版本的整体思想:
    整体思想也是跟前面两个版本一样的,用递归实现
c 复制代码
void QuickSort(int* arr, int regin, int end)
{
	if (regin >= end)
	{
		return;
	}

	int kayi = PartSort2(arr, regin, end);

	//左区间          分割下标  右区间
	//[regin,kayi-1]    kayi   [kayi+1,end]
	QuickSort(arr, regin, kayi - 1);//递归kayi的左区间
	QuickSort(arr, kayi + 1, end);//递归kayi的右区间
}
  • 整体思想的一个优化
  • 大家都知道递归深度太深会导致栈溢出,所以当递归到10个数以内的区间后,可以用插入排序进行排序,这样就减少了递归的次数
c 复制代码
void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	//小区间优化,不在递归分割排序,降低了递归次数
	if (begin + end + 1 > 10)
	{
		int kayi = PartSort3(arr, begin, end);
		//[begin, kayi-1] kayi [kayi+1, end]
		QuickSort(arr, begin, kayi - 1);
		QuickSort(arr, kayi + 1, end);
	}
	else
	{
		InsertSort(arr, begin + end + 1);
	}

}
📄2.3.2.4 快速排序(非递归实现)

用递归排序正常的数据是可以排好的,但在一些特定的情况下,递归深度太深导致栈溢出,那么就需要改成非递归了

  • 非递归的快排思想:
  1. 非递归的快排,我们要借用栈的数据结构来实现
  2. 利用栈的先进后出原则,先入开始的区间,走单趟排序,接收kayi后,右区间先入栈左区间在入栈,出栈时,排序左区间,再排右区间
c 复制代码
void QuickSortNonR(int* arr, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);
		int kayi = PartSort3(arr, left, right);
		if (kayi + 1 <= right)
		{
			STPush(&st, right);
			STPush(&st, kayi + 1);
		}

		if (left <= kayi - 1)
		{
			STPush(&st, kayi - 1);
			STPush(&st, left);
		}

	}

	STDestroy(&st);
  • 总结:
  1. 快速排序的时间复杂度:O(N*logN)
  2. 快速排序的空间复杂度:O(logN)

📙2.4 归并排序

📓2.4.1 归并排序

先看图

  • 归并排序就是分割出左右区间有序就归并,左右区间没有序就在分割左右区间,分割到只有一个数时那么一个数就是有序的把单个数归并到临时开辟的数组里,再拷贝回原数组,
  • 1个和1个的归并有序后再进行2个和2个的归并,然后再进行4个和4个的归并
  • 思路为:
  1. 开辟一个和原数组一样大的临时空间
  2. 因为要用递归实现,所以要写一个子函数进行递归式归并
  • 归并排序
c 复制代码
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(arr, tmp, 0, n-1);
	free(tmp);
	tmp = NULL;
}
  • 子函数的实现
c 复制代码
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int mid = (begin + end) / 2;
	//[begin, mid] [mid+1, end] 区间的分割
	_MergeSort(arr, tmp, begin, mid);//递归左区间
	_MergeSort(arr, tmp, mid + 1, end);//递归右区间
	//[begin1, end1] [begin2, end2]
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	//左右区间取小的尾插到tmp数组里
	while (begin1 <= end1 && begin2 <= end2)
	{

		if (arr[begin1] <= arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}

	}
	//如果begin1没有小于end1,那么就循环尾插到tmp数组里
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}
	//如果begin2没有小于end2,那么就循环尾插到tmp数组里
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}

	//拷贝到原数组里
	memcpy(arr+begin,tmp+begin,sizeof(int)*(end-begin+1));
}

📓2.4.2 归并排序(非递归实现)

  • 用递归写的代码,要有非递归版本,不然在一些特定的情况下递归深度太深会导致栈溢出
  • 归并排序的非递归思路:
  1. 开辟一个和原数组一样大小的空间
  2. gap从去开始(11归并、22归并、44归并等)
  • 单趟思路:
  1. 循环 i 控制每次循环上来i += 2 * gap
  2. 这里需要注意的是区间的控制:
  3. 归并到tmp数组里了后,把数据拷贝回原数组里
c 复制代码
void MergeSortNonR(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int gap = 1;
	int i = 0;
	for (i = 0; i < n; i += 2 * gap)
	{
		//[begin1, end1] [begin2, end2]
		int begin1 = i;
		int end1 = i + gap - 1;
		int begin2 = i + gap;
		int end2 = i + 2 * gap - 1;
		int index = begin;
		//左右区间取小的尾插到tmp数组里
		while (begin1 <= end1 && begin2 <= end2)
		{
	
			if (arr[begin1] <= arr[begin2])
			{
				tmp[index++] = arr[begin1++];
			}
			else
			{
				tmp[index++] = arr[begin2++];
			}
	
		}
		//如果begin1没有小于end1,那么就循环尾插到tmp数组里
		while (begin1 <= end1)
		{
			tmp[index++] = arr[begin1++];
		}
		//如果begin2没有小于end2,那么就循环尾插到tmp数组里
		while (begin2 <= end2)
		{
			tmp[index++] = arr[begin2++];
		}
		//拷贝回原数组里
		memcpy(arr+i,tmp+i,sizeof(int) * (end2-i+1));
	}

	free(tmp);
	tmp = NULL;
}
  • 整体思路:
    在加一层循环,条件为gap<n,里面执行结束后,gap *= 2,这样就可以11归并、22归并、44归并了
c 复制代码
void MergeSortNonR(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			//[begin1, end1] [begin2, end2]
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;

			while (begin1 <= end1 && begin2 <= end2)
			{

				if (arr[begin1] <= arr[begin2])
				{
					tmp[index++] = arr[begin1++];
				}
				else
				{
					tmp[index++] = arr[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				tmp[index++] = arr[begin1++];
			}

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

			memcpy(arr+i,tmp+i,sizeof(int)*(end2-i+1));
		}
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}
  • 这样归并排序就实现好了,当还没完全实现完
  • 8个数据没有出现问题,当9个数据或10个数据时会有越界访问的问题
  • 具体是begin1、end1、begin2、end2哪个区间有越界我们不知道,那么就打印一下区间来看看
  • 知道了是end1、begin2、end2或begin2、end2或end2在些区间会出现越界后,就加上判断即可
  1. 判断begin2是否大于等于n,是就break跳出循环,不归并这组数
  2. 如果只是end2大于等于n,那么修正一下把end2改为n-1即可
c 复制代码
void MergeSortNonR(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			//[begin1, end1] [begin2, end2]
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;

			//如果第二组不存在,那么这一组就不用归并了
			if (begin2 >= n) 
			{
				break;
			}

			//最右边不存在的话,修正一下
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			//printf("[%d][%d][%d][%d] ", \
					begin1, end1, begin2, end2);

			while (begin1 <= end1 && begin2 <= end2)
			{

				if (arr[begin1] <= arr[begin2])
				{
					tmp[index++] = arr[begin1++];
				}
				else
				{
					tmp[index++] = arr[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				tmp[index++] = arr[begin1++];
			}

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

			memcpy(arr+i,tmp+i,sizeof(int)*(end2-i+1));
		}
		//printf("\n");
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}
  • 总结:
  1. 归并排序的时间复杂度:O(N*logN)
  2. 归并排序的空间复杂度:O(N)

📙2.5 非比较排序

📓2.5.1 计数排序

  1. 计算排序是通过一个临时数组统计每个数据出现的次数

  2. 然后通过个数覆盖到原数组里

    这种方法称为映射,上面那种是绝对映射,还有一种是相对映射

    思路:

  3. 遍历找出最大和最小值

  4. 计算出开辟空间的大小,然后开辟空间命名为count

  5. 用原数组的数据减去最小值,然后在count数组里统计出现的次数

  6. 把count数组统计的次数依次覆盖到原数组里

c 复制代码
void CountSort(int* arr, int n)
{
	int max = arr[0];
	int min = arr[0];
	//找最大和最小值
	int i = 0;
	for (i = 1; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}

		if (arr[i] < min)
		{
			min = arr[i];
		}

	}

	int range = max - min + 1;

	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	memset(count, 0, sizeof(int) * range);//初始化为0

	//统计数据出现的次数
	for (i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}

	//排序,依次取出覆盖到原数组里
	int j = 0;
	for (i = 0; i < range; i++)
	{

		while (count[i]--)
		{
			arr[j++] = i + min;
		}

	}

}
  • 总结:
  1. 计数排序的时间复杂度:O(N + range)
  2. 计数排序的空间复杂度:O(range)

三、 排序算法的复杂度和稳定性

时间复杂度 空间复杂度 稳定性 不稳定的原因
插入排序 O(N^2) O(1) 稳定 ------------
希尔排序 O(N^1.3) O(1) 不稳定 预排序时,相同的数据可能分在不同的组里
选择排序 O(N^2) O(1) 不稳定
堆排序 O(N*logN) O(1) 不稳定
冒泡排序 O(N^2) O(1) 稳定 ------------
快速排序 O(N*logN) O(logN) 不稳定
归并排序 O(N*logN) O()N 稳定 ------------

四、本篇章的代码

📙4.1 Sort.h

c 复制代码
#pragma once
#include<stdio.h>
#include<string.h>

//打印函数
void PrintArray(int* arr, int n);
//直接插入排序
void InsertSort(int* arr, int n);
//希尔排序
void ShellSort(int* arr, int n);
//堆排序
void HeapSort(int* arr, int n);
//冒泡排序
void BubbleSort(int* arr, int n);
//直接选择排序
void SelectSort(int* arr, int n);
//快速排序
void QuickSort(int* arr, int begin, int end);
//快速排序 非递归
void QuickSortNonR(int* arr, int begin, int end);
//归并排序
void MergeSort(int* arr, int n);
//归并排序 非递归
void MergeSortNonR(int* arr, int n);
//计数排序
void CountSort(int* arr, int n);

📙4.2 Sort.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"
#include"Stack.h"

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

}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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];
			}
			else
			{
				break;
			}

			--end;
		}

		arr[end + 1] = tmp;
	}

}

void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//gap = gap / 2;
		gap = gap / 3 + 1;
		int i = 0;
		for (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 AdjustDown(int* arr, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//找小的孩子
		if (child+1 < n && arr[child + 1] > arr[child])
		{
			++child;
		}

		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}

	}

}

void HeapSort(int* arr, int n)
{
	//向下调整,建堆
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}

	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		--end;
	}

}

void BubbleSort(int* arr, int n)
{
	int j = 0;
	for (j = 0; j < n; j++)
	{
		int flag = 0;
		int i = 0;
		for (i = 1; i < n - j; i++)
		{

			if (arr[i - 1] > arr[i])
			{
				Swap(&arr[i - 1], &arr[i]);
				flag = 1;
			}

		}

		if (flag == 0)
		{
			break;
		}

	}
}

void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin <= end)
	{
		//[begin, end] 左闭右闭区间
		int mini = begin;
		int maxi = begin;
		int i = 0;
		for (i = begin; i < end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}

			if (arr[i] < arr[mini])
			{
				mini = i;
			}

		}
		Swap(&arr[begin], &arr[mini]);
		//max如果被替换了,修正一下
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&arr[end], &arr[maxi]);
		++begin;
		--end;
	}

}

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

		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if (arr[left] > arr[right]) //mid是最大值的下标
		{
			return left;
		}
		else
		{
			return right;
		}

	}
	else //arr[mid] < arr[left]
	{

		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if (arr[left] < arr[right]) //mid是最小值的下标
		{
			return left;
		}
		else
		{
			return right;
		}

	}

}

//hoare版本
int PartSort1(int* arr, int left, int right)
{
	int midi = GetMidi(arr, left, right);
	Swap(&arr[midi], &arr[left]);

	int kayi = left;
	while (left < right)
	{
		//找小
		while (left < right && arr[right] >= arr[kayi])
		{
			--right;
		}

		//找大
		while (left < right && arr[left] <= arr[kayi])
		{
			++left;
		}

		Swap(&arr[left], &arr[right]);
	}

	Swap(&arr[left], &arr[kayi]);
	return left;
}

//挖坑法
int PartSort2(int* arr, int left, int right)
{
	int midi = GetMidi(arr, left, right);
	Swap(&arr[midi], &arr[left]);

	int kay = arr[left];
	int hole = left;
	while (left < right)
	{
		//找小,找到后与坑位交换,右边形成新的坑
		while (left < right && arr[right] >= kay)
		{
			--right;
		}
		Swap(&arr[right], &arr[hole]);
		hole = right;

		//找大,找到后与坑位交换,左边形成新的坑
		while (left < right && arr[left] <= kay)
		{
			++left;
		}
		Swap(&arr[left], &arr[hole]);
		hole = left;
	}
	arr[hole] = kay;
	return hole;
}

//前后指针版本
int PartSort3(int* arr, int left, int right)
{
	int midi = GetMidi(arr, left, right);
	Swap(&arr[midi], &arr[left]);
	
	int prev = left;
	int cur = prev + 1;
	int kayi = left;
	while (cur <= right)
	{
		if (arr[cur] <= arr[kayi] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[prev], &arr[kayi]);
	return prev;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	//小区间优化,不在递归分割排序,降低了递归次数
	if (begin + end + 1 > 10)
	{
		int kayi = PartSort3(arr, begin, end);
		//[begin, kayi-1] kayi [kayi+1, end]
		QuickSort(arr, begin, kayi - 1);
		QuickSort(arr, kayi + 1, end);
	}
	else
	{
		InsertSort(arr, begin + end + 1);
	}

}

void QuickSortNonR(int* arr, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);
		int kayi = PartSort3(arr, left, right);
		if (kayi + 1 <= right)
		{
			STPush(&st, right);
			STPush(&st, kayi + 1);
		}

		if (left <= kayi - 1)
		{
			STPush(&st, kayi - 1);
			STPush(&st, left);
		}

	}

	STDestroy(&st);
}

void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int mid = (begin + end) / 2;
	//[begin, mid] [mid+1, end]
	_MergeSort(arr, tmp, begin, mid);
	_MergeSort(arr, tmp, mid + 1, end);
	//[begin1, end1] [begin2, end2]
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{

		if (arr[begin1] <= arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}

	}

	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}

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

	//拷贝到原数组里
	memcpy(arr+begin,tmp+begin,sizeof(int)*(end-begin+1));
}

void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(arr, tmp, 0, n-1);
	free(tmp);
	tmp = NULL;
}

void MergeSortNonR(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			//[begin1, end1] [begin2, end2]
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;

			//如果第二组不存在,那么这一组就不用归并了
			if (begin2 >= n) 
			{
				break;
			}

			//最右边不存在的话,修正一下
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			//printf("[%d][%d][%d][%d] ", \
						begin1, end1, begin2, end2);

			while (begin1 <= end1 && begin2 <= end2)
			{

				if (arr[begin1] <= arr[begin2])
				{
					tmp[index++] = arr[begin1++];
				}
				else
				{
					tmp[index++] = arr[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				tmp[index++] = arr[begin1++];
			}

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

			memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		//printf("\n");
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}

//时间复杂度:O(N + range)
//空间复杂度:O(range)
void CountSort(int* arr, int n)
{
	int max = arr[0];
	int min = arr[0];
	//找最大和最小值
	int i = 0;
	for (i = 1; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}

		if (arr[i] < min)
		{
			min = arr[i];
		}

	}

	int range = max - min + 1;

	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	memset(count, 0, sizeof(int) * range);//初始化为0

	//统计数据出现的次数
	for (i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}

	//排序
	int j = 0;
	for (i = 0; i < range; i++)
	{

		while (count[i]--)
		{
			arr[j++] = i + min;
		}

	}

}

📙4.3 Test.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"

void TestInsertSort()
{
	int arr[] = { 9,5,3,7,2,8,1,4,5,6,10 };
	int size = sizeof(arr) / sizeof(arr[0]);
	InsertSort(arr, size);
	PrintArray(arr, size);
}

void TestShellSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int size = sizeof(arr) / sizeof(arr[0]);
	ShellSort(arr, size);
	PrintArray(arr, size);
}

void TestHeapSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int size = sizeof(arr) / sizeof(arr[0]);
	HeapSort(arr, size);
	PrintArray(arr, size);
}

void TestBubbleSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int size = sizeof(arr) / sizeof(arr[0]);
	BubbleSort(arr, size);
	PrintArray(arr, size);
}

void TestSelectSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	int size = sizeof(arr) / sizeof(arr[0]);
	SelectSort(arr, size);
	PrintArray(arr, size);
}

void TestQuickSort()
{
	int arr[] = {6,1,2,7,9,3,4,5,10,8 };
	int size = sizeof(arr) / sizeof(arr[0]);
	QuickSort(arr, 0, size - 1);
	PrintArray(arr, size);
}

void TestQuickSortNonRSort()
{
	int arr[] = { 6,1,2,7,9,3,4,5,10,8 };
	int size = sizeof(arr) / sizeof(arr[0]);
	QuickSortNonR(arr, 0, size-1);
	PrintArray(arr, size);
}

void TestMergeSort()
{
	int arr[] = { 10,6,7,1,3,9,4,2 };
	int size = sizeof(arr) / sizeof(arr[0]);
	MergeSort(arr, size);
	PrintArray(arr, size);
}

void TestMergeSortNonR()
{
	int arr[] = { 6,7,1,3,9,4,2,5,9,1,1 };
	int size = sizeof(arr) / sizeof(arr[0]);
	MergeSortNonR(arr, size);
	PrintArray(arr, size);
}

void TestCountSort()
{
	int arr[] = { 6,7,1,3,9,4,2,5,9,1,1 };
	int size = sizeof(arr) / sizeof(arr[0]);
	CountSort(arr, size);
	PrintArray(arr, size);
}

int main()
{
	//TestInsertSort();
	//TestShellSort();
	//TestHeapSort();
	//TestBubbleSort();
	//TestSelectSort();
	//TestQuickSort();
	//TestQuickSortNonRSort();
	//TestMergeSort();
	//TestMergeSortNonR();
	TestCountSort();

	return 0;
}
相关推荐
pianmian12 分钟前
python数据结构基础(7)
数据结构·算法
好奇龙猫2 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
sp_fyf_20243 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
ChoSeitaku3 小时前
链表交集相关算法题|AB链表公共元素生成链表C|AB链表交集存放于A|连续子序列|相交链表求交点位置(C)
数据结构·考研·链表
偷心编程3 小时前
双向链表专题
数据结构
香菜大丸3 小时前
链表的归并排序
数据结构·算法·链表
jrrz08283 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time3 小时前
golang学习2
算法
@小博的博客3 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
南宫生4 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法