【数据结构】八大排序

八大排序

排序的概念:

  1. 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
  2. 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  3. 内部排序:数据元素全部放在内存中的排序。
  4. 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。

一.插入排序

基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列(实际中我们玩扑克牌时,就用了插入排序的思想)。

1.直接插入排序

思路:当插入第i(i>=1)个元素时,前面的array[0]、array[1]...array[i-1]已经排好序,此时用array[i]的排序码与array[i-1]、array[i-2]...的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序整体后移一位。

动图如下:

c 复制代码
void InsertSort(int* a, int n)
{
	//循环n-1次,防止tmp越界
	for (int i = 0; i < n - 1; i++)
	{
		int end = i; //[0, end]有序,将end-1位置的值插入依旧保持有序
		int tmp = a[end + 1]; //保存要插入的数据
		while (end >= 0)
		{
			if (a[end] > tmp) //以升序为准,若tmp值小,则数据后移一位
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break; //退出内层循环
			}
		}
		a[end + 1] = tmp; //将end-1位置的值插入
	}
}

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

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高。
  2. 时间复杂度:O(N^2) (最好情况:顺序O(N),最坏情况:逆序O(N^2))
  3. 空间复杂度:O(1)
  4. 稳定性:稳定。

2.希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数(通常是gap=n/3+1),把待排序数据分成gap组,所有的距离相差为gap的分在同一组内,并对每一组内的数据进行插入排序,然后gap=gap/3+1得到下一个整数gap,再将数组分成gap组,进行插入排序,当gap=1时,只存在一组数据,就相当于直接插入排序。希尔排序它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率要高于直接插入排序。
希尔排序:

  1. 预排序(让数组接近有序)。
  2. 直接插入排序。

预排序:

  1. gap越大,大的数可以更快的跳到后面,小的数可以更快的跳到前面,越不接近有序。
  2. gap越小,跳的越慢,但是越接近有序。
  3. gap==1时相当于直接插入排序。

希尔排序图解如下:

