常见的排序算法

常见的排序算法

前言: 文章主要介绍以下几大类排序算法

在这里插入图片描述

说明: 文章中的代码示例均为升序写法

1.插入排序

1.1直接插入排序

基本思想

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

在这里插入图片描述

代码
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
        //单趟插入排序
		int end = i;
		//把要插入的数据保存起来
		//理由:将后一个数据往前插入,所以需要把后一个数据保存起来
		int tmp = a[end + 1];        //要插入的数据
      
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;    //插入的数据永远放到结束位置的后一个
	}
}
分析

1.时间复杂度: O(N^2) (在逆序情况下最坏,时间复杂度成等差数列为O(N^2))
有序或接近有序时最好, 顺序有序时间复杂度为O(N)
2.空间复杂度: O(1)
3.稳定性: 稳定
(稳定性: 简单来说排序一组数, 如果相同的数可以保证它们的相对顺序不变,那就是稳定的)

1.2希尔排序

基本思想

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

总结起来就是: 预排序(分组排序)+插入排序的过程

过渡过程

在写希尔排序代码前的过渡过程: 分为3个过渡部分

希尔排序简单来说就是高级的直接插入排序

画图讲解:

1.

在这里插入图片描述

代码
void ShellSort1(int* a, int n)
{
	int gap = 3;
	for (int i = 0; i < n - gap; i+=gap)
	{
		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=1的一一比较换成了gap=3的组内比较

实现的排序效果:(以数组 9 8 7 6 5 4 3 2 1 为例)

在这里插入图片描述

2.

在这里插入图片描述

代码
void ShellSort2(int* a, int n)
{
	int gap = 3;
	for (int j = 0; j < gap; j++)
	{
		for (int i = j; i < n - gap; i += gap)
		{
			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;
		}
	}
}

从第1个过程写第2个过程,过程2在过程1上就是加了一个循环,实现了一个组距为gap的3组排序

实现的排序效果:(以数组 9 8 7 6 5 4 3 2 1 为例)

在这里插入图片描述

3.

在这里插入图片描述

代码
void ShellSort3(int* a, int n)
{
	int gap = 3;

	for (int i = 0; i < n - gap; i++)
	{
		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;
		}
	}
}

3是2的升级, 3在2的基础上去掉了一层循环却实现了相同的功能,其实现方法如上图所示,不用外加循环一组一组进行gap组排序而是直接实现gap组并排

实现的排序效果:(以数组 9 8 7 6 5 4 3 2 1 为例)

在这里插入图片描述

总结: 以上希尔排序的过渡过程实现了预排序,使要排序的数组接近有序,要达到真正的有序要在数组接近有序的条件下进行直接插入排序

实现真正的希尔排序两大过程: 预排序(分组排序)+插入排序
1.预排序: 在过渡过程中已经实现, 现在唯一要解决的是要确定gap的值,为何上面演示例子中gap=3
2.插入排序: 在预排序的情况完成下,只需使最后一次排序为直接插入排序,即最后一次排序使gap=1

主要问题: 确定gap的值,并且要保证gap是可变的

gap取值问题:

排序中gap取值不同排序出来的效果:

在这里插入图片描述

由此得出以下结论:

gap越大,大的数可以更快到后面,小的数可以更快到前面。越不接近有序。
gap越小,数据跳动越慢。越接近有序。

gap最终取值:

在这里插入图片描述上面两种取法任意一种即可

在外面添加一层循环, 保证每次gap的值是可变的

代码
void ShellSort(int* a, int n)
{
	//gap>1 预排序
    //gap=1 直接插入排序

	int gap = n;

	while (gap > 1)
	{
		gap = gap/3 + 1;

		for (int i = 0; i < n - gap; i++)
		{
			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;
			}
		}
	}
}
分析
  1. 时间复杂度: O(N^1.3)(不固定)

    希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定

在这里插入图片描述

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

  2. 稳定性: 不稳定

    (预排序可能会使相同的数据分到不同的组中)

2.选择排序

2.1直接选择排序

基本思想

在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素。
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

在这里插入图片描述

代码
原版:

只找最大值或最小值来排序

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void SelectSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int mini = i;

		for (int j = i+1; j < n; j++)
		{
			if (a[j] < a[mini])
			{
				mini=j;
			}
		}
		Swap(&a[mini], &a[i]);
	}
}
优化版:

找最大值和最小值来排序

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
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[i] < a[mini])
			{
				mini = i;
			}

			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[mini], &a[begin]);
		//更新成原来为i的下标
		if (maxi == begin)
			maxi = mini;
		Swap(&a[maxi], &a[end]);

		begin++;
		end--;
	}
}
分析
  1. 时间复杂度: O(N^2)
  2. 空间复杂度: O(1)
  3. 稳定性: 不稳定

