数据结构《排序》

article/2025/8/14 20:58:50

在之前数据结构之算法复杂度章节中我们学习了复杂度相关的概念,这就使得懂得如何来区分算法的好坏,在之前C语言专题中在指针的学习时我们了解了冒泡排序,之后再数据结构的二叉树章节中我们又学习了堆排序,其实排序不止这两种,还有直接插入排序、快速排序等,在本篇中我们就会来相信学习各种的排序,还会对这些排序方法进行复杂度的分析来分析出哪些是好的排序方法,接下来就开始排序篇章的学习吧!!  !

863c19ad485f4fe98d645d580158df5c.jpeg


1.排序概念及运用

在学习各种排序之前我们先来了解排序的相关概念和在现实中的运用

1.1概念

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

1.2 运用

排序广泛运用在各种场景中,就例如计算机中的文件系统的文件排序就是根据时间、文件名、大小、修改日期等属性进行排序,以下的文件就是按照时间排序的

d6dde7825ef849c0973c3a1d9ac862c4.png

再比如在购物网站中也非常频繁的用到排序,就比如在购物网站要购买电脑时就可以看到会出现按评论数排序、按价格排序等的排序方法;有了这些不同的排序就可以让购物者更快速的选择到心仪的商品
f0519dae0d974855a7b732e410ed5fc8.png

1.3 常见排序算法

在以上了解的排序的概念和运用后接下来我们就来了解常见的排序算法有哪些
7ca382be180d4656a39e8dfeb2daa09a.png

通过以上的图示就可以看出排序可以分为4大类,在这些排序当中我们之前了解过了冒泡排序和堆排序,接下来我们就来深入理解其他排序

2. 实现常见排序算法

2.1插入排序 

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

de35b2c478fa497aabafe390619cbc1c.png

就比如在打扑克时,在整理牌的时候就用到了插入排序的思想,我们拿到一张牌后就会将这张牌插入到合适的位置

2.1.1直接插入排序  

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

32befbafc87f4deabbf2a0fe8fae1a79.gif

例如在以下的示例当中要将其排序为升序要如何实现呢?
e862db5ff8fd437c8a68ea649f1ca2ba.png

要将以上的数据排为升序先定义一个变量end一开始实现第一个数据,定义变量tmp存储下一个数据的值,如果end指向的数据大于tmp就将end指向的数据值赋值给end+1的位置,之后将end-1继续将该位置的数据与tmp比较重复以上的操作;若不大于就让end+1,将tmp也改变为end+1指向的数据的值

例如以上示例中在将4插入到前面的数据中时会进行以下操作

0cb48ffc00b847eca7f82c63d13e03f0.png

abc80e00a85d4864b748d53076baa6bc.png

之后其他数据的插入也是按照以上的方法来实现

代码实现

完成了直接插入排序方法的分析后接下来就来实现该排序算法的代码实现

先在Sort.h内完成函数的声明

//直接插入排序
void InsertSort(int* arr, int n);

将函数命名为InsertSort,函数的参数有两个,第一个是数组名,第二个是数组的元素个数  

之后就是在Sort.c内进行函数的定义

以下使用for循环来遍历数组,在此让变量tmp=arr[end+1]这样就可以使得tmp的值为end下标下一位的值,在此由于end+1的值最大只能到n-2,否则就造成数组的越界访问,因此在for循环的判断条件为i<n-1

在for循环内再定义一个while循环来实现tmp内的数据与之前已排序好的数据进行比较,在出了while循环后就将tmp内的数据插入到数组下标为end+1的位置

//直接插入排序
void InsertSort(int* arr, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;//值不大于tmp内的值就跳出while循环}}arr[end + 1] = tmp;}
}
复杂度分析

在实现了直接插入排序的代码后接下来我们来分析这种算法的复杂度
670ee5bc17da4bfe9be1774ef21e1042.png
 

在外层循环为n-1次,内层循环在第一个外层循环时最坏情况循环一次;在第二个外层循环时最坏情况循环两次,因此内层的循环次数就为一个等差数列,总的while指向次数就是等差数列求和,最终算法的时间复杂度就为eq?O%28N%5E%7B2%7D%29,并且当要排序的数据一开始就是有序的,那么时间复杂度就为eq?O%28N%29

因此在直接插入排序中元素集合越接近有序,直接插入排序算法的时间效率越高

因为在该排序算法中没有申请新的内存空间,因此空间复杂度为eq?O%281%29