c 复制代码
void ShellSort(int* a, int n)
{
	int gap = n;

	while (gap > 1)
	{
	    //或者gap = gap / 2; 但是没有gap = gap / 3 + 1好
		gap = gap / 3 + 1; //更新gap

		//注意:i<n-gap; 防止tmp数据越界
		for (int i = 0; i < n - gap; i++)
		{
			//当gap==1时,本质就是直接插入排序
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

尝试计算希尔排序的时间复杂度:gap = gap/3(忽略+1,方便计算)

通过以上的分析,可以画出这样的曲线图:

因此,希尔排序在最初和最后的排序的次数都为n,即前一阶段排序次数是逐渐上升的状态,当到达某一顶点时,排序次数逐渐下降至n,而该顶点的计算暂时无法给出具体的计算过程。大约是O(N^1.3)

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,直接插入排序,这样整体而言,可以达到优化的效果。
  3. 时间复杂度:O(N^1.3),因为gap的取值方法很多,导致很难去计算。
  4. 稳定性:不稳定。

二.选择排序

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

1.直接选择排序

思路:

  1. 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素。
  2. 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
  3. 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
c 复制代码
//将最小值往前交换
void SeleteSort(int* a, int n)
{
	//只需循环n-1轮
	for (int i = 0; i < n - 1; i++)
	{
		int mini = i; //默认mini下标对应最小值。若记录最小值,交换时Swap函数无法完成数组中数据的交换
		for (int j = i + 1; j < n; j++)
		{
			if (a[mini] > a[j])
			{
				mini = j; //更新mini
			}
		}
		if (mini != i) //若mini==i,则无需交换
		{
			Swap(&a[mini], &a[i]);
		}
	}
}

//双向找最大值与最小值,放到头和尾处
void SeleteSort(int* a, int n)
{
	//找到最大值与最小值的下标时,用于交换下标begin和下标end的数据
	int begin = 0, end = n - 1;

	while (begin < end)
	{
		int mini = begin, maxi = begin; //默认最大、最小值的下标为begin
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[mini] > a[i]) //遍历找真正最小值的下标
			{
				mini = i;
			}
			if (a[maxi] < a[i]) //遍历找真正最大值的下标
			{
				maxi = i;
			}
		}
		//找到后交换数组中的数据
		Swap(&a[begin], &a[mini]);
		if (begin == maxi) //注意:如果最大值的下标是begin,则需要更新最大值的下标
		{
			maxi = mini;
		}

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

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

  1. 直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用。
  2. 时间复杂度:O(N^2) (最好情况:顺序O(N^2), 最坏情况:逆序O(N^2))
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定。

2.堆排序

思路:首先N个数据建堆(升序建大堆,降序建小堆),交换堆顶与堆尾数据(此时堆尾排序成功),堆顶向下前N-1个数据调整恢复大堆,循环直到排序成功为止。

什么是?点击跳转

以升序为例(建大堆:任何一个父亲节点都大于孩子节点)

开始排序:

c 复制代码
void AdjustDown(int* a, int n, int parent)
{
	//假设左孩子大于右孩子
	int child = 2 * parent + 1;

	while (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 = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//排升序:建大堆
	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); //重新前n-1个数据恢复大堆
		end--;
	}
}

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定。

三.交换排序

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

1.冒泡排序

思路:从第一个数据开始,依次比较相邻的两个数据的大小,将较大的值冒泡到最后面(以升序为例),循环直到数据有序为止。

动图如下:

c 复制代码
void BubbleSort(int* a, int n)
{
	//只需循环n-1轮
	for (int i = 0; i < n - 1; i++)
	{
		int flag = 1; //标记flag==1默认已经有序(优化,可以提高点效率)
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1]) //以升序为准,若前大于后,交换数据
			{
				Swap(&a[j], &a[j + 1]);
				flag = 0; //
			}
		}
		if (flag == 1) //若flag任然为1,表明上一轮循环未交换数据(数据有序)可以提前结束循环
		{
			break;
		}
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序,实际没什么意义。
  2. 时间复杂度:O(N^2) (最好情况:顺序(含优化O(N),不含优化O(N^2)),最坏情况:逆序 O(N^2))
  3. 空间复杂度:O(1)
  4. 稳定性:稳定(相同数据的相对位置没有发生变化)

2.快速排序

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

递归方法

1.hoare版本

思路:数组array[0]做key,右边先走找到小于key的值为止,左边再走找到大于key的值为止,交换数组中的两个值,循环直到左与右相遇为止,将数组中的key与相遇点的数据交换,最后递归左区间+递归右区间。

动图如下:

快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。递归图如下:

c 复制代码
void QuickSort1(int* a, int left, int right)
{
	if (left >= right) //递归结束条件:区间只有一个值、区间不存在
		return;

	int keyi = left; //选择left为基准值的下标
	int begin = left, end = right; //从left、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]); //交换最大值和最小值
	}
	//交换keyi与begin、end的相遇位置的值,将数组划分为三个部分:左边小于基准值区间、基准值、右边大于基准值区间
	Swap(&a[keyi], &a[begin]); //相遇位置一定比a[keyi]小
	keyi = begin;

	//递归左区间+右区间 (类似二叉树的前序遍历)
	
	//[left, keyi-1] keyi [keyi+1,right]
	QuickSort1(a, left, keyi - 1);
	QuickSort1(a, keyi + 1, right);
}

总结:

  1. 左边做key,必须右边先走找小,左边再走找大,相遇位置一定比key小。
  2. 右边做key,必须左边先走找大,右边再走找小,相遇位置一定比key大。
  3. 一边key,让另一边先走。
2.挖坑法

