数据结构之排序

概念

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

应用场景:在网络上购物我们点击的价格高低、销量多少等关键词时,页面的就会展现出对应关键词的排序

抖音的热点的排序也是,
总结:在生活中排序无处不在

直接插入排序

直接插入排序就像我们在玩扑克牌时理牌的顺序,如图:

思路:创建两个整型变量 end:作为有序数组最后一个元素的下标,tmp:保存与end比较的end + 1的元素值让arr[end]与tmp比较,若arr[end] > tmp :当end >= 0将arr[end]元素挪到arr[end + 1]处,再让end--,直到end < 0时,然后将tmp保存的值赋给arr[end + 1]处,若arr[end] < tmp:说明是有序的,直接break跳出循环。循环遍历到数组结束

核心代码:

c 复制代码
//直接插入排序
//思路:创建两个整型变量 end:作为有序数组最后一个元素的下标,tmp:保存与end比较的end + 1的元素值
//让arr[end]与tmp比较,若arr[end] > tmp :当end >= 0将arr[end]元素挪到arr[end + 1]处,再让end--,直到end < 0时,然后将tmp保存的值赋给
//arr[end + 1]处,若arr[end] < tmp:说明是有序的,直接break跳出循环。循环遍历到数组结束
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)//这里数组最后一次遍历是n-1,不是n,若为n,则最后一次保存tmp时会导致越界
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}

如图:

直接插入排序的时间复杂度最差情况下为有序且为降序时:O(N^2);

最好的情况为升序:O(N)

我们到这学习了三种排序:冒泡排序、堆排序、直接插入排序,三者性能比较如下:

冒泡排序最坏为O(N^2) 最好只有为升序时为O(N) ,它更接近于O(N^2)的;

堆排序:为O(NlogN),在三者里最优;

直接插入排序为降序时最坏O(N2),平时都是<O(N2)的,所以它比冒泡排序要好一些

以下是性能测试代码及结果:

c 复制代码
void TestOP() 
{
	srand(time(0));
	const int N = 100000;
	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];
	}
	//clock -- 计算程序运行的时间
	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(a5, 0, N - 1);
	//int end5 = clock();

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

	int begin7 = clock();
	BubbleSort(a7, N);
	int end7 = 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);
	printf("BubbleSort:%d\n", end7 - begin7);

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

Debug条件下运行代码它会加载以下调试的运行信息,出结果较慢,这里测试各个排序的算法好坏用Realase更快

测试结果:

数组为降序序列时,直接插入排序是能够得到优化吗?

当然可以,使用希尔排序就可以实现

希尔排序解释:希尔排序又叫缩小增量法,将整个数据分成gap组,每组使用直接插入排序算法,先达到基本有序;最后再使用直接排序得到有序的数据

创建一个整数gap,当gap > 1时先对数组进行预排序;直到gap == 1时,再进行直接插入排序

它是在直接插入排序算法的基础上进行改进而来的,它的效率高于直接插入排序算法。

希尔排序算法实现:

推导图:

思路:先创建gap组,创建两个整型变量 end:作为有序数组最后一个元素的下标,tmp:保存与end比较的end + gap的值;先让arr[end]与tmp比较,若arr[end] > tmp :当end >= 0时将arr[end]元素挪到arr[end + gap]处,再让end-=gap,直到end < 0时,然后将tmp保存的值赋给arr[end + gap]处,若arr[end] < tmp:说明是有序的,直接break跳出循环。循环遍历到gap的组数结束

核心代码:

c 复制代码
//希尔排序时间复杂度大约为:O(N^1.3)
void ShellSort(int* arr, int n)
{
	//当gap > 1 时,预排序
	//当gap = 1时,直接插入排序
	int gap = n;
	while (gap > 1)//条件里不含=1原因:若包含=1,当n = 6时,gap取余两次后gap = 0,一直+1会造成死循环
	{
		gap = gap / 3 + 1;// /2与/3中 /3好,因为:/3分的组数少;+1:原因:若n = 6时,gap = 0,没法进行排序了,所以+个1。
		//for (int i = 0; i < gap; i++)//这里四个循环,太麻烦了,修改下,将这个循环删除后,改变里面那个循环条件: i += gap 改成:i++
		//{
			for (int i = 0; i < n - gap; i++)//修改成i++后,原本是一组一组的排序,现在是排完第一组第一个后再去排第二组第一个,以此类推。。。
				//前面学的直接插入排序时n - 1,这里是每隔gap个数据分为一组,所以数组最后一个数据为tmp =  n - gap - 1 +gap = n - 1 	
			{
				int end = i;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (arr[end] > tmp)
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				arr[end + gap] = tmp;
			}
		}
	//}
	}

这里是将数组调成最坏的情况:降序所得,很明显看出希尔排序比直接插入排序的性能好

希尔排序时间复杂度推导

