今天,我将带来数据结构的排序算法,排序算法作为校招中常考知识点之一,我们必须要熟练的掌握它,对自己提出高要求,才能有高回报。
排序的概念和应用
排序的概念:
排序,就是就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
在生活中,排序的应用是非常的广泛的,比如在我们高考前,我们会在网上搜寻大学的排名,或者在双十一等日子,我们在淘宝挑着想要购买的电脑,它们都按照着一个关键字的大小来进行排序的,如大学排行榜按高考成绩,淘宝显示的电脑顺序按价格或者好评率。
大学排行榜

淘宝

由此可见,排序算法是多么的重要,那么,我们开始算法的教学吧。
内部排序和外部排序
概念:
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
简单来说,当我们需要排序的数据远远大于内存所能存放的最大空间,我们只能通过某种方式直接对磁盘存储的数据进行排序,称之为外部排序。如果,我们需要排序的数据量小,可以拿到内存进行排序,称之为内部排序
排序算法需要掌握的知识
1.在学习完八大排序后,我们必须熟练的掌握它们的思想,并且能够熟悉它们的代码实现。
2.我们必须要理解它们对应的时间复杂度和空间复杂度。
至于时间复杂度和空间复杂度的讲解,我在前面的文章已经提到,下面是
传送门
时间复杂度和空间复杂度(以题目的方式来介绍和分析)
3.我们必须掌握每个排序算法的稳定性。
稳定性的概念:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
如:我们需要排序序列,1 2 3 2,黑色字体的2在浅色字体2的后面。
如果我们排序后,结果为1 2 2 3,即两个数字2的顺序没有被打乱,依然是黑色字体的2在浅色字体2的后面,那么该排序算法就是稳定的。
如果排序后的序列为1 2 2 3,即两个数字2的顺序被打乱,黑色字体的2在浅色字体2的前面,那么该排序算法就是不稳定的。
插入排序
1.直接插入排序
假设我要让一个乱序的数组变成升序,那么我将从第二个元素开始,依次跟前面的元素进行比较。
过程如下:
我选择第二个元素跟第一个元素进行比较,如果,第二个元素小于第一个元素,则进行交换,如果是第二个元素大于第一个元素或者相等,则不进行交换。
这一套下来,前两个元素已经升序了。
接下来,我选择第三个元素,依次跟第二、第一个元素进行比较,依然是第三个元素小,就进行交换。
直到比较完最后一个元素,那么数组就变成升序了。
以下是动图:

下面是代码实现:
#include<stdio.h>
void InsertSort(int* arr,int n)
{
for (int i = 0; i < n - 1; i++) //考虑tmp最后要取到最后一个元素,即n-1,i最大为n-2,保证i+1最大为 n - 1
{
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;
}
}
int main()
{
int arr[] = {
10,2,19,3,12,25,15,36,30,5};
InsertSort(arr,sizeof(arr)/sizeof(arr[0]));
return 0;
}
排序前:
排序后:
直接插入排序的时间复杂度分析
最坏的情况:当我们要对数组进行排为升序的时候,初始数组为降序;当我们要排降序的时候,初始数组为升序。
上面的两种情况导致了,我们对每一个元素都需要往前进行调整多次。
如:5 4 3 2 1
我们要把它排成升序,按照直接插入排序。
第一趟结果4 5 3 2 1,4往前调整1次
第二趟结果3 4 5 2 1,3往前调整2次
第三趟结果2 3 4 5 1,2往前调整3次
第三趟结果1 2 3 4 5,1往前调整4次
如果上面调整次数有点不清楚,可以看上面的动图。
在上面的直接插入排序的代码中,有for循环和while循环。
当for循环第一次进入时,即为第一趟排序,因为要排序前两个元素;第二次进入时,即为第二趟排序,因为要排序前三个元素,所以for循环即为该排序的趟数。
当while循环第一次进入时,即为某一趟的第一次调整,第二次进入时,即为某一趟的第二次调整,所以while循环即为该排序的调整次数。
综上,排序第几趟与for循环第几次相关,调整次数与while循环次数相关。
在上面的排序数组5 4 3 2 1中,我们还可以发现,排序第一趟,调整一次,排序第二趟,调整两次,即第几趟排序,就调整几次。综上,for循环第几次进入,就相应的进行几次while循环。