思路:数组中array[0]做基准值key,保留坑位的下标0,右边先找到小于key的值为止,将改值填入坑位,并更新坑位为改值在数组中的下标,左边再找到大于key的值为止,将改值填入坑位,并更新坑位为改值在数组中的下标,循环此步骤直到左右相遇为止,这时相遇点就是坑位,然后将key填入坑位,最后递归左区间+右区间。

动图如下:

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

	int key = a[left]; //保存基准值
	int begin = left, end = right; //从left、right开始遍历,找最大值和最小值
	int hole = left; //坑位的下标

	while (begin < end)
	{
		while (begin < end && a[end] >= key) //右边找小 注意:加上=否则死循环
		{
			end--;
		}
		a[hole] = a[end]; //将小于key的值填入坑位
		hole = end; //更新坑位

		while (begin < end && a[begin] <= key) //左边找大
		{
			begin++;
		}
		a[hole] = a[begin]; //将大于key的值填入坑位
		hole = begin; //更新坑位
	}
	a[hole] = key; //相遇后将key填入坑位

	//递归左区间+右区间 (类似二叉树的前序遍历)
	//[left,hole-1] hole [hole+1,right]
	QuickSort(a, left, hole - 1);
	QuickSort(a, hole + 1, right);
}

总结:虽然挖坑法没有效率的提升,但是不用分析,左边做key,右边先走的问题,也不用分析相遇位置为什么比key小的问题,因为相遇的位置是坑(看作没有值,但代码实际是数据的覆盖)。

3.前后指针法(推荐)

思路:数组array[0]做key,定义两个指针prev = 0和cur = 1,遍历数组,当arrry[cur] <

key时,++prev,交换arrry[cur]与arrry[prev],++cur,否则只++cur。递归左区间+递归右区间。