因为它的有序程度和移动次数都是不确定的,它的时间复杂度计算推断出约为:O(N^1.3)

关于gap取3是否为最优吗?

推荐取3原因如下:

直接选择排序

找到最小值,让它与起始位置的值进行交换,循环遍历,直到数组有序

创建两个整型变量:begin:放到下标为0的位置,mini:找到数组中最小值的下标;

先找到下标mini的最小值,就与下标为begin的值进行交换,交换完成后让begin++,(bigin之后的数据就是待排序的数据)继续在待排序的数据中找最小的,放到bigin的位置,begin++;依次循环遍历直到数组有序

核心代码:

c 复制代码
void SelectSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		//int begin = i;//没有begin也可以
		int mini = i;
		for (int j = i + 1; j < n; j++)
		{
			if (arr[j] < arr[mini])
			{
				mini = j;
			}
		}
		//Swap(&arr[begin], &arr[mini]);
		Swap(&arr[i], &arr[mini]);
	}
}

这里有两层循环,则它的时间复杂度为O(N^2),是否能够对它进行优化呢?

当然可以:

在原来的基础上再创建max :初始状态下指向下标为0的位置,它是用来找数据中最大值的;end:初始状态下指向数组最后一个元素;

在数组中找最大和最小的值,并分别让max和mini指向各自的下标,一遍遍历完后,先交换下标为begin与下标为mini数组中的值,交换完后begin++;再交换下标为end与下标为max数组中的值,交换完后end--;

特殊情况:当max == begin时:

核心代码:

c 复制代码
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;

	while (begin < end)
	{
		int max = begin;
		int mini = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (arr[i] > arr[max])
			{
				max = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
	
		}
		//此时mini max 各自找到了最大值,最小值的下标
		//避免maxi begini都在同一个位置,begin和mini交换之后,maxi数据变成了最小的数据,这里将max与mini交换位置
		if (max == begin)
		{
			mini = max;
		}
		Swap(&arr[mini], &arr[begin]);
		Swap(&arr[max], &arr[end]);

		++begin;
		--end;
	}
}

通过代码里面推导得知:直接选择排序的时间复杂度为O(N^2) ,它的时间复杂度没有前面那些排序分情况讨论,它在任何情况下没有优化的可能,一直为O(N^2);它在工作中没有用,在教学上也没有用

快速排序

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

思路:

去找基准值

  1. 初始化参数:left 和 right

    right:从右到左找比基准值小的数据,初始状态下在数组最后一个数据的位置(下标n - 1)

    left:从左到右找比基准值大的数据,初始状态下在数组下标为0的位置

  2. 创建基准值keyi:初始状态下等于下标left;

  3. 创建while循环,当left < right 时进行循环:

    创建while循环:当数组下标为left 的值大于基准值时,跳出循环;若<= : 让left++;

    创建while循环,当数组下标为right 的值 小于基准值时,跳出循环;不小于则让right--

  4. 各自找对应的值后,先判断下标left < right ?若< 则让arr[left] 和 arr[right] 交换,

  5. 依次循环直到下标left > right跳出循环后:让arr[right] 和 arr[keyi]交换,最后返回下标right(下标right就是新的基准值)

    这里踩得坑很多,如图中写的都是需要注意的:

这是hoare版本的快排:

核心代码:

c 复制代码
//找基准值函数
int _QuickSort(int* arr, int left, int right)
{
	int keyi = left;//left初始状态下是为下标为0的。keyi是在left的位置上,而不是单纯的=0
	++left;
	//也就是说它是left 和right轮流找值得,而不是谁先全部找完
	while (left <= right)
	{
		//right找到比基准值小的 / 等于?
		while (left <= right && arr[right] > arr[keyi])//注意这里是&&关系,找到比基准值小的后,不满足条件跳出循环,进入到left找比基准值大的循环里
		{
			right--;
		}
		//left找到比基准值大的 / 等于?
		while (left <= right && arr[left] < arr[keyi])//注意这里是&&关系,找到比基准值大的后,不满足条件跳出循环
		{
			left++;
		}
		
		//right left 判断交换值
		if (left <= right)
		{
			Swap(&arr[left++], &arr[right--]);
		}
	}
	Swap(&arr[right], &arr[keyi]);
	return right;
}
void QuickSort(int* arr, int left, int right)
{
	//判断区间是否为有效区间
	if (left >= right)
	{
		return;
	}
	//找基准值keyi
	int keyi = _QuickSort(arr, left, right);
	//递归左子序列[left, keyi - 1]
	QuickSort(arr, left, keyi - 1);
	//递归右子序列[keyi + 1, right]
	QuickSort(arr, keyi + 1, right);	
}

快排为什么快,因为它每次找到基准值后分成两个序列,然后两个序列单独去排序找到基准值后继续分成两个序列再去排序找基准值(换句话说它每次排序都是以二分查找的形式去排序因此它的时间复杂度很低,效率更高)