总共循环次数为1 + 2 + 3 + …… + n = n^2/2 + n /2
由大O渐表示法可得,最坏的情况的时间复杂度为:O(N^2)。
值得注意的是,上面的for循环是第几次进入,所以计算循环次数即为每进入一次for循环后,进行的while循环个数。
最好的情况:当我们要排序升序时,初始数组是升序或者接近升序;当我们要排序降序时,初始数组是降序或者是接近降序。
此时,我们的每趟排序的调整次数都是0或者接近于都是0,即while循环大多数都是进入,然后直接break出来,即接近于都是循环1次。

循环次数:n次。
由大O渐表示法可得,最好的情况的时间复杂度是:O(N)。
值得注意的是,上面的for循环是第几次进入,所以计算循环次数即为每进入一次for循环后,进行的while循环个数。
由时间复杂度和空间复杂度(以题目的方式来介绍和分析)的文章可以得知,时间复杂度都是按最坏的情况,所以直接插入排序的时间复杂度是:O(N^2)。
直接插入排序的空间复杂度分析
由于直接插入排序的开辟的空间为常量级,所以空间复杂度为O(1)。
直接插入排序的稳定性分析
稳定性:稳定。
原因如下:我们在排序数组时,可以让数字在比对的过程中,如果相等就不要替换,直接插入到该数字的后面的位置,保证该排序的稳定性。
如:1 2 3 2
第一次调整,1 2 2 3 后
两个数字2相等,但是我们不要进行交换,保证直接插入排序算法的稳定性。
总结:
直接插入排序中:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
2.希尔排序
希尔排序跟直接插入排序相比,增加了一个预排序的过程,来达到优化直接插入排序的效果。
在预排序中,增加了一个gap值,这个gap值是用来分组的。如下:
假设一个数组有10个元素,我们要排为升序,gap值为4,那么相同组中的元素,中间隔4个其他组的元素,如下图:





上面的图片就已经是分组完成的了,那么分组有什么用呢?接下来,我来解答这个问题。
在上面的分组完成的图片中,我们可以确定有5组,每组有2个元素,那么在比对的过程中,我们只让组中元素进行比对,比如蓝色组只在蓝色组内比较,红色组只在红色组内比较,如下图:

那么,我们什么时候比对完成呢,毫无疑问,当比对掉所有的组,即比对完黄色的那一组,我们就已经比对完了。
比对完的结果如下:

在此次的预排序中,我们让大的元素一下跳跃几步到后面,小的元素一下跳跃几步到前面,最典型的还是数值2和数值3,只一步就跳到了前面。为直接插入排序做好了准备,防止某些元素在直接插入排序中,移动过多,拉低了整体排序的效率。
上面的讲解中,我们已经知道了预排序的gap是用来分组的,并且搞懂了是如何分组的,还有知道了是在组内元素进行比对的,还有搞懂了预排序中组内比对的好处。
接下来,我们就要搞懂gap的整个取值过程。
在最开始的过程中,gap初始化为n(数组元素个数),注意初始化后是为了公式求值,而不是gap的第一次值就是n。
接下来,gap = gap / 3 + 1
每取到一个gap值,就进行一次分组,组内比对,比对完,按公式改变gap的值,直到gap的值为1时,预排序结束,直接插入排序开始,这就是希尔排序的过程。
以下是动图

代码如下:
#include<stdio.h>
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)//当gap等于2进入循环,就可以取到1了,循环条件改为大于等于1,会死循环
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)//当gap为1时,n - gap的值是n - 1,满足后面的直接插入排序对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;
}
}
}
int main()
{
int arr[] = {
10,3,5,25,2,12,19,30,15,16};
ShellSort(arr,sizeof(arr)/sizeof(arr[0]));
return 0;
}
排序前:
排序后:
希尔排序的时间复杂度分析
希尔排序的时间复杂度的分析较为困难,下面截至一本书。
《数据结构(C语言版)》— 严蔚敏