动图如下:

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

	int keyi = left; //选择left为基准值的下标
	int prev = left, cur = left + 1; //前后指针
	while (cur <= right)
	{
		if (a[cur] < a[keyi]) //cur找小于a[keyi]的值
		{
			prev++;
			if (prev != cur)
			{
				Swap(&a[cur], &a[prev]);
			}
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;

	//递归左区间+右区间 (类似二叉树的前序遍历)

	//[left, keyi-1] keyi [keyi+1,right]
	QuickSort3(a, left, keyi - 1);
	QuickSort3(a, keyi + 1, right);
}

总结:前后指针法依然没有效率的提升,但是代码更加简单(推荐)

递归方法的优化

1.三数取中(选基准值)

若数据本身就是有序的话,快速排序就会退化:

  1. 时间复杂度变成:O(N^2) 。 原因:基准值为array[0],右边找小,直到遍历到array[0]结束,array[0]自己与自己交换数据,消耗N。再递归左区间(区间不存在,递归直接结束),递归右区间(N-1个数据),本质变成了等差数列求和。
  2. 并且数据多的情况下递归深度太深,建立的函数栈帧太多,面临栈溢出的风险。
  1. 递归次数(或称为递归调用次数)指的是在递归过程中,递归函数被调用的总次数。这包括了初始的调用以及所有由该调用直接或间接触发的后续调用。

  2. 递归深度则是指递归调用过程中,递归函数在调用栈上达到的最大深度。换句话说,它是从初始调用开始,到达到最深层的递归调用(即还没有返回上一层递归调用的那一层)所经过的递归调用层数。

三数取中优化:选取三个数(数组首、尾、中间的数),找出中间的值与基准值(数组的第一个值)进行交换,使得快速排序的大逻辑保持不变,且提高了些效率。

c 复制代码
int GetMidindex(int* a, int left, int right)
{
	int midi = (left + right) / 2;

	if (a[left] < a[midi])
	{
		if (a[midi] < a[right]) //a[left] < a[midi] < a[right]
		{
			return midi;
		}
		else if (a[left] < a[right]) //a[midi]最大,剩下的两个返回较大值的下标
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[midi]
	{
		if (a[midi] > a[right]) //a[left] > a[midi] > a[right]
		{
			return midi;
		}
		else if (a[left] < a[right]) //a[midi]最小,剩下的两个返回较大值的下标
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	//三数取中优化:取中间值与a[left]交换位置,使得大逻辑不变
	int midi = GetMidindex(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;

	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
2.小区间优化

小区间优化:减少递归次数,提升性能。

方法:当区间数据较少时,为了减少递归次数消耗的性能,可以考虑利用直接插入排序取代递归。

c 复制代码
//C语言中的qsort函数代码类似如下:
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	//小区间优化:区间数据个数很少时,不再递归分割排序,减少递归次数
	if ((right - left + 1) <= 5)
	{
		//数据量很少时:可以利用直接插入排序取代递归
		InsertSort(a + left, right - left + 1); //注意:起点是a+left
	}
	else
	{
		//三数取中优化:取中间值与a[left]交换位置,使得大逻辑不变
		int midi = GetMidindex(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;

		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

非递归方法

1.用栈模拟递归
  1. 采用递归的方法,在32位平台下,内存中的栈大约8M,递归深度太深,栈会溢出。可以用数据结构的栈模拟递归,数据结构的栈(可以在堆上申请空间),内存中的堆大约2G,堆区>>栈区,溢出风险大大减小。
  2. 递归思路:先入一个区间,循环每走一次(相当于一次递归),取出栈顶区间,进行一趟排序,再分割左右区间,若左右区间数据个数大于1,先入右区间,再入左区间,栈为空时终止循环,排序完成。

事先拷贝一份栈的代码(Stack.h和Stack.c)点击跳转

c 复制代码
//单趟排序,返回分割成左右区间的下标
int _QuickSortNonR(int* a, int left, int right)
{
	int keyi = left;
	int prev = left, cur = left + 1;

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

	return prev;
}

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 = _QuickSortNonR(a, begin, end);

		//[begin, keyi-1] keyi [keyi+1,end]
		//若区间数据个数大于1个,先入右区间,再入左区间
		if (keyi + 1 < end) 
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	STDestory(&st);
}
  1. 用栈模拟递归(前序遍历、深度优先遍历)

  2. 也可以利用队列模拟递归(层序遍历、广度优先遍历)
    快速排序的特性总结:

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

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

  5. 空间复杂度:O(logN)(递归的深度)

  6. 稳定性:不稳定。

四.归并排序

递归方法

基本思想:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使两两子序列合并到一起有序(先分区间,使得每个区间只有一个数据为止(单个数据是有序的--->区间有序),再两两区间合并,类似二叉树的后序遍历)。

动图如下:

c 复制代码
void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin == end) //递归结束条件:区间只有一个数据
		return;

	int mid = (end + begin) / 2; //以中间值划分区间

	//划分两个区间[begin,mid] [mid+1,end],若有序就可以进行归并了(只有一个数据默认有序)
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	//合并两个有序区间到tmp中
	int begin1 = begin, begin2 = mid + 1;
	int end1 = mid, end2 = end;

	int i = begin; //注意:tmp数组起始数据的下标是begin
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2]) //取小尾插到tmp中
		{
			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 fail!");
		return;
	}

	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
}

注意:分的区间有讲究,否则稍不小心就会造成死循环,如下图

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)(额外开了数组)
  4. 稳定性:稳定。

非递归方法

由于类似后序遍历,单个栈实现不了,两个栈又太麻烦了,可以考虑循环。

思路:将数组中单个数据看成一个区间,进行两两归并,再将两个数据看成一个区间,进行两两归并......区间数据以2^n方式递增,当区间数据个数大于等于数组中的数据个数停止归并。

核心代码如下:

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

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

			//若第一个区间的end1越界、若第二个区间的begin1越界,则两区间不需要归并
			if (begin2 >= n)
			{
				break;
			}

			//第二组begin2没越界,但是end2越界,只需修正end2为n-1继续归并
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int k = i; //tmp数组起始数据的下标是i
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = a[begin2++];
			}

			//将数据归并到tmp数组后,将数据拷贝回a中,为了下一次的归并数据(必须归并一次拷贝回去,例如22归并完,再将tmp中的所有数据拷贝回数组a,可能会造成由于越界,导致a中不需进行归并的数据被tmp中的数据覆盖,数据丢失)
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int)); //注意:不能写成2*gap,修正时数据个数会改变
		}

		gap *= 2; //11归并后,再22归并...
	}

	free(tmp);
	tmp = NULL;
}