二分查找解释:

二分查找是一种在有序数组中查找特定元素的高效搜索算法。反复将待搜索区间分成两半,并排除其中不可能存在目标值的那一半来工作。其根本思想是分而治之,算法复杂度为 O(log n),远优于线性查找的 O(n)。

二分查找就是一种每次都能排除一半错误答案的快速搜索法,但它要求数据必须是有序的。

如图中所示,在输入一百万个数据后,快排的性能依旧稳定

快排复杂度推导:

空间复杂度:这里的空间复杂度主要是它递归时创建了多少个函数栈帧引起的,与层数有关,我们知到二叉树节点个数求解公式:2^k - 1(k 为层数),则二叉树的计算层数公式 k = logN(计算时间复杂度+1省略了),因此该空间复杂度为logN

时间复杂度:

在QuickSort代码中第一次将n个数据分成n/2,第二次在n/2的基础上再分成n/2/2个进行递归排序,以此类推,直到有序,类似于有n个数,每次除以2,假设n = 8,要除的次数为:x,则x = log以二为底的8 = 3次,若n = n,则为logN次(求复杂度,底数2可省略);其次在找基准值代码中虽然有三个循环,这三个循环: 一个从数组的左边开始遍历,一个是从右边开始遍历,外层的那个只是用来判断的,是同时进行的,因此实际上只是遍历了一遍数组,每递归遍历一躺数组数据就会减少,为了方便计算,不管它递归遍历了几趟都将数组中的数据个数默认遍历了n个,因此在找基准值的时间复杂度为O(N)

递归一趟的遍历n个数据 * 递归的趟数 = 时间复杂度 = O(NlogN),这是最坏的情况,将每递归遍历一次的数据个数都视为n的情况看待的,实际上每次递归遍历个数是减少的,也就是说实际它的时间复杂度达不到O(NlogN)

挖坑法找基准值(分界值也叫基准值)

思路:

创建专门挖坑的函数和参数:数组,数组的左右下标。⾸先创建坑将left的位置赋予hole,让分界值key将下标为hole的值挖走,此时hole所在的位置为空,再从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",也就是说它是left 和right轮流找值,而不是谁先全部找完 ,结束循环(当left = right时说明循环结束)后将最开始存储的分界值(每个区间的起始位置)放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)(只要是坑的下标位置所在的值都为空)

推导图:

核心代码:

c 复制代码
//挖坑法找基准值
int _QuickSort1(int* arr, int left, int right)
{
	int hole = left;
	int key = arr[hole];
	while (left < right)//这里可没有=号
	{
		while (left < right && arr[right] > key)//这里arr[right] > key不含=号是为了防止数组的值都一样时,会一直--
		{
			right--;
		}
		//此时找到比key小的值了
		arr[hole] = arr[right];
		hole = right;

		while (left < right && arr[left] < key)
		{
			left++;
		}
		//此时找到比key大的了
		arr[hole] = arr[left];
		hole = left;
	}
	//此时都找完了,处理left >= right情况
	arr[hole] = key;
	return hole;
}

特殊情况:当数组有序且为升序时,快排时间复杂度为O(N) --- 解决办法:三数取中法(后面会学到)

快排类似与二分查找,但是快排它是找到中间值,两边是同时递归排序(前面画图推导用等差数列求和时算的不对,原因是没有将两边看成同时递归的场景),二分查找则是找到中间值,排除另外一半去找另外一半的中间值继续排除一般,直到找到要找的值

也就是一个是分而治之 + 递归,二分查找则只有分而治之,但它们的时间复杂度都是logN

挖坑法left < right 没有=等号:是因为left与right不是同时自增自减的,所以不需要带=号,前面hoare时同时自增自减的要带上=号

双指针法找基准值(也叫lumoto前后指针法):

思路:创建三个变量:cur:初始情况下在下标为left

  • 1的位置
    ,prev :初始情况下在下标为left的位置,key :下标为left的位置
    cur指向的位置数据与key的值比较:
    当cur <= right时:
    若arr[cur] < arr[key]并且下标prev往后走一步后和下标cur 的位置不同时 :让下标为prev的值和下标为cur位置的值交换,再让cur++(cur往后走一步);
    若arr[cur] >= arr[key]:让cur++(cur往后走一步)
    直到cur > right 越界, 让下标为prev 和下标为key的值交换,此时prev就是基准值的下标返回prev即可
    推导图:

核心代码:

c 复制代码
int _QuickSort(int* arr, int left, int right)
{
	int prev = left, cur = left + 1;
	int key = left;

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

因此在if (arr[cur] < arr[key] && ++prev != cur) 加不加=号都一样

前面这三种方法找基准值都是利用了递归的思想,但由于递归需要不断地创建函数栈帧,会导致栈溢出问题,为解决这个问题:我们可以使用非递归版本的快排

非递归版本的快排:

实现他需要用到数据结构:栈

思路:

  1. 先创建栈并将其初始化,将数组中的左右区间的下标left 和right 入栈,这里先入右再入左
  2. 当栈不为空时取两次栈顶元素(取一次就将栈顶元素出栈),创建两个变量begin 和 end 分别存放第一次取栈顶元素left得值和第二次取栈顶元素right的值(也就是将数组的左右区间值从栈里取出来并保存)
  3. 用三个方法选择其一去找基准值keyi,再根据基准值和公式:
    左区间:[begin,keyi-1] 右区间:[keyi+1,end]
    划分左右区间并入栈(先入左区间和右区间都可以,这里先入右区间为例):
    若keyi + 1 < end 时,将右区间的keyi + 1(右边)和end (左边)依次入栈
    若key - 1 > begin 时,将左区间的key -1(右边) 和begin (左边)依次入栈
    这里需要注意的是:入左区间和右区间的顺序可以换,但是入其中一个区间的左右下标不能改变必须是先入右下标,再入左下标
    直到栈为空停止循环,并将创建的栈进行销毁
    推导图:

    核心代码:
c 复制代码
//非递归版本快排
void QuickSortNonR(int* arr, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		//取栈顶元素 -- 取两次
		int begin = StackTop(&st);
		StackPop(&st);

		int end = StackTop(&st);
		StackPop(&st);

		//根据取的栈顶元素找基准值
		int prev = begin;
		int cur = begin + 1;
		int keyi = begin;

		while (cur <= end)//若大于说明cur越界了
		{
			if (arr[cur] < arr[keyi] && ++prev != cur)//这里必须是++prev:先往后走一步,再与cur比较是否相等
			{
				Swap(&arr[cur], &arr[prev]);
			}
			cur++;
		}
		Swap(&arr[prev], &arr[keyi]);
		keyi = prev;

		//找到基准值keyi,进行划分左右区间并将入栈,循环模拟递归进行排序
		//左区间:[begin, keyi - 1]
		//右区间:[keyi + 1, end]

		if (begin < keyi - 1)
		{
			
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
		//这里先入左区间,右区间都可以,但是要左右下标都要按属顺序入(先入右端,再入左端 原因:栈是先入栈的后出栈,取栈顶元素时需要先取左端,再取右端),不能修改
		if (keyi + 1 < end)
		{
			
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}

	}
	StackDestroy(&st);
}

排序提问方式:问你排序有那些,时间复杂度各个比较一下,能不能手撕一下这些排序代码

学习后及时复习,做到想到啥知识能够随口说出!!!

计算机工作工资高不是白来的,需要我们做到持续学习的状态,因此要提前去找实习,以后工作了会加班,工资不同有不同的活法,因此要不断的去学习敲代码

面试造飞机,工作拧螺丝,现在学习是为了让你掌握纸上的知识点,以后拧螺丝,是为了让我们实实在在的掌握实际业务是怎样运行的,它会让你的视野更加扩展

秋招的拿到的薪资与你现在敲的代码量是直接相关的!!!

归并排序

思想:归并排序是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;**即先使每个⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并

归并排序核⼼步骤:

动态申请临时数组tmp:用来保存分解后要合并的有序的值;

分解:创建子函数专门用来递归分解和合并操作:

子函数传四个参数:原数组arr, 数组最左边left,最右边right,动态申请的数组tmp ,

调用子函数并传相关参数

最后程序运行快结束时记得释放

先去判断left >=right是否满足:满足则为无效区间或者该区间只有一个数据,不用分解递归了,直接返回

不满足:先通过left 和right 找中间值mid,再利用中间值找到左右区间[left, mid] [mid + 1, right]继续递归左右区间进行分解区间里的值

注意这里mid与前面的基准值keyi不一样:前面的基准值keyi已经知道位置了,不需要改变,因此也不用将它放到左右区间里;这里的mid在划分时要划分到左区间里

合并:

创建begin1:存放左区间的left,end1:存放右区间的mid, begin2:存放右区间的mid + 1, end2:存放右区间的right

创建index作为数组tmp的下标初始状态下存放下标begin1;(初始状态下是0,但还有其他情况:当我们还要遍历下一个数组的左区间,下一个数组的左区间的下标不一定是0)

开始将左右区间进行合并:

当begin1 <= end1(为有效区间,等于时也是有效区间)&& begin2 <= end2 时

下标begin1 和begin2比大小,如果begin1小就把下标bigin2的值放到数组tmp[index]中,并让index往后走一步,且存放下标begin1的值也要往后走一步(index++是为了方便下次往后入值,begin++是为了存放完该值后方便下次比较该区间的下一个值);begin2小,就存begin2的值,然后让begin2++

遍历完后此时要么右区间越界,要么左区间越界,将没有越界的继续往数组tmp里放,放完后让index++,且对应的下标begin1++/begin2++方便存放往后存放下一个值

此时已经将数据排好序放到tmp数组中了,要求合并的是有序的区间,但继续往回合并时合并的是arr那个没有顺序的区间,而且tmp的变化不会影响arr,所以需要将tmp拷贝到arr上,然他下次循环合并时合并的时有序的,从arr的left开始循环拷贝,直到arr将下标right的最后一个值拷贝完成

推导图:

核心代码:

c 复制代码
void QuickSortNonR(int* arr, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		//取栈顶元素 -- 取两次
		int begin = StackTop(&st);
		StackPop(&st);

		int end = StackTop(&st);
		StackPop(&st);

		//根据取的栈顶元素找基准值
		int prev = begin;
		int cur = begin + 1;
		int keyi = begin;

		while (cur <= end)//若大于说明cur越界了
		{
			if (arr[cur] < arr[keyi] && ++prev != cur)//这里必须是++prev:先往后走一步,再与cur比较是否相等
			{
				Swap(&arr[cur], &arr[prev]);
			}
			cur++;
		}
		Swap(&arr[prev], &arr[keyi]);
		keyi = prev;

		//找到基准值keyi,进行划分左右区间并将入栈,循环模拟递归进行排序
		//左区间:[begin, keyi - 1]
		//右区间:[keyi + 1, end]

		if (begin < keyi - 1)
		{
			
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
		//这里先入左区间,右区间都可以,但是要左右下标都要按属顺序入(先入右端,再入左端 原因:栈是先入栈的后出栈,取栈顶元素时需要先取左端,再取右端),不能修改
		if (keyi + 1 < end)
		{
			
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}

	}
	StackDestroy(&st);
}
void _MergeSort(int* arr, int left, int right, int* tmp)
{
	//分解
	if (left >= right)//是left >= right,不是<=
	{
		return;
	}
	//找中间值
	int mid = (left + right) / 2;
	//继续递归左区间和右区间
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);
//////////////////////////////////////////////////
	//合并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1,  end2 = right;
	int index = begin1;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}
	}
	//此时比较完了,肯定有一个区间里还有值没有越界  --- 要么begin1越界,要么begin2越界
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}

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

	for (int i = left; i <= right; i++)
	{
		arr[i] = tmp[i];
	}
}
void MergeSort(int* arr, int n)
{

	int* tmp = (int*)malloc(sizeof(int) * n);
	/////////////
	_MergeSort(arr, 0, n - 1, tmp);
	/////////////
	free(tmp);
}