跟直接插入排序比较, 谁更好 — 直接插入排序
直接插入排序适应性很强, 对于有序或局部有序, 都能效率提升font>
选择排序任何情况下都是O(N^2), 包括有序或接近有序/font>

2.2堆排序

见文章链接: 链接

3.交换排序

3.1冒泡排序

基本思想

在这里插入图片描述

在这里插入图片描述

代码
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int exchange = 0;

		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				exchange = 1;
				Swap(&a[j], &a[j+1]);
			}
		}
        //如果一趟冒泡排序中并没有发生交换,说明原数组已经有序,不需要再处理
		if (exchange == 0)
		{
			break;
		}
	}
}
分析
  1. 时间复杂度:O(N^2)

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

  3. 稳定性:稳定

3.2 快速排序

基本思想

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

快速排序有递归和非递归两种写法

递归
过渡过程
1. hoare版本

在这里插入图片描述

[6,1,2,7,9,3,4,5,8,10]通过一趟快速排序达到了以下效果:

在这里插入图片描述

基于hoare提出的方法,写出最初的代码:

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

	int left = begin, right = end;
	int keyi = left;

	//一趟快速排序
	while (left < right)
	{
		//右边先走,找小
		while (left<right && a[right] >= a[keyi]) 
		{
			right--;
		}

		//左边再走,找大
		while (left<right && a[left] <= a[keyi])
		{
			left++;
		}

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

	Swap(&a[left], &a[keyi]);
	keyi = left;

	//[begin, keyi-1]  keyi  [keyi+1, end]

	//递归左区间和右区间
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

此代码2个小细节问题:

while (left < right)
while (left<right && a[right] >= a[keyi]) 

1. 最外层的while循环已经判断了 left<right, 内层循还是要判断left<right

一旦满足条件从外层循环进入内层循环,在外层只判断了一次left<right,而内层也是循环会进入多次,每一次进入内层循环必须要满足left<right的条件

2. a[right]>=a[keyi]和a[left]<=a[keyi]中必须有‘=’,否则会出现错误

(1)a[keyi]右边所有值都大于左边时, 没有’=‘会出现越界的问题
(2)左右两边有跟a[keyi]相等的值时, 没有’='会出现死循环的问题

因为还有两种方法,所以快速排序代码分成两部分: 框架+单趟排序的过程

hoare代码

int PartSort1(int* a, int begin, int end)
{
	int left = begin;
	int	right = end;
	int keyi = left;

	//一趟快速排序
	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//左边再走,找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

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

	Swap(&a[left], &a[keyi]);
	keyi = left;

	return keyi;
}

框架

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

	int keyi = PartSort1(a, begin, end);

	//[begin, keyi-1]  keyi  [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
2. 挖坑法

在这里插入图片描述

挖坑代码

int PartSort2(int* a, int begin, int end)
{
	int left = begin;
	int key = a[left];
	int	right = end;
	int hole = left;

	//一趟快速排序
	while (left < right)
	{
		//右边找小, 填到左边的坑里
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大, 填到右边的坑里
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
    //让key填到自己的坑位上
	a[hole] = key;
	return key;
}

挖坑和hoare版本有些相似,二者都是左右指针控制来进行一趟快速排序,不同的是挖坑法不像hoare法交换元素,挖坑法设置了一个坑位,将第一个数据存储到临时变量key中, 形成一个坑位,通过多次填坑,使key这个元素确定自己的最终位置

3.前后指针版本

不同于上面两种写法, 此法代码简单好写但是难以理解

在这里插入图片描述

总结下来写法:
在这里插入图片描述

前后指针代码

版本1:

int PartSort3(int* a, int begin, int end)
{
	int key = begin;
	int prev = begin;
	int cur = begin+1;

	while (cur <=end)
	{
		if (a[cur]<a[key])
		{
			//找到比key小的值时, 跟++prev交换, 小的往前翻, 大的往后翻
			Swap(&a[++prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);

	key = prev;
	return key;
}

版本2:

int PartSort3(int* a, int begin, int end)
{
	int key = begin;
	int prev = begin;
	int cur = begin + 1;

	while (cur <= end)
	{
        //优化了一下,如果prev和cur指向同一个元素不用交换
		if (a[cur] < a[key] && ++prev != cur)
		{
			//找到比key小的值时, 跟++prev交换, 小的往前翻, 大的往后翻
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);

	key = prev;
	return key;
}
两种优化
1.三数取中

快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,所以快速排序的时间复杂度理想状况下O(N*logN)

在这里插入图片描述

但是如果一组数据为顺序有序或逆序有序来排序,那么每次都选取第一个元素作为key,代码效率会很低

三数取中的出现是为了解决这类问题

在这里插入图片描述

代码

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;

	if (a[begin] < a[mid])
	{
		if (a[end] > a[mid])
		{
			return mid;
		}
		else if (a[end] < a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else    //a[begin] > a[mid]
	{
		if (a[end] < a[mid])
		{
			return mid;
		}
		else if (a[end] > a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

三数取中放在快速排序单趟的最前面来优化,代码写法见下文

2.小区间优化

解决快速排序最后几次递归调用时递归次数太多的问题, 最后几次递归调用用直接插入排序来处理

在这里插入图片描述

小区间优化放在快速排序的框架来优化,代码写法见下文

非递归

栈详见链接: 链接

思路

非递归的思路很像递归, 首先了解递归的关键是区间,非递归也是。
依次把我们需要单趟排的区间入栈,依次取栈里面的区间出来单趟排,再把需要处理的子区间入栈,以此循环,直到栈为空的时候即处理完毕。

在这里插入图片描述

代码
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);

	//先把第一段区间进去,先入左再入右
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		//单个区间,先出右再出左
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		//单趟排序
		int keyi = PartSort3(a, begin, end);

		//由keyi划分出来的区间,先排序左区间,再排序右区间 --> 先入右有区间再入左区间

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

		//区间剩一个数不用进栈或区间不存在

		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);    //单个区间先入左再入右,先出左再出右
			StackPush(&st, right);
		}

		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}
	StackDestroy(&st);
}
优点

当递归深度过大会存在栈溢出的问题。

非递归不存在栈溢出的风险,数据结构的栈不太会崩因为是动态开辟的,动态开辟的栈在堆上,堆空间远大于栈空间

代码(递归)
三数取中
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;

	if (a[begin] < a[mid])
	{
		if (a[end] > a[mid])
		{
			return mid;
		}
		else if (a[end] < a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else    //a[begin] > a[mid]
	{
		if (a[end] < a[mid])
		{
			return mid;
		}
		else if (a[end] > a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
框架
void QuickSort(int* a, int begin, int end)
{
	//该区间不存在
	if (begin >= end)
		return;

	if ((end - begin + 1) < 15)     //这里的'15'也可以调整成其他数(比如说十几)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort2(a, begin, end);

		//[begin,keyi-1]   keyi   [keyi+1, end] 
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}
1. hoare版本
int PartSort1(int* a, int begin, int end)
{
    //三数取中
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin;
	int	right = end;
	int keyi = left;

	//一趟快速排序
	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//左边再走,找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

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

	Swap(&a[left], &a[keyi]);
	keyi = left;

	return keyi;
}
2. 挖坑法
int PartSort2(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);
	
	int left = begin;
	int key = a[left];
	int	right = end;
	int hole = left;

	//一趟快速排序
	while (left < right)
	{
		//右边找小, 填到左边的坑里
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大, 填到右边的坑里
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
    //让key填到自己的坑位上
	a[hole] = key;
	return key;
}
3. 前后指针版本
第一版
int PartSort3(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);
    
	int key = begin;
	int prev = begin;
	int cur = begin+1;

	while (cur <=end)
	{
		if (a[cur]<a[key])
		{
			//找到比key小的值时, 跟++prev交换, 小的往前翻, 大的往后翻
			Swap(&a[++prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);

	key = prev;
	return key;
}
第二版
int PartSort3(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);
    
	int key = begin;
	int prev = begin;
	int cur = begin+1;

	while (cur <=end)
	{
		if (a[cur]<a[key])
		{
			//找到比key小的值时, 跟++prev交换, 小的往前翻, 大的往后翻
			Swap(&a[++prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);

	key = prev;
	return key;
}
分析(递归过程)
  1. 时间复杂度: O(N*logN)
  2. 空间复杂度: O(1ogN) (递归的深度是logN)
  3. 稳定性: 稳定

4.归并排序

基本思想

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

在这里插入图片描述

在这里插入图片描述

递归

单趟的思路有些像leetcode这题: 88. 合并两个有序数组 - 力扣(LeetCode)

总结: 开辟一个新的数组,单趟排序就是取小的数尾插到新的数组中后拷贝到原数组

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

    //划分子区间
	int mid = (begin + end) / 2;

	//[begin, mid]  [mid+1, end]     递归让子区间有序
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	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++];
		}
	}

    //数组2先结束,数组1直接尾插
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

    //数组1先结束,数组2直接尾插
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

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

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

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

	free(tmp);
	tmp = NULL;
}
非递归
思路

用循环来写,设置rangN为每趟归并中每组的数据个数,最开始rangN=1,每组归并中数的个数为1,两组两组归并,归并完后要拷贝回去,可以整体归并完一趟再整体,拷贝回去,也可以归并完一次拷贝回去,每趟归并完后rangN*=2,就完成了一趟归并

在这里插入图片描述

在这里插入图片描述

一边归并一边拷贝

在这里插入图片描述

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

	//rangN归并每组数据,从1开始,因为1个认为是有序的,可以直接归并
	int rangN = 1;
	while (rangN < n)
	{
        //一趟归并
		for (int i = 0; i < n; i += rangN * 2)
		{
            //一次归并
            
			int begin1 = i, end1 = i + rangN - 1;
			int begin2 = i + rangN, end2 = i + 2 * rangN - 1;
			int j = i;

			//end1, begin2, end2越界
			if (end1 >= n)
			{
				break;
			}
			//begin2, end2越界
			else if (begin2 >= n)
			{
				break;
			}
			//end2越界
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			//取小的尾插
			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));
		}
		rangN *= 2;
	}
	free(tmp);
	tmp = NULL;
}
整体归并完再拷贝

在这里插入图片描述

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

	//rangN归并每组数据,从1开始,因为1个认为是有序的,可以直接归并
	int rangN = 1;
	while (rangN < n)
	{
		//一趟归并
		for (int i = 0; i < n; i += rangN * 2)
		{
			//一次归并

			int begin1 = i, end1 = i + rangN - 1;
			int begin2 = i + rangN, end2 = i + 2 * rangN - 1;
			int j = i;

			//修正区间 -> 整体拷贝数据
			//end1, begin2, end2越界
			if (end1 >= n)
			{
				//修正成不存在的区间
				end1 = n-1;
				begin2 = n;
				end2 = n - 1;
			}
			//begin2, end2越界
			else if(begin2>=n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			//end2越界
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			//取小的尾插
			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, tmp, sizeof(int) * n);

		rangN *= 2;
	}
	free(tmp);
	tmp = NULL;
}
分析
  1. 时间复杂度: O(N*logN)

在这里插入图片描述

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

  2. 稳定性: 稳定

5.计数排序

基本思想

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

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

在这里插入图片描述

在这里插入图片描述

代码

void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];

	//找最大数和最小数,以确定开辟数组的大小
	for (int i = 0; i < n; ++i)
	{
		if (a[i] > max)
			max = a[i];
	
		if (a[i] < min)
			min = a[i];
	}
	
	int range = max - min + 1;
	int* countA = (int*)calloc(range, sizeof(int));
	if (countA == NULL)
	{
		perror("calloc fail");
		exit(-1);
	}
	
	//1. 统计次数
	for (int i = 0; i < n; ++i)
	{
		countA[a[i] - min]++;    //存放的是数据出现的次数
	}


	//2. 排序
	int k = 0;
	for (int j = 0;j<range; j++)
	{
		while (countA[j]--)
		{
			a[k++] = j + min;
		}
	}
	
	free(countA);
	countA = NULL;
}

分析

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

排序总结

排序性能评估
// 测试排序的性能对比
void Test()
{
   srand(time(0));
   const int N = 10000;
   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);
    
   for (int i = 0; i < N; ++i)
   {
      a1[i] = rand();
      a2[i] = a1[i];
      a3[i] = a1[i];
      a4[i] = a1[i];
      a5[i] = a1[i];
      a6[i] = a1[i];
      a7[i] = a1[i];
   }
      int begin1 = clock();
      InsertSort(a1, N);
      int end1 = clock();
    
      int begin2 = clock();
      ShellSort(a2, N);
      int end2 = clock();
    
      int begin3 = clock();
      SelectSort(a3, N);
      int end3 = clock();
    
      int begin4 = clock();
      HeapSort(a4, N);
      int end4 = clock();
    
      int begin5 = clock();
      QuickSort(a4, 0, N-1);
      int end5 = clock();
    
      int begin6 = clock();
      MergeSort(a6, N);
      int end6 = clock();
      
      printf("InsertSort:%d\n", end1 - begin1);
      printf("ShellSort:%d\n", end2 - begin2);
      printf("SelectSort:%d\n", end3 - begin3);
      printf("HeapSort:%d\n", end4 - begin4);
      printf("QuickSort:%d\n", end5 - begin5);
      printf("MergeSort:%d\n", end6 - begin6);
     
      free(a1);
      free(a2);
      free(a3);
      free(a4);
      free(a5);
      free(a6);
      free(a7);
 }
八大排序时间/空间复杂度一览

在这里插入图片描述

稳定性的意义:

比如我们做了一个考试系统,考生当中先交卷的,成绩在数组的前面,后交卷的,成绩在数组后面。当我们对前几名进行排名的时候,就可能会遇见几个分值相同的考生,这时候为了公平性考试可以把用时较短者排在前面。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值