一、什么是排序算法
在实际中,数据基本不会单一出现,而是以组的形式出现。而且,出于某种需求,需要对这组数据进行排序。
问题定义:
输入:n个数的数列<a1 , a2 , … , an> 。
输出:输入数列的一个排列<a’1 , a’2 , … , a’n>,满足a’1 ≤ \le ≤a’2 … ≤ \le ≤a’n 。
排序算法,就是如何使得记录(数据)按照要求排列的算法。
排序算法在很多领域非常重要,尤其是在大量数据的处理方面,一个优秀的算法可以节省大量的资源。
二、为什么要排序
大部分计算机科学研究人员认为排序是算法研究中最基础的问题,原因有很多:
- 数据本身就需要被排序,如银行需要按编号对支票进行排序、学校需要按学生成绩进行排序;
- 很多算法把排序作为关键子程序。例如,在Photoshop中需要根据图层将画面进行叠加;
- 现有大量的排序算法,其中体现了丰富的技术思想。因此,排序问题具有很好的学习与历史价值。
三、有哪些排序算法
算法 | 最坏情况运行 | 平均情况/期望运行时间 |
---|---|---|
冒泡排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n 2 ) \Theta(n^2) Θ(n2) |
插入排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n 2 ) \Theta(n^2) Θ(n2) |
归并排序 | Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) | Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) |
堆排序 | O ( n l g n ) O(nlgn) O(nlgn) | – |
快速排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)(期望) |
计数排序 | Θ ( k + n ) \Theta(k+n) Θ(k+n) | Θ ( k + n ) \Theta(k+n) Θ(k+n) |
基数排序 | Θ ( d ( k + n ) ) \Theta (d(k+n)) Θ(d(k+n)) | Θ ( d ( k + n ) ) \Theta (d(k+n)) Θ(d(k+n)) |
桶排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n ) \Theta(n) Θ(n)(平均情况) |
上述排序算法并没有囊括所有的排序算法,但这些基本体现了所有在排序算法中用到的技术与思想。
冒泡排序和插入排序最坏情况可以在 Θ ( n 2 ) \Theta(n^2) Θ(n2) 时间内将 n n n 个数排好序,其内部循环结构紧凑,在 n n n 较小时,这两种算法是非常快的原址排序算法。
在上一句话里有几个问题需要进行解释,为什么要讨论最坏、平均情况的运行时间?有没有最好的情况?为什么时间复杂度有不同的表示 Θ 、 O \Theta、O Θ、O ? 什么是原址排序?
在上一节中我们说过,评价一个算法主要有两大指标,时间和空间复杂度。
- 为什么要讨论最坏、平均情况的运行时间?
一个算法的最坏运行情况运行时间给出了任何输入的运行时间的上界。知道这个,我们可以拍着胸脯保证,这个算法在确定规模下绝对不需要更多的时间。对于某些算法,最坏情况经常发生,如在数据库中查找某条不在数据库中的信息时,但对于某些算法,最坏情况出现的概率很低,因此我们考虑算法运行时间的平均情况。 - 有没有最好的情况?
当然存在,只不过相比最坏情况,最好情况的出现一般同样概率极低,同时其无法像最坏的情况给我们不会更坏的保证,因此最好情况的意义不大。当然,在讨论算法时,一般会分析什么情况是达到最好的情况。 - 为什么时间复杂度有不同的表示
Θ
、
O
\Theta、O
Θ、O ?
在实际分析一个算法的复杂度时会出现很多情况,因此在算法时间复杂度上引入了 Θ 、 O 、 Ω 、 o \Theta、O、\Omega、o Θ、O、Ω、o 等符号来进行分析。
这些分析相对复杂,并且略微抽象,我这里不打算详细去讨论这个。对于研究算法的人来说,这种做法多此一举,多于初学者来说,没有必要对此进行深入研究。对于时间复杂度问题可以普遍把 Θ 、 O \Theta、O Θ、O符号看成目前普遍流行的 O O O符号 , 虽然不精确。 - 什么是原址排序?
上述问题都是关于时间复杂度的,这个问题则是关于空间复杂度的。如果在排序过程中仅有常数个元素需要在排序过程中储存在数组之外,则称排序算法是原址的。简单来说就是,排序过程中元素在数组内部进行交换即可完成排序。
解释完这几个问题,我们继续。
归并排序是一种 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) 的排序算法,但是它分解和归并过程中不是原址的;
堆排序是一种 O ( n l g n ) O(nlgn) O(nlgn) 的原址排序算法,其中用到了堆数据结构;
快速排序是一种原址排序算法,它的最坏运行时间是 Θ ( n 2 ) \Theta(n^2) Θ(n2) , 但期望运行时间为 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) , 在实际运行中通常比堆排序快。是排序大数组的最常用方法。
上述几种算法均为比较排序算法,它们通过对元素进行比较从而对数组进行排序,根据决策树模型,可以证明比较排序算法排序 n n n 个元素的最坏情况的下界是 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) ,也就是说比较排序算法无法做到最坏时间比 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) 更低的算法了。
但是,如果我们拥有更多的信息,就可能打破 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn) ,计数排序算法假定数组中元素均在某个有限元素的集合内,如为1-10的整数,通过对每个值进行计数,就可以在 Θ ( k + n ) \Theta(k+n) Θ(k+n) 内进行排序。基数排序与之相关,可以拓展计数排序的适用范围。
桶排序基于数组中数据的概率分布。对于半开区间 [ 0 , 1 ) [0,1) [0,1) 内服从均匀分布的 n n n 个实数,其平均运行时间为 Θ ( n ) \Theta(n) Θ(n) 。
四、小结
这一节对排序算法进行了简单的介绍,下一节将介绍冒泡排序,这个学习排序算法无法绕开的算法。我会给出算法的C++代码,当然水平有限,同时给出的代码仅供学习,没考虑复杂情况,由于我只会进行简单的测试,无法保证没有bug,如果有bug,希望能告诉我,非常感谢。至于为什么采用C++, 主要相比于C语言,C++面向对象的特性使编程更加丰富;相比于后续的语言,C++更能体现各种内存上的细节,运行速度更快。