这里快排排序的实际慢的原因:在第一次递归的快排基础上再调用一次非递归版本的快排:运行实际长原因:第一次拍完后为有序且为升序,此时在调用一次非递归版本的快排的化会触发非递归版本排序的缺陷

通过代码的性能测试测试: 希尔、堆、快排性能差不多

归并排序的空间复杂度与时间复杂度

各个排序简单说明:

直接选择排序:在茫茫人海中找到最好的男朋友/女朋友,之最:max 和mini 就是直接选择排序

直接插入排序:打牌前拿起一张牌往已经有序的牌里插入张牌就是直接插入排序

当数组中的数据都是大的在前小的在后,这时直接插入排序时间复杂度最差为O(N^2),通过希尔排序进行优化:先对该数组的数据进行分组预排序,最后再进行排序这样就达到小的数据在前,大的在后从而让时间复杂度降低了

非比较排序:

计数排序:

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

直接把数据的内容当作它该放的位置号。

举个例子:

想象一个超大号的文件柜,有100个格子(0到99号)。

用直接定址法:

如果你的学号是 25,你的档案就直接放进 25号格子。

如果学号是 88,档案就直接放进 88号格子。

优点:找东西飞快!想知道学号88的档案在哪?直接打开88号格子就行,不用到处翻。

致命缺点:如果只有50个学生,但学号范围是0-99,你就得准备100个格子的文件柜,浪费了50个空格子。如果学号是20241111这样的大数,那你得准备一个超超大、有几千万个格子的文件柜,这显然不现实。

所以,它只适合存放数量不多、并且编号很紧凑的数据

思想:

统计每个整数在待排序数组中出现的次数。

创建新数组:

新数组中对应的值是在待排序数组中每个数据出现的次数,新数组值的下标是待排序数组中的值,

在往新数组中存放数据时需要与待排序数组中的值作为下标,并对应着将该值出现的次数作为新数组对应下标的元素

最后再将该新数组元素值是几就将其下标打印几次,最后就变成有序的数组了

新数组的大小如何确定? ---- 用最大值就可以了吗?

当出现负数怎么办 ---- 用绝对值,若出现绝对值相同的值怎么办?

