记得很多年以前,在我研究生毕业找工作那会儿,面试的时候被面试官问到过两次关于排序算法的问题。这件事至今给我的印象比较深刻,因为很少有在不同的面试过程中会被问到相同的问题。由此可见排序算法在面试中占有很大的比重,其实这也是考察程序员的一个基础。前几天在招聘的过程中又想起了这件事,觉得虽然这个问题比较基础,但是还是有必要整理总结一下的。
大多数的人应该都能回答出冒泡排序和选择排序的算法,我想这可能是因为国内目前的教材中(比如「C语言编程基础」)这两种为例子的比较多的缘故吧。其实在后来的工作中了解到不止这两种算法,还有一些其他的算法也可以实现。当然能够回答出冒泡排序和选择排序也应该是合格的。下面我就来总结一下目前我所知道的常见的几种排序算法。
1. 冒泡排序
我们就从最常见的冒泡排序来说起。为了统一起见,本文的排序都是以从小到大的序列为准。文中的例子都以C
语言代码编写,对python
和Java
的源码感兴趣的同学,可以在这里找到源码。
冒泡排序规则是:从左到右,数组中相邻的两个元素进行比较,将较大的放到后面。
按照这个原则,我们编写代码。
C语言实现
打印结果如下
结果分析
数组长度11,一共比较了10轮。第一轮比较结束后序列中最大数45
从序列的最右边冒出来。第二轮比较后,所有数中第二大的那个数23
就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。
轮次 |
比较次数 |
结果 |
第1轮 |
10 |
6,17,19,1,2,23,5,4,11,3,45 |
第2轮 |
9 |
6,17,1,2,19,5,4,11,3,23 ,45 |
第3轮 |
8 |
6,1,2,17,5,4,11,3,19 ,23,45 |
第4轮 |
7 |
1,2,6,5,4,11,3,17 ,19,23,45 |
第5轮 |
6 |
1,2,5,4,6,3,11 ,17,19,23,45 |
第6轮 |
5 |
1,2,4,5,3,6 ,11,17,19,23,45 |
第7轮 |
4 |
1,2,4,3,5 ,6,11,17,19,23,45 |
第8轮 |
3 |
1,2,3,4 ,5,6,11,17,19,23,45 |
第9轮 |
2 |
1,2,3 ,4,5,6,11,17,19,23,45 |
第10轮 |
1 |
1,2 ,3,4,5,6,11,17,19,23,45 |
时间/空间复杂度
由于我们要重复执行n次冒泡,每次冒泡要执行n次比较,也就是O(n^2)
。 空间复杂度是O(n)
。
2. 选择排序
选择排序规则是:从第一个位置开始比较,找出最小的,和第一个位置互换,开始下一轮。第二轮从第二个位置开始查找,然后找出剩余序列中最小的,和第二个位置互换,然后重复之前的操作,依次比较,直至所有数都排列完毕。
C语言实现
打印结果
结果分析
也是一共比较了10轮。第一轮比较结束后序列中最小数a[4]
和a[0]
互换。第二轮从a[1]
开始比较,因为a[0]
已经是最小的数了,比较完成后,a[1]~a[10]
中最小的数a[5]
和a[1]
互换。依次论推,直到把数据排列好。
轮次 |
比较次数 |
结果 |
第1轮 |
10 |
1 ,6,19,45,17 ,2,23,5,4,11,3 |
第2轮 |
9 |
1,2 ,19,45,17,6 ,23,5,4,11,3 |
第3轮 |
8 |
1,2,3 ,45,17,6,23,5,4,11,19 |
第4轮 |
7 |
1,2,3,4 ,17,6,23,5,45 ,11,19 |
第5轮 |
6 |
1,2,3,4,5 ,6,23,17 ,45,11,19 |
第6轮 |
5 |
1,2,3,4,5,6 ,23 ,17,45,11,19 |
第7轮 |
4 |
1,2,3,4,5,6,11 ,17,45,23 ,19 |
第8轮 |
3 |
1,2,3,4,5,6,11,17,45,23,19 |
第9轮 |
2 |
1,2,3,4,5,6,11,17,19 ,23,45 |
第10轮 |
1 |
1,2,3,4,5,6,11,17,19,23,45 |
时间/空间复杂度
每次要找一遍最小值,最坏情况下找n次,这样的过程要执行n次,所以时间复杂度还是O(n^2)
。空间复杂度是O(n)
。
3. 快速排序
快速排序规则是:选择序列中的一个数为基准数,序列中的数逐个与之比较,小的放到它的左边,大的放大它的右边,然后再对左右区间递归执行上述步骤,直至整个序列有序。
C语言实现
打印结果
结果分析
以17(a[0])
做为基准数,先从序列尾部(右边)开始比较,大家可以思考一下为什么要先从右边开始,如果从左边开始会出现什么问题? 右边第一个数字3(a[10])
小于基准数,因此需要调换位置(需要调换到的目的位置要从头部开始查找),尾部的比较停止在这个位置,然后从头部开始比较(左边),寻找大于基准数的元素的位置,找到该位置后与刚才的元素3
对调。我们看到找到的是元素19(a[2])
,将a[2]
与a[10]
对调。执行完这一步后序列元素如打印结果
中第一行所示。
序列 |
元素 |
初始序列 |
17,6,19,45,1,2,23,5,4,11,3 |
第一次交换后 |
17,6,3 ,45,1,2,23,5,4,11,19 |
然后从尾部接着查找下一个小于基准数的元素,重复刚才的步骤,直到头尾部相遇为止。经过这一轮排序后,整个序列已经按照基准数分为了两组(如下),左边的小于基准数17
,右边的大于基准数。最后将最后相遇的位置与a[0]
对调。
序列 |
元素 |
初始序列 |
17,6,19,45,1,2,23,5,4,11,3 |
第一轮交换后 |
5,6,3,11,1,2,4, 17,23,45,19 |
然后对左右两组分别递归调用排序,直到所有的数排列有序为止。
时间/空间复杂度
4. 插入排序
插入排序规则是:对于每个未排序数据,在已排序序列中从后向前
扫描,找到相应位置并插入。
C语言实现
打印结果
结果分析
从a[0]
开始,逐个与下个相邻元素比较,如果a[i]
大于a[i+1]
,那么就把a[i+1]
放到a[i]
之前的序列中进行排序。如第三轮中交换后元素1
被交换到序列6,17,19,45
中进行排序。依次重复,直到所有数都有序排列为止。
序列 |
元素 |
初始序列 |
6,17,19,45,1 ,2,23,5,4,11,3 |
交换后 |
1 ,6,17,19,45,2,23,5,4,11,3 |
时间/空间复杂度
因为要选择n次,而且插入时最坏要比较n次,所以时间复杂度同样是O(n^2)。空间复杂度是O(n)。
5. Shell排序
Shell排序规则是:分组插入排序, 将数组按照一定步长分成几个子数组,对子数组(列)分别进行插入排序,重复这过程,不过每次用更小的步长-更长的列
来进行。
C语言实现
打印结果
结果分析
以步长为5开始进行排序,可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
对这5列进行插入排序后,序列变为
将这5列连接在一起的原始序列为2,6,5,4,1,3,23,19,45,11,17
,然后再对该序列以步长2排序。方法相同,排序完成后的数列为1,3,2,4,5,6,17,11,23,19,45
。重复该步骤直到步长为1。
时间/空间复杂度
希尔排序的时间复杂度受步长的影响。