五.非比较排序

1.计数排序

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

  1. 统计相同元素出现次数。
  2. 根据统计的结果将序列回收到原来的序列中。

存在缺陷:根据最大值进行开空间,存在大量的空间浪费。

改进方法:可以根据最大值与最小值进行开空间。

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

		if (max < a[i])
			max = a[i];
	}

	//开辟空间
	int range = max - min + 1; //要开空间的数据的个数
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail!");
		return;
	}

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

	//遍历count数组,进行排序; 时间复杂度:O(range+N)
	int k = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[k++] = i + min;
		}
	}

	free(count);
}

计数排序的特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限(可以排负数,但是不能排浮点数)
  2. 时间复杂度:O(MAX(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定。

2.基数排序(了解即可)

总结:无法排序负数,对于一些特殊的数据分布(如数据集中某个位数上的数字分布极不均匀),基数排序的效率可能会下降,不如计数排序。

3.桶排序(了解即可)

总结:不如计数排序。

六.八大排序性能测试

c 复制代码
void TestOP()
{
	srand((unsigned int)time(NULL));
	const int N = 1000000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand() + i;
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];
	}

	int begin1 = clock();
	BubbleSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	SeleteSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	InsertSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	ShellSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();

	int begin7 = clock();
	HeapSort(a7, N);
	int end7 = clock();

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = clock();

	printf("BubbleSort:%d\n", end1 - begin1);
	printf("SelectSort:%d\n", end2 - begin2);
	printf("InsertSort:%d\n", end3 - begin3);
	printf("ShellSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);
	printf("HeapSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
}

七.排序算法时间复杂度、稳定性总结

排序方法 平均情况 最好情况 最坏情况 空间复杂度 稳定性
冒泡排序 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(N*logN) ~ O(N^2) O(N^1.3) O(N^2) O(1) 不稳定
堆排序 O(N*logN) O(N*logN) O(N*logN) O(1) 不稳定
归并排序 O(N*logN) O(N*logN) O(N*logN) O(N) 稳定
快速排序 O(N*logN) O(N*logN) O(N^2) O(logN) ~ O(N) 不稳定
相关推荐
running thunderbolt16 分钟前
C++:类和对象全解
c语言·开发语言·c++·算法
埋头编程~1 小时前
【初阶数据结构】详解树和二叉树(一) - 预备知识(我真的很想进步)
c语言·数据结构·c++·学习
小陈的进阶之路1 小时前
c++刷题
开发语言·c++·算法
model20052 小时前
sahi目标检测java实现
java·算法·目标检测
源代码•宸2 小时前
Leetcode—322. 零钱兑换【中等】(memset(dp,0x3f, sizeof(dp))
c++·算法·leetcode·职场和发展·dp
机械心2 小时前
最优化理论与自动驾驶(一):概述
人工智能·算法·自动驾驶
给自己做减法2 小时前
排序算法快速记忆
java·算法·排序算法
新知图书2 小时前
Rust的常量
算法·机器学习·rust
DdddJMs__1353 小时前
C语言 | Leetcode题解之第403题青蛙过河
c语言·数据结构·算法
小林熬夜学编程3 小时前
【Linux系统编程】第二十弹---进程优先级 && 命令行参数 && 环境变量
linux·运维·服务器·c语言·开发语言·算法