若原数组中的值为100000 、100001、100002、100004、100005,难道我们要申请100000个数空间创建数组吗? --- 不可以这样会造成前面申请的空间没有用造成浪费

针对以上问题解决方式如下:

新数组的大小:原数组的 最大值 - 最小值 + 1

往新数组count中插入原数组元素出现的次数:

原数组元素作为新数组中的下标,原数组出现的元素在新数组中的下标:原数组中的元素值 - 原数组中最小值,现在新数组中剩下的元素都为0(意味着在原数组中没有出现)

count 数组的值: count[arr[i] - min]++

核心代码:

c 复制代码
void CountSort(int* arr, int n)
{
	//根据原数组中最大值和最小值,确定新数组的大小并创建
	int max = arr[0], min = arr[0];
	for (int i = 0; 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);
	}

	////count数组初始化
	//for (int i = 0; i < range; i++)
	//{
	//	count[i] = 0;
	//}
	//方法2:用memset也可以  -- 加头文件memory
	memset(count, 0, sizeof(int) * range);//参数: 参数名,要初始化的值, 数组大小单位字节
	//往count放原数组出现的次数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
		
	}
	
	//去count中的数据,往arr里放
	int index = 0;
	for (int i = 0; i < range; i++)
		//时间复杂度:O(range + N) -- 外层循环一直在遍历,内层的循环遇到0的直接就跳出循环了,实际上只循环count中是将不为0的,也就是count的个数n(这里假设count的个数为n)
		//空间复杂度:O(range) --- 动态申请了range个空间的大小
	{
		while (count[i]--)
		{
			arr[index++] = i + min;//注意这里不是count[i] + min
		}
	}
}
复制代码
	时间复杂度:O(range + N) -- 外层循环一直在遍历,内层的循环遇到0的直接就跳出循环了,
	实际上只循环count中是将不为0的,也就是count的个数n(这里假设count的个数为n)
	空间复杂度:O(range) --- 动态申请了range个空间的大小

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

稳定性

假设有一个数组,数组中相同元素的它们的相对次序发生改变则为不稳定,不改变则为稳定

排序算法复杂度及稳定性分析

在判断各自排序算法是否稳定时,不要去死背,忘了时通过举例子配合算法思想去判断是否稳定

归并排序稳定:数组进行分解时它是按照一块一块来分布的,最后合并时也是一块一块合并的,若有相同的元素,它们的顺序不变,所以它是稳定的

test.c

c 复制代码
#include"sort.h"
// 测试排序的性能对⽐ --- 这里在调试时要将已经实现的进行测试对比,没实现的将相应的代码进行注释
void TestOP() 
{
	srand(time(0));
	const int N = 100000;
	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();
		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];
	}
	//clock -- 计算程序运行的时间

	int begin7 = clock();
	//BubbleSort(a7, N);
	int end7 = clock();//测试降序的希尔与直接插入排序区别,将各个的a几都改成a7 -- 这里话需要测试其他的,所以需要将这里改回来

	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(a5, 0, N - 1);
	//QuickSortNonR(a5, 0, N - 1);
	int end5 = clock();

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

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = 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);
	printf("BubbleSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}
int main()
{
	////////排序测试  
	//int arr[] = { 5,3,9,6,2,4,7,1,8 };
	////int arr[] = { 5,3,9,6,2 };//测试后:9 2 3 6 5
	//int n = sizeof(arr) / sizeof(int);
	//printf("测试前:");
	//PrintArr(arr, n);
	////InsertSort(arr, n);
	////SelectSort(arr, n);
	////QuickSort(arr, 0, n - 1);//n-1数组是从0开始的因此在数据有效个数-1,才是最后一个数据的下标
	////QuickSortNonR(arr, 0, n - 1);
	////MergeSort(arr, n);
	//CountSort(arr, n);
	//printf("测试后:");
	//PrintArr(arr, n);

	////性能测试:
	TestOP();

	return 0;
}

sort.c

c 复制代码
#include"sort.h"
#include"stack.h"
//直接插入排序
//思路:创建两个整型变量 end:作为有序数组最后一个元素的下标,tmp:保存与end比较的end + 1的元素值
//让arr[end]与tmp比较,若arr[end] > tmp :当end >= 0将arr[end]元素挪到arr[end + 1]处,再让end--,直到end < 0时,然后将tmp保存的值赋给
//arr[end + 1]处,若arr[end] < tmp:说明是有序的,直接break跳出循环。循环遍历到数组结束
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)//这里数组最后一次遍历是n-1,不是n,若为n,则最后一次保存tmp时会导致越界
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}
//打印数组
void PrintArr(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 AdjustDown(int* arr, int parent, int n)//这里修改成int* 对应该数组
{
	int child = parent * 2 + 1;//左孩子
	//while (parent < n)
	while (child < n)
	{
		//小堆:找左右孩子中找最小的
		//大堆:找左右孩子中找大的
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;

		}
		else
		{
			break;
		}
	}
}