2.1.2希尔排序

在以上的直接插入排序中当要排序的数据一开始是降序的时候这时排序的效率就为最差,时间复杂度为eq?O%28N%5E%7B2%7D%29,那有什么方法能优化直接插入排序这种不足呢?

希尔排序就是直接插入排序优化后得来的,在排序过程中我们会加入以下操作:
先选定⼀个整数(通常是gap = n/3+1),把待排序文件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进行排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进行插入排序,当gap=1时,就相当于直接插入排序。

注:在希尔排序中在gap为1之前的排序称为预排序 ,gap为1时就是直接插入排序

例如以下的示例使用希尔排序的思想过程就如下所示

第一趟排序中如果gap为5,那么就使得将距离为5的数据化为一组,将每组都排序好就变为以下形式
38c75afa327b427e882e86ce0e41652d.png

之后如将以上第一趟排序好的数据再按照gap为2进行第二趟排序,那么就使得距离为2的数据化为一组,将每组排序好就变为以下形式
376c4c538d4743448e0ce62757b5bc99.png 最后再完成以上的排序后相比原来的数据,以上排序之后就变为基本有序,最后再进行一次gap为1的排序就能将原数据变为升序

8e88f13d42684713a80a2c62986ee713.png

 完成了以上的分析就可以试着来实现希尔排序的代码了

代码实现

先在Sort.h内完成函数的声明

//希尔排序
void ShellSort(int* arr, int n);

将函数命名为ShellSort,函数的参数有两个,第一个是数组名,第二个是数组的元素个数  

之后就是在Sort.c内进行函数的定义

假设gap值为3,那么以下代码的第一个for循环就将原数组分为3部分,在第二个for循环内就进行每个部分的排序,在该部分中基本与直接插入排序基本相同,不过在希尔排序中每部分每个元素相距gap,因此以下原来直接插入排序中加减一的部分要改为加减gap

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

虽然以上代码确实符合希尔排序,但是以上代码是用3个循环嵌套的,是否能优化这点不足呢?

在之前实现希尔排序的代码中我们是将分组后各个部分的排序分开进行,其实可以直接将不同部分的排序同时进行,这就只需要将以上代码内的第一个for循环删除再将第二个for循环内的调整部分改为i++,并且在这些代码外层再包含一层while循环,该循环来控制gap的值,在此gap在/3后再加一是为了保证最后一次gap一定为1

以下就是将以上代码修改之后的希尔排序最终的代码