在希尔排序的时间复杂度分析中,还进行了大量的实验,我们直接记住结论就行。
希尔排序的时间复杂度:O(N^1.3)。
希尔排序的空间复杂度分析
由于希尔排序开辟的空间是常量级的,所以希尔排序的时间复杂度是:O(1)。
希尔排序的稳定性
由于在预排序中,相同的数字可能不在同一组中,那么进行组内交换时可能会改变相同数字的位置,所以希尔排序是不稳定的。
希尔排序的稳定性:不稳定。
总结:
希尔排序中:
时间复杂度:O(N^1.3)
空间复杂度:O(1)
稳定性:不稳定
选择排序
1.直接选择排序
直接选择排序是查找到序列中最小的值和最大的值,然后如果要排序升序的话,就将最小值和第一个元素交换,将最大值和最后一个元素交换。然后排除掉第一个元素和最后一个元素,继续寻找最小值和最大值,分别与第二个元素和倒数第二个元素进行交换,依次下去,直到数组有序。
比如,我要在一个数组中排序为升序,并且排序为升序,该数组的长度为n。(我们采用begin和end来表示需要排序的范围,比如最开始时,begin等于0,end等于数组元素减一,排序范围为全部的元素,当进行第一趟排序后,begin等于1,end等于n-2,排序掉第一个元素和倒数第一个元素)

下面是动图