//这里在调用Swap 和 AdjustDown 函数时,需要先将定义放到函数调用的上面
//冒泡排序
//时间复杂度:0(N^2)
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		int exchange = 0;
		for (int j = 0; j < n - i - 1; j++)
		{
			//升序
			if (arr[j] < arr[j + 1])//为了测试希尔排序比直接插入排序性能好,这里调成最坏的情况:降序试试,将前面的测试性能代码也改变成与冒泡排序相关的a7
			{
				exchange = 1;
				Swap(&arr[j], &arr[j + 1]);
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

//堆排序
//时间复杂度为O(n*logn)




void HeapSort(int* arr, int n)
{
	//建堆
	//升序---大堆
	//降序----小堆
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(arr, i);//向上调整算法建堆时间复杂度为NlogN,向下调整算法时间复杂度为O(N)这里堆排序用向下调整算法实现
	//}
	//向下调整算法建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);
	}

	//循环将堆顶数据跟最后位置(会变化,每次减少一个数据)的数据进行交换
	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}
//希尔排序时间复杂度:O(N^1.3)
void ShellSort(int* arr, int n)
{
	//当gap > 1 时,预排序
	//当gap = 1时,直接插入排序
	int gap = n;
	while (gap > 1)//条件里不含=1原因:若包含=1,当n = 6时,gap取余两次后gap = 0,一直+1会造成死循环
	{
		gap = gap / 3 + 1;// /2与/3中 /3好,因为:/3分的组数少;+1:原因:若n = 6时,gap = 0,没法进行排序了,所以+个1。
		//for (int i = 0; i < gap; i++)//这里四个循环,太麻烦了,修改下,将这个循环删除后,改变里面那个循环条件: i += gap 改成:i++
		//{
			for (int i = 0; i < n - gap; i++)//修改成i++后,原本是一组一组的排序,现在是排完第一组第一个后再去排第二组第一个,以此类推。。。

				//前面学的直接插入排序时n - 1,这里是每隔gap个数据分为一组,所以数组最后一个数据为tmp =  n - gap - 1 +gap = n - 1 	
			{
				int end = i;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (arr[end] > tmp)
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				arr[end + gap] = tmp;
			}
		}
	//}
	}

//void SelectSort(int* arr, int n)
//{
//	for (int i = 0; i < n; i++)
//	{
//		//int begin = i;//没有begin也可以
//		int mini = i;
//		for (int j = i + 1; j < n; j++)
//		{
//			if (arr[j] < arr[mini])
//			{
//				mini = j;
//			}
//		}
//		//Swap(&arr[begin], &arr[mini]);
//		Swap(&arr[i], &arr[mini]);
//	}
//}

void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;

	while (begin < end)
	{
		int max = begin;
		int mini = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (arr[i] > arr[max])
			{
				max = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
	
		}
		//此时mini max 各自找到了最大值,最小值的下标
		//避免maxi begini都在同一个位置,begin和mini交换之后,maxi数据变成了最小的数据,这里将max与mini交换位置
		if (max == begin)
		{
			mini = max;
		}
		Swap(&arr[mini], &arr[begin]);
		Swap(&arr[max], &arr[end]);

		++begin;
		--end;
	}
}