//希尔排序
void ShellSort(int* arr, int n)
{int gap = n;while (gap>1){gap = gap / 3 + 1;//保证gap最后一次一定为1for (int i = 0; i < n-gap; i++){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;}}
}

在以上实现了希尔排序的代码后你可能会想希尔排序是优化直接插入排序的不足,那么希尔排序怎么相比直接插入排序还多了一层循环,那么时间复杂度不应该提高吗?

其实不是这样的,希尔排序相比直接插入排序的时间复杂度是减少很多的,原因是希尔排序在预排序中就会基本让数组中数据大的在前,小的在后,这就会使得数据基本有序在预排序结束之后直接插入排序时排序的次数会大大减少,这就会使得时间复杂度大大减低
 

复杂度分析

在实现了希尔排序的代码后接下来就来细致的分析希尔排序的复杂度

首先来分析希尔排序外层循环的时间复杂度,在第一次进入循环时gap=n/3,之后第二次就变为n/3/3,之后不断进入循环直到gap为1时就跳出该while循环,因此进入循环的公式就可以的出3^{x}=n,进入循环次数就为\log_{3}n 

分析了外层循环接下来继续来分析内存循环

如以上示例当n为9时如果gap为3,那么每组就为3个元素,将这3个元素排序好最坏的情况次数就为1+2 ,移动总数就为9

假设⼀共有n个数据,合计gap组,则每组为n/gap个;在每组中,插入移动的次数最坏的情况下为
1 + 2 + 3 + .... +(\frac{n}{gap}-1)⼀共是gap组,因此:
总计最坏情况下移动总数为: gap *(1+2+3+....+(\frac{n}{gap}-1))
gap取值有(以除3为例):n/3 n/9 n/27 ...... 2 1

• 当gap为n/3时,移动总数为:\frac{n}{3}*(1+2)=n
• 当gap为n/9时,移动总数为:\frac{n}{9}*(1+2+3+....+8)=\frac{n}{9}*\frac{8(1+8)}{2}=4n
• 最后⼀躺,gap=1即直接插入排序,内层循环排序消耗为n

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

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

希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定。《数据结构(C语言版)》--- 严蔚敏书中给出的时间复杂度为:

 

2.2选择排序 

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

2.2.1直接选择排序

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

例如在以下的示例当中要将其排序为升序要如何实现呢?

首先创建两个变量begin和endbegin为一开始数组第一个元素得下标,而end一开始为数组最后的下标再创建一个变量min来表示数组中最小值的下标,并且一开始让其初始化时与begin值相同,之后通过遍历数组找到数组中得最小值将元素下标赋值给min,之后交换下标begin和下标min的数据,完成以上操作后将begin加一。一直重复以上操作直到begin值和end相同时

 完成了以上的分析就可以试着来实现希尔排序的代码了

代码实现

先在Sort.h内完成函数的声明

//直接选择排序
void SelectSort(int* arr, int n);

将函数命名为SelectSort函数的参数有两个,第一个是数组名,第二个是数组的元素个数  

 之后就是在Sort.c内进行函数的定义

//直接选择排序
void SelectSort(int* arr, int n)
{int begin = 0;int end = n - 1;while (begin < end){int min = begin;for (int i = begin + 1; i <= end; i++){if (arr[i] < arr[min]){min = i;}}Swap(&arr[min], &arr[begin]);++begin;}
}

以上代码确实可以实现直接选择排序,但以上的算法在一次循环中只能找到最小值其实可以对以上的算法进行优化让其在一次循环能找到最大值max和最小值min,这时只需要将完成找大和找小后交换下标end和下标max的数据,在完成操作后将begin的加一同时将end减一

以下就是优化后的代码 

注意使用这种算法时会有一种特殊的情况要处理,就是当在使用Swap交换前min下标和begin相同;max和end下标相同,例如以下示例
 

在以上的数组中如果是使用之前的算法就需要将下标为begin的元素和下标为下标为min的元素交换,这时进行完之后就变为了1 2 3已经变为了升序,当之后下标为end的元素和下标为下标为max的元素交换这就会使得数组又变为3 2 1

通过以上的示例就可以发现当begin和max相等且end和min相等时,交换两次就会使得这次交换失效,在此解决方法是当begin等于max时;将max移到min的位置也就是将min赋值给max

注:在以上特殊情况判断时如果在if判断部分写的是max==begin,那么在if语句内部就是max=min如果在if判断部分写的是min==end,那么在if语句内部就是min=max,如果将这两种交叉使用就会在排序结果无法预测

//直接选择排序
void SelectSort(int* arr, int n)
{int begin = 0;int end = n - 1;while(begin<end){int min = begin;int max = begin;for (int i = begin + 1; i <= end; i++){if (arr[i] < arr[min]){min = i;}if (arr[i] > arr[max]){max = i;}}if (max ==begin){max = min;}Swap(&arr[min], &arr[begin]);Swap(&arr[max], &arr[end]);++begin;--end;}
}

复杂度分析 

在实现了直接选择排序的代码后接下来就来细致的分析直接选择排序的复杂度

通过以上代码的分析就可以看出该算法代码是由两个循环嵌套的,第一次内层循环次数为n-2次,第二次为n-4次,直到最后2次,因此总的内层循环执行次数就为等差数列求和,最终可求出直接选择排序的时间复杂度为O(N^{2})

该排序由于没有申请新的内存空间,因此空间复杂度为O(1)

2.2.2堆排序

堆排序相关的介绍和讲解在数据结构之《二叉树》(中),因此在本篇就不再对该算法进行讲解

2.3交换排序

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

2.3.1冒泡排序

冒泡排序相关的介绍和讲解在C语言的深入理解指针(2),冒泡排序的复杂度讲解在算法复杂度,因此在本篇就不再对该算法进行讲解

2.3.2快速排序

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

在快速排序中过程就如以上图示是不断的在找几尊值,当被分到每个区间都只存在基准值时就将数据排序好了。通过以上图示还可以看出快速排序在不断找基准值过程中展现出的结构就如二叉树一样

了解了快速排序的大体执行流程后,那么快排的代码该如何实现呢?

接下来我们就来分析看看首先是快速排序函数的参数和之前实现的排序有所不同,快速排序一开始是在整个数组中找一个基准值当由于之后要继续划分多个区间因此需要有区间的左右值,因此快速排序的参数除了数组名外还有有数组的起始和结束对应的下标

//快速排序
void QuickSort(int* arr, int left, int right);

快速排序实现主框架:

在快速排序中我们使用的是递归的方法来实现各区间中基准值的寻找,递归的限制条件是当left大于right,也就是当左区间大于等于右区间时就停止递推

在此QuickSort中的_QuickSort是该函数的子方法,是用于找基准值

//快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分//找基准值int keyi=_QuickSort3(arr, left, right);//[left,keyi-1]  [keyi+1,right]QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

在快速排序中找基准值的方法有多种,也就是_QuickSort的实现方法有多种,接下来将一一介绍

 hoare版本

算法思路 :
(1)创建左右指针,确定基准值
(2)从右向左找出比基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环

例如以下示例: 

 

在以上数组当中使用hoare版本找基准值的方法流程如下所示
首先先定义变量keyi为基准值下标,一开始将keyi初始化值为left的值,之后将left+1,之后left向右找比5大的值下标,right向左找比基准值小的值下标,之后交换left下标和right下标的元素

 

之后继续重复以上操作最终left等于right就停止操作,这时再将下标keyi的值和下标right的值交换,这时下标为right的元素就为基准值 

该过程完整流程如下所示:

完成了hoare版本找基准值的示例分析接下来就试着来实现代码

//找基准值——hoare版本
int _QuickSort(int* arr, int left, int right)
{int keyi = left;left++;while (left <= right){while (left <= right && arr[left] < arr[keyi]){left++;}while (left <= right && arr[right] > arr[keyi]){right--;}if (left <= right){Swap(&arr[left++], &arr[right--]);}}Swap(&arr[right], &arr[keyi]);return right;
}

对于以上的代码你可能会有以下的疑问

问题1:为什么left 和 right指定的数据和key值相等时也要交换?

要解答这个以上这个问题来看下面这个示例

在以上的数组当中如果在找基准值时left和right的数据相等时不交换就会使得在right从右向左找小过程中right一直到key的位置,这时基准值就是用来key下标的数据

这时就会使得每次找到基准值都为最左侧的元素,这回就会使得找基准值进行n次,这时程序的效率就会很低 

那如果找基准值时left和right的数据相等时交换又会是什么样的情况呢?
 

这种情况时left和right第一次移动后图示如下

最终找到基准值的图示如下所示

这时按照这种方法之后找基准值的次数一定不会在为n次,以上示例解释为什么left和right指定的数据和key值相等时也要交换

问题2:为什么跳出循环后right位置的值⼀定不大于key?

要解答这个以上这个问题来看下面这个示例

在以上的示例当中若代码当中判断部分都是left<right那么最终基准值就为6,但这时就出现问题了
6的左边有大于基准值的也有小于基准值的这就不符合对于取基准值的要求 

在来看代码当中判断部分都是left<=right以上示例最终基准值就变为以下形式

这时在基准值6的左边都是小于基准值的,在基准值6的右边都是大于基准值的这就符合我们的预期了

通过以上的示例就可以解释为什么当left等于right时还要再进入循环一次

挖坑法

思路:
创建左右指针。首先从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新的"坑",然后从左向右找出比基准大的数据,找到后立即放入右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放入当前的"坑"中,返回当前"坑"下标(即分界值下标)

 例如以下示例: 

 

当我们使用挖坑法来找以上数组的基准值时,先将坑位hole初始化为数组首元素下标,并且保存该坑位的值5到变量keyi中,之后right从右往左找比5小的,找到后将找到的值赋值给hole并且之后将坑位改变,之后left从左往右找比5大的,找到后将找到的值赋值给hole并且之后将坑位改变,之后一直重复以上操作直到left等于right时就停止

 完成了hoare版本找基准值的示例分析接下来就试着来实现代码

//找基准值——挖坑法
int _QuickSort2(int* arr, int left, int right)
{int hole = left;int keyi = arr[left];while (left < right){if (left<right && arr[right]>keyi){right--;}arr[hole] = arr[right];hole = right;if (left < right && arr[left] < keyi){left++;}arr[hole] = arr[left];hole = left;}arr[hole] = keyi;return hole;
}

对于以上的挖坑法的代码你可能又会有疑惑为什么不同于hoare版本的代码在此判断又变成left<right,在left等于right时不再继续移动

以上是由于在挖坑法中left和right的移动是单独进行的,而hoare版本中left和right的移动是同时进行的

lomuto前后指针 

算法思路 :
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

以下是该算法的示例: 

  完成了前后指针法找基准值的示例分析接下来就试着来实现代码

//找基准值——前后指针法
int _QuickSort3(int* arr, int left, int right)
{int prev = left;int pcur = left + 1;int keyi = left;while (pcur<=right){if (arr[pcur] < arr[keyi] && ++prev != pcur){Swap(&arr[prev], &arr[pcur]);}++pcur;}Swap(&arr[keyi], &arr[prev]);return prev;
}
复杂度分析 

在以上我们实现了快速排序,那么接下来就对其复杂度进行分析

在快速排序中函数递归次数为\log n,而且每层都要遍历n个元素,因此快速排序时间复杂度为O(n\log n)

空间复杂度为O(\log n)

2.3.3快速排序非递归版本

在以上我们实现的快速排序是用递归的方式来实现的,但当递归的深度很深时就会使得创建的函数栈帧非常多,就任意导致栈溢出,因此在快速排序中我们除了使用递归的方式还有什么其他的方法能实现快速排序呢?

在此非递归版本的快速排序需要借助数据结构:栈

在实现非递归版本的快速排序代码前先来了解是如何借助栈来实现快速排序的

例如以下示例:

 首先先将数组最右侧和最左侧的的下标向后入栈

之后取两次栈顶元素后出两次栈,取出两次栈根据取出的两个栈顶元素得到相应的基准值,再划分数组区间 

该数组第一次基准值数组下标为3,因此就将数据划分为[0,2]和[4,6]两个区间,之后再将右区间的两个下标入栈,再将左区间的两个下标入栈。

之后再取两次栈中的的栈顶元素再出栈两次,再根据基准值划分区间

在此就得到 [0,-1]和[1,2]两个区间,[0,-1]为无效区间就不再入栈,[1,2]入栈

 

之后再取两次栈中的的栈顶元素再出栈两次,再根据基准值划分区间

在此就得到 [1,0和[2,2]两个区间,[1,0]为无效区间就不再入栈,[2,2]内只有一个数据不入栈,因此之后再取两次栈中的的栈顶元素再出栈两次,再根据基准值划分区间

在此就得到 [4,3]和[5,6]两个区间,[4,3]为无效区间就不再入栈,[5,6]入栈

之后再取两次栈中的的栈顶元素再出栈两次,再根据基准值划分区间

在此就得到 [5,4和[5,5]两个区间,[5,4]为无效区间就不再入栈,[5,5]内只有一个数据不入栈,在此之后栈为空就完成了整个的操作

通过以上的示例就了解了如何借助栈来完成非递归版本的快速排序接下来就来实现代码

//快速排序(非递归法)
void QuickSortNonR(int* arr, int left, int right)
{stack 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 pcur = begin + 1;int keyi = begin;while (pcur <= end){if (arr[pcur] < arr[keyi] && ++prev != pcur){Swap(&arr[prev], &arr[pcur]);}++pcur;}Swap(&arr[prev], &arr[keyi]);keyi = prev;//划分区间//[begin,keyi-1] [keyi+1,end]if (keyi + 1 < end){StackPush(&st, end);StackPush(&st, keyi + 1);}if (begin<keyi-1){StackPush(&st, keyi - 1);StackPush(&st, begin);}}StackDestory(&st);
}

2.4归并排序

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

归并排序核心步骤: 

在归并排序的分解过程就结构像是二叉树一样,也像是递归过程中的递推过程,在该过程中不断对数组进行二分,直到每个区间都只有一个元素时,这时每个序列就都是有序的;再将两个相对应的序列合并,在此两个有序序列的合并的算法思想就和之前顺序表算法题篇章中合并两个有序数组算法题相同

在合并两个有效序列过程中由于要合并的序列都是在同一个数组内的,这就使得合并的过程会较为繁琐,因此再创建一个大小于原数组大小相同的数组来临时存储有效序列合并后的值,之后再在每一次合并后再将值重新赋值给原数组

例如以下示例归并过程:

在以上示例中tmp是我们创建的临时数组,以上tmp数组的是将原数组分解后各个序列进行第一次合并后得到的形式,之后再将数组内的值赋值给原数组

之后再进行第二次有序序列的合并,tmp数组以及原数组就变为以下形式

之后再进行第三次有序序列的合并,tmp数组以及原数组就变为以下形式

通过以上的多次合并原数组就变为有序

代码实现

通过以上的示例就了解了归并排序的整个流程接下来就来实现代码

在以上代码中 _MergeSort是MergeSort的一个子方法,这样在递归原数组时就不需要多次创建tmp数组

//归并排序
void _MergeSort(int* arr, int left, int right, int* tmp)
{if (left >= right){return;}int mid = (left + right) / 2;//[left,mid] [mid+1,right]_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right,tmp);//合并int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;int index = begin1;//临时数组下标起始值while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] > arr[begin2]){tmp[index++] = arr[begin2++];}else{tmp[index++] = arr[begin1++];}}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);tmp = NULL;
}
复杂度分析

在实现了归并排序后接下来就来分析其的复杂度

首先是归并排序递归的深度为\log n,接下来归并排序中的子方法_MergeSort的时间复杂度

在以上函数中就可以看出在第一个循环中当是在对最多元素进行合并时,假设这时合并的两个序列元素个数都为n/2,该循环的执行次数就为n/2次, 之后的两个循环执行次数相较该循环就可以忽略不记,最后一个for循环在当是在对最多元素进行合并时将tmp数组内n个元素赋值给原数组;因此该for循环执行次数就为n次。通过以上分析就可以得出单次递归时间复杂度为O(N)因此该排序算法总的时间复杂度就为O(N\log N)

而由于创建了n大小的数组tmp,外加归并排序递归的深度为\log n因此该排序算法总的空间复杂度就为O(N)

2.5测试代码:排序性能对比

在以上我们已经了解了常见的排序算法,我们大体得出各个排序算法的时间复杂度,但是还是无法直观对比各个排序的时间效率,为了解决以上的不足,下面这段测试代码就能使我们直观的得出每个排序的快慢

在此这段代码是先随机生成100000个数据,再将这些数据都拷贝到各个数组当中,这样就可以让每个排序算法排序时的数据都是一样的,之后再通过得出各个排序排序前后的时间,再相减就得到了各个排序算法的运行时间,通过运行时间就可以得出运行效率 

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

运行以上代码输出结果如下所示: 

 

通过排序的时间来看希尔排序、堆排序、快速排序、归并排序的时间效率都非常的不错,而剩余的排序算法的效率不高

2.6 非比较排序

在之前的排序算法当中都会使用数据的比较,那么接下来我们要学习的比较排序就是不再使用比较的方法也能实现排序,在本篇中非比较排序我们学习的是计数排序

2.6.1 计数排序

首先来了解计数排序的原理:

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
(1)统计相同元素出现次数
(2)根据统计的结果将序列回收到原来的序列中

计数排序中是先将待排序中各元素出现次数统计,之后再创建一个数组这时将之前出现的元素当作数组下标,将元素出现的次数放在对应数组下标内 

例如以下示例:

在此就会有一个问题了,在新创建的数组的大小是如何确定的呢?这时你可能会想到根据原来数据中最大的数据来确定不就可以了吗,就比如以上示例当中原数据最大为9那么新创建数组的大小就为9,在以上这个示例当中这种想法确实没问题,但是如果原数组是以下这种情况呢?
 这时如果我们按照以上的想法原数据都集中到100~109之间,但是我们申请的数组空间为109个,这时就会造成大量的空间浪费,因此以上这种根据原来数据中最大的数据来确定新数组大小是想不通的

在此对于该问题最佳的解决方法就是让新数组的大小为原数据中最大值减去最小值再加一
就例如以上的例子当中使用这种方法相比我们之前的方法就大大减少空间的浪费

代码实现

通过以上的示例就了解了计数排序的整个流程接下来就来实现代码

//计数排序
void CountSort(int* arr, int n)
{int max = arr[0], min = arr[0];for (int i = 1; i < n; i++){if (arr[i] > max){max = arr[i];}if (arr[i] < min){min = arr[i];}}int counts = max - min + 1;int* tmparr = (int*)calloc(counts, sizeof(int));if (tmparr ==NULL){perror("calloc fail");exit(1);}memset(tmparr, 0, counts * sizeof(int));for (int i = 0; i < n; i++){tmparr[arr[i] - min]++;}int index = 0;for (int i = 0; i < counts; i++){while (tmparr[i]--){arr[index++] = i + min;}}
}

 复杂度分析

在实现了计数排序的代码后接下来来分析计数排序的复杂度

通过以上代码就可以看出在第一个for循环执行次数为n次,第二个for循环执行次数也为n次,第三个for循环执行次数为n+counts次,因此代码的时间复杂度为O(N+counts)

在代码中我们创建大小为counts的数组Count,因此代码空间复杂度为O(counts) 

在计数排序中当待排数据很集中时排序的效率会非常的高,但是计数排序在一些场景下会无法使用,就比如计数排序只能对整数进行排序无法对小数排序,因此计数排序会有以下的特征

计数排序的特性:
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

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

在本本篇中我们学习了许多种排序算法,那么接下来就对这些排序算法做一个总结,在此之前我们要来了解排序算法的稳定性是说明

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,⽽在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

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

以上就是数据结构——排序章节所有的内容了,希望能得到你的点赞收藏


http://www.hkcw.cn/article/LwVInvTpbD.shtml

相关文章

TSP-旅行商问题(基于动态规划或蚁群算法求解)

1. TSP问题 旅行商问题(Travelling salesman problem, TSP)是运筹学和理论计算机科学中经典的问题.具体问题如下:给定一系列城市和每对城市之间的距离,求解访问每座城市一次并回到起始城市的最短回路. 2. 动态规划 本节参考旅行商问题(动态规划) 2.1 理论介绍 假设节点数…

【算法与数据结构】深入解析二叉树(二)之堆结构实现

文章目录 &#x1f4dd;二叉树的顺序结构及实现&#x1f320; 二叉树的顺序结构&#x1f320; 堆的实现&#x1f320; 堆的实现&#x1f309;堆向下调整算法&#x1f309;堆的创建&#x1f309;建堆时间复杂度&#x1f309;堆的插入&#x1f309;堆的删除 &#x1f320;堆向上调…

【leetcode】优先级队列的两种妙用:词频统计与动态中位数(附代码模板)

前言 &#x1f31f;&#x1f31f;本期讲解关于力扣的几篇题解的详细介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…

【算法学习】哈希表篇:哈希表的使用场景和使用方法

算法学习&#xff1a; https://blog.csdn.net/2301_80220607/category_12922080.html?spm1001.2014.3001.5482 前言&#xff1a; 在之前学习数据结构时我们就学习了哈希表的使用方法&#xff0c;这里我们主要是针对哈希表的做题方法进行讲解&#xff0c;都是leetcode上的经典…

HDFS详解

一、HDFS 概述 定位与特点 分布式文件系统&#xff1a;HDFS&#xff08;Hadoop Distributed File System&#xff09;是 Hadoop 生态的核心组件&#xff0c;专为海量数据存储和批处理设计。 核心设计原则&#xff1a; 高容错性&#xff1a;数据自动多副本冗余&#xff0c;支持…

【数据结构】String字符串的存储

目录 一、存储结构 1.字符串常量池 2.字符串哈希表 2.1结构 2.2基础存储单位 2.2.1键对象 2.2.2值对象 二、存储过程 1.搜索 2.创建 三、存储位置 四、存储操作 1.new新建 2.intern入池 这是String类的详解&#xff1a;String类变量 一、存储结构 1.字符串常量池…

数据结构大作业——家谱管理系统(超详细!完整代码!)

目录 设计思路&#xff1a; 一、项目背景 二、功能分析 查询功能流程图&#xff1a; 管理功能流程图&#xff1a; 三、设计 四、实现 代码实现&#xff1a; 头文件 结构体 函数声明及定义 创建家谱树头结点 绘制家谱树&#xff08;打印&#xff09; 建立右兄弟…

北京将有7级大风小冰雹 雷电蓝色预警发布

6月1日17时50分,北京发布雷电蓝色预警,预计当天20时至次日2时,自西向东将有雷阵雨天气,局地短时雨强较大,并伴有7级左右短时大风和小冰雹,请注意防范。明天上午至中午前后依旧会出现分散性雷阵雨,雨量总体不大。午后至前半夜北风增强,阵风明显,外出时请做好防风措施,…

专家:印太战略实质是霸权工具 不会得逞

针对美国防长赫格塞思在香格里拉对话会上涉及中国的部分表态,有中国学者指出,美国所谓的“印太战略”实质上是霸权工具,不会得逞。在对话会上,赫格塞思再次提到所谓的“印太战略”,并呼吁亚太地区同盟国和合作伙伴国与美国一起构筑更现实的战略关系。国防大学教授孟祥青表…

SCNN(Spatial CNN) 模型学习记录

目录 1.模型架构 2.核心模块SCNN_*分析 SCNN&#xff08;Spatial As Deep: Spatial CNN for Traffic Lane Detection&#xff09;是一种专为交通车道线检测任务设计的深度神经网络架构&#xff0c;由中国科学院计算技术研究所提出&#xff0c;旨在在语义分割框架中增强空间信…

Lerobot框架使用(含本地数据训练)

本文包含从安装环境到完整使用Lerobot框架进行算法复现全流程。 A Install LeRobot 安装miniconda管理python环境 Linux mkdir -p ~/miniconda3 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh bash ~/minicon…

小红书 web x-s x-t X-Mns 分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 cp execjs.compile(open(v…

国产化中间件基本使用_东方通(TongWeb7.0.E.6_P2)

tongweb开发操作文档 1、前期准备 进入官网申请使用,官网地址:https://www.tongtech.com 若提供的安装程序的授权文件已过期,请去官方网站重新申请。 2、安装部署 2.1、下载安装Tongweb 进入官网申请试用,官方会提供响应的嵌入式安装包及试用授权证书(3个月) 申请…

010302-oss_反向代理_负载均衡-web扩展2-基础入门-网络安全

文章目录 1 OSS1.1 什么是 OSS 存储&#xff1f;1.2 OSS 核心功能1.3 OSS 的优势1.4 典型使用场景1.5 如何接入 OSS&#xff1f;1.6 注意事项1.7 cloudreve实战演示1.7.1 配置cloudreve连接阿里云oss1.7.2 常见错误1.7.3 安全测试影响 2 反向代理2.1 正向代理和反向代理2.2 演示…

FREERTOS+LWIP+IAP实现TCP、HTTP、网页访问并固件升级、更新配置 (三)lwip实现httpd服务并在web访问

前言 在前两篇文章中配置freeRTOS和&#xff0c;并实现了TCP、UDP的通信协议&#xff0c;现在终于轮到重头戏lwip的httpd服务&#xff0c;LWIP官方例程中是有很多自带的网页的&#xff0c;但是远远不够满足实际项目的使用需求&#xff0c;因此我也是踩了很多坑&#xff0c;从前…

lighthouse(灯塔)前端性能测试工具

前端性能测试工具之lighthouse灯塔 介绍下载链接使用方法前端性能指标解读 介绍 Lighthouse 是一个开源的自动化工具&#xff0c;用来测试前端页面性能&#xff0c;反馈页面问题以提升页面体验。可以联合谷歌浏览器&#xff0c;作为插件导入&#xff0c;开启后可测试页面性能 …

Ai智能体四:互动式 AI 聊天助手:前端实现

在现代 web 应用中,集成智能对话功能已经成为提升用户体验的重要手段之一。本文将介绍如何通过 Vue 3 和 Element Plus 构建一个高效的 AI 聊天助手界面,并详细讲解其实现原理和功能。 1. 整体架构 该聊天界面分为 左侧边栏 和 右侧内容区域,实现了清晰的布局结构,左侧边…

Spring Boot 3.x 引入springdoc-openapi (内置Swagger UI、webmvc-api)

接触的原因 因开发自己的项目时&#xff0c;写接口文档很繁琐&#xff0c;查到后端都在用swagger等接口工具来记录接口文档&#xff0c;于是学习了一下&#xff0c;本文记录个人配置过程&#xff0c;有问题欢迎指正交流&#x1f601; Swagger&#xff1a; Swagger是一种Rest AP…

OpenWebUI配置异常的外部模型导致页面无法打开

一、使用Ollama关闭OpenAI OpenWebUI自带OpenAI的API设置&#xff0c;且默认是打开的&#xff0c;默认情况下&#xff0c;启动后&#xff0c;会不断的去连https://api.openai.com/v1&#xff0c;但是无法连上&#xff0c;会报错&#xff0c;但是不会影响页面&#xff0c;能正常…

Postman(Apifox)调用WebServicer接口

postman调用WebServicer接口 前言 Postman使用方法Apifox使用方法参数与配置请求代码(当然一般开发会给一个样例)步骤 前言 之前都是使用SoapUI测试WebServicer接口,由于工作所需,需要使用Postman测试工具 Postman使用方法 可以直接在请求里写全部的wsdl地址 ,参数会自动带进…