下面是代码实现:
void swap(int* a,int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* arr,int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int Mini = begin, Maxi = end;
for (int i = begin; i <= end; i++)
{
if (arr[i] < arr[Mini])
{
Mini = i;
}
if (arr[i] > arr[Maxi])
{
Maxi = i;
}
}
swap(&arr[begin],&arr[Mini]);
if (begin == Maxi) //调整,最大值因为上面的交换由begin下标所在的位置变为了Minx下标所在的位置
Maxi = Mini;
swap(&arr[Maxi],&arr[end]);
begin++;
end--;
}
}
int main()
{
int arr[] = {
10,3,5,25,2,12,19,30,15,16 };
SelectSort(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
排序前:
排序后:
直接选择排序的时间复杂度分析
观察代码可以得知,有两层循环,但是我们不能认为认为两层循环,该排序的时间复杂度就是:O(N^2),有时候结果不是这样。
假设排序数组中,数组的元素个数是n。
第一趟排序中,begin的值为0,end的值为n-1,那么在寻找最大值和最小值的比较次数是n。
第二趟排序中,begin的值为1,end的值为n-2,那么在寻找最大值和最小值的比较次数是n-2。
第三趟排序中,begin的值为2,end的值为n-3,那么在寻找最大值和最小值的比较次数是n-4。
……
第n/2趟排序中,begin的值为n/2-1,end的值为n/2,那么在寻找最大值和最小值的比较次数是2。
第几趟排序即为代码中while循环中的第几次循环,比较次数即为for循环的第几次循环。

所以总共循环(比较)n + (n-2)+ (n-4) + …… + 1 = n^2
直接选择排序的时间复杂度是:O(N^2)。
直接选择排序的空间复杂度
直接选择排序开辟的空间为常量级,所以直接选择排序的空间复杂度是:O(1)。
直接选择排序的稳定性
直接选择排序的稳定性:不稳定。因为在选择最值时,直接往begin或者end位置替换的时候,会打乱相同数值的顺序。
比如:1 4 4 3排序升序,在第一趟的排序中,4做为从左往右找到的第一个最大值,直接与end位置交换,即与3进行交换,结果为1 3 4 4,那么,两个数字4的位置就乱了。
总结:
直接选择排序中:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
2.堆排序
在堆排序中,我们首先要进行建堆,建堆算法在前面的文章中我已经进行分析了,下面是传送门。
<<数据结构>>向上调整建堆和向下调整建堆的分析(特殊情况,时间复杂度分析,两种建堆方法对比,动图)
在上面的文章中,我已经对向上建堆和向下建堆进行了对比,所以,在这里的堆排序中的建堆算法,我选择向下调整建堆。
排序升序,建大堆
排序降序,建小堆
如果我们要对一个数组进行堆排序,将这个数组排为了升序,那么我们应该先建为大堆,此时,堆顶就是最大的元素,我们将堆顶的元素与堆尾部的元素交换,然后排除掉堆尾部,重新调整堆,此时,这个堆最大的元素就在后面了,持续下去。在这种方法下,大的元素将一直往后面移动,直到把所有的元素排序完成。
同理,如果我们要对一个数组进行堆排序,将这个数组排为了降序,那么我们应该先建为小堆,此时,堆顶就是最小的元素,我们将堆顶的元素与堆尾部的元素交换,然后排除掉堆尾部,重新调整堆,此时,这个堆最小的元素就在后面了,持续下去。在这种方法下,小的元素将一直往后面移动,直到把所有的元素排序完成。
下面,我以排序升序为例子。

如上面数组中,我将该数组排为升序(采用向下建堆),下面是动图

下面是代码:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = 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 + 1 < n
{
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)
{
//向下建堆算法
for (int 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--;
}
}
int main()
{
int arr[] = {
12,19,5,25,36,10,3,30,15,2,14,20,30,43,30 };
HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
排序前:
排序后:
堆排序的时间复杂度

可能有人认为,有的结点调整的高度次数远小于我们所计算的高度次数,导致计算结果过大,但是,时间复杂度本身就不是一个准确的计算,如大O渐进表示法就规定省略一些计算表达式中的细节。
堆排序的空间复杂度
堆排序开辟的空间为常量级,所以空间复杂度是:O(1)。
堆排序的稳定性
不稳定,堆顶和堆底元素交换,向下调整,这些都可能打乱相同数字的顺序。
总结:
堆排序中:
时间复杂度为:O(N*logN)
空间复杂度为:O(1)
稳定性:不稳定
交换排序
1.冒泡排序
冒泡排序是我们在编程学习中的老相识了,我就直接上动图了,如果还是不熟悉的,可以看看这篇文章,下面是传送门
冒泡排序(详细)
如果感觉可以的话,那么就直接往下看吧。
下面是动图。

下面是代码:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
swap(&arr[j],&arr[j + 1]);
}
}
}
}
int main()
{
int arr[] = {
12,19,5,25,36,10,3,30,15,2,14,20,30,43,30 };
BubbleSort(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
排序前:
排序后:
如果需要代码注释,可前往上面冒泡排序的传送门,那篇文章有详细的代码注释。
冒泡排序的时间复杂度

循环次数满足等差数列,总共循环(n-1) + (n-2) + (n-3) + … + 2 + 1 = (n^2 + n) / 2
由大O渐进表示法可得,冒泡排序的时间复杂度是:O(N^2)。
冒泡排序的空间复杂度
由于冒泡排序开辟的空间为常量级,所以冒泡排序的空间复杂度是:O(1)。
冒泡排序的稳定性
稳定,因为我们可以控制在比对的过程中,如果两个数相等,就不交换它们的位置,来保证相同数字的位置不会变换。
总结:
冒泡排序中:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
2.快速排序
快速排序的知识点还是相对比较多的,它有三个版本,分别是hoare版本,挖坑法,前后指针法,并且我们还需要掌握它的非递归版本,并且还有三种优化方法,分别是三数取中,小区间优化,三目并排。
不着急,我依次深入地进行讲解。

本文聚焦数据结构中的排序算法,是校招常考知识点。介绍了排序概念、内外排序区别,详细讲解插入、选择、交换、归并、基数、计数等排序算法,包括原理、代码实现、复杂度分析和稳定性判断,还提及文件外排序、测试代码及校招考核范围。










最低0.47元/天 解锁文章
1万+

被折叠的 条评论
为什么被折叠?