//hoare版本找基准值函数
int _QuickSort1(int* arr, int left, int right)
{
	int keyi = left;//left初始状态下是为下标为0的。keyi是在left的位置上,而不是单纯的=0
	++left;
	while (left <= right)
	{
		//right找到比基准值小的 / 等于?
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}
		//left找到比基准值大的 / 等于?
		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}
		
		//right left 判断交换值
		if (left <= right)
		{
			Swap(&arr[left++], &arr[right--]);
		}
	}
	Swap(&arr[right], &arr[keyi]);
	return right;
}
//挖坑法找基准值
int _QuickSort2(int* arr, int left, int right)
{
	int hole = left;
	int key = arr[hole];
	while (left < right)//这里可没有=号
	{
		while (left < right && arr[right] > key)
		{
			right--;
		}
		//此时找到比key小的值了
		arr[hole] = arr[right];
		hole = right;

		while (left < right && arr[left] < key)
		{
			left++;
		}
		//此时找到比key大的了
		arr[hole] = arr[left];
		hole = left;
	}

	//此时都找完了,处理left >= right情况
	arr[hole] = key;
	return hole;
}
//lomuto前后指针法找基准值
int _QuickSort(int* arr, int left, int right)
{
	int prev = left, cur = left + 1;
	int key = left;

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

void QuickSort(int* arr, int left, int right)
{
	//判断区间是否为有效区间
	if (left >= right)
	{
		return;
	}
	//找基准值keyi
	int keyi = _QuickSort(arr, left, right);
	//递归左子序列[left, keyi - 1]
	QuickSort(arr, left, keyi - 1);
	//递归右子序列[keyi + 1, right]
	QuickSort(arr, keyi + 1, right);	
}
//非递归版本快排
void QuickSortNonR(int* arr, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		//取栈顶元素 -- 取两次
		int begin = StackTop(&st);
		StackPop(&st);

		int end = StackTop(&st);
		StackPop(&st);

		//根据取的栈顶元素找基准值
		int prev = begin;
		int cur = begin + 1;
		int keyi = begin;

		while (cur <= end)//若大于说明cur越界了
		{
			if (arr[cur] < arr[keyi] && ++prev != cur)//这里必须是++prev:先往后走一步,再与cur比较是否相等
			{
				Swap(&arr[cur], &arr[prev]);
			}
			cur++;
		}
		Swap(&arr[prev], &arr[keyi]);
		keyi = prev;

		//找到基准值keyi,进行划分左右区间并将入栈,循环模拟递归进行排序
		//左区间:[begin, keyi - 1]
		//右区间:[keyi + 1, end]

		if (begin < keyi - 1)
		{
			
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
		//这里先入左区间,右区间都可以,但是要左右下标都要按属顺序入(先入右端,再入左端 原因:栈是先入栈的后出栈,取栈顶元素时需要先取左端,再取右端),不能修改
		if (keyi + 1 < end)
		{
			
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}

	}
	StackDestroy(&st);
}
void _MergeSort(int* arr, int left, int right, int* tmp)
{
	//分解
	if (left >= right)//是left >= right,不是<=
	{
		return;
	}
	//找中间值
	int mid = (left + right) / 2;
	//继续递归左区间和右区间
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);
//////////////////////////////////////////////////
	//合并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1,  end2 = right;
	int index = begin1;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}
	}
	//此时比较完了,肯定有一个区间里还有值没有越界  --- 要么begin1越界,要么begin2越界
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}

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

	for (int i = left; i <= right; i++)
	{
		arr[i] = tmp[i];
	}
}
void MergeSort(int* arr, int n)
{

	int* tmp = (int*)malloc(sizeof(int) * n);
	/////////////
	_MergeSort(arr, 0, n - 1, tmp);
	/////////////
	free(tmp);
}

void CountSort(int* arr, int n)
{
	//根据原数组中最大值和最小值,确定新数组的大小并创建
	int max = arr[0], min = arr[0];
	for (int i = 0; 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);
	}

	////count数组初始化
	//for (int i = 0; i < range; i++)
	//{
	//	count[i] = 0;
	//}
	//方法2:用memset也可以  -- 加头文件memory
	memset(count, 0, sizeof(int) * range);//参数: 参数名,要初始化的值, 数组大小单位字节
	//往count放原数组出现的次数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
		
	}
	
	//去count中的数据,往arr里放
	int index = 0;
	for (int i = 0; i < range; i++)
		//时间复杂度:O(range + N) -- 外层循环一直在遍历,内层的循环遇到0的直接就跳出循环了,实际上只循环count中是将不为0的,也就是count的个数n(这里假设count的个数为n)
		//空间复杂度:O(range) --- 动态申请了range个空间的大小
	{
		while (count[i]--)
		{
			arr[index++] = i + min;//注意这里不是count[i] + min
		}
	}
}

sort.h

c 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include"stack.h"
#include<memory.h>
void InsertSort(int* arr, int n);
void PrintArr(int* arr, int n);
void HeapSort(int* arr, int n);
void BubbleSort(int* arr, int n);
void ShellSort(int* arr, int n);
void SelectSort(int* arr, int n);
void QuickSort(int* arr, int left, int right);
void QuickSortNonR(int* arr, int left, int right);
void MergeSort(int* arr, int n);
void CountSort(int* arr, int n);
相关推荐
小年糕是糕手27 分钟前
【C++】类和对象(二) -- 构造函数、析构函数
java·c语言·开发语言·数据结构·c++·算法·leetcode
kupeThinkPoem1 小时前
跳表有哪些算法?
数据结构·算法
前端小L1 小时前
图论专题(二十一):并查集的“工程应用”——拔线重连,修复「连通网络」
数据结构·算法·深度优先·图论·宽度优先
前端小L2 小时前
图论专题(二十三):并查集的“数据清洗”——解决复杂的「账户合并」
数据结构·算法·安全·深度优先·图论
啊董dong2 小时前
课后作业-2025年11月23号作业
数据结构·c++·算法·深度优先·noi
dlz08363 小时前
从架构到数据结构,到同步逻辑,到 show run 流程优化
数据结构
jllws13 小时前
数据结构_字符和汉字的编码与查找
数据结构
学困昇3 小时前
C++11中的包装器
开发语言·数据结构·c++·c++11
weixin_4577600012 小时前
Python 数据结构
数据结构·windows·python