排序算法
本文来自于:https://en.wikipedia.org/wiki/Sorting_algorithm
本文保留了上面链接的绝大部分内容,省略了我“自认为”不重要的部分。你也点击上面链接查看原文
目录
六、非比较的排序算法(Non-comparison sorts)
八、流行的排序算法(Popular sorting algorithms)
3、冒泡排序及其变体(Bubble sort and variants)
九、内存使用模式和索引排序(Memory usage patterns and index sorting)
一、概述
在计算机科学中,排序算法就是把list中的元素 按特定顺序进行摆放 的算法。最频繁使用的顺序是 数字顺序(numerical order)和字典顺序(lexicographical order)。其他算法,例如搜索算法,合并算法,要求输入的数据是已排序的,所以,高效的排序算法 对 优化其他算法是非常重要的
此外,排序算法的输入数据一般存储在允许随机访问的array中,而不是存储在 只允许 顺序访问的list中。不过,很多排序算法经过修改,则即可以排序array中的数据,也可以排序list中的数据
二、历史(History)
从计算机发明开始,排序算法就吸引了大量研究。
Betty Holberton (née Snyder)是1951年前后早期排序算法作者之一,她在ENIAC and UNIVAC(50年代的早期计算机)上工作。冒泡排序 早在1956就被分析研究啦。
基于比较的排序算法(comparison sorting algorithm)最少需要Ω(n log n)次比较,基于比较的排序算法的时间复杂度最好就是O(n logn)。
非比较的排序算法(non comparison sorting algorithm),例如计数排序(couting sort)性能会更好(但,需要更多的辅助空间)。
渐近最优算法(asymptotically optimal algorithm)自20世纪中叶就已经为人所知,但,新的排序算法依然被发明出来,当前广泛使用的Timsort可以追溯到2002年,而library sort(insertion sort的变体)则是在2006年首次发布
排序算法在计算机科学入门课程中被广泛使用,丰富的算法 引导出 大量核心的算法概念。例如:
1. 大O表示法(Big O notation)
2. 分治算法(divide and conquer algorithm)
3. 数据结构(例如:heap, binary tree)
4. 随机算法(randomized algorithm)
5. 最好/平均/最差 分析(best/average/worst analysis)
6. 时间空间权衡(time-space tradeoffs)
7. 上下边界(upper and lower bounds)
三、排序算法的分类(Classification)
一般可以从下面九个维度 对排序算法进行归类
一、时间复杂度(time complexity)
worst, average, best
基于比较的排序算法,对于大部分的输入,只是至少需要Ω(n logn)次比较。基于比较的排序算法,时间复杂度最好就是 O(nlogn)
串行serial(单线程)的排序算法:理想的表现是 O(n),但,average为O(n)是不可能的,较好的表现是 O(nlogn)
并行parallel(多线程) 的排序算法:最优的表现是 O(logn)。较好的表现是 O( (logn)^2 )
无论串行/并行 排序,较差的表现是O(n^2)
二、交换元素的(swap) 计算复杂度(computational complexity)
三、内存使用
有些排序算法是in-place的。什么是in-place呢?严格来说,仅需要O(1)辅助内存空间的排序算法称为是in-place排序算法;不过,有时需要 O(logn)辅助内存空间的排序算法也称为是in-place排序算法。
四、递归
有些算法 是递归的 或 非递归的;有些算法可以兼具两者(例如:归并排序merge sort就兼具两者)
五、稳定性
排序前后,关键字相等的元素保持不变的相对位置,则称排序算法是稳定的。
快排、heap sort、shell sort 都不稳定;
冒泡、insertion sort、merge sort 是稳定的
六、基于比较的排序,还是 non-comparison的排序
七、属于哪种通用的排序方法
有下面5种通用排序方法
1. 插入排序(insertion)。Shell sort(代码少,无call stack, 辅助空间O(1),嵌入式或内核、不稳定)
2. 交换排序(exchange)。冒泡、快排
3. 选择排序(selection)。heap sort(时间复杂度不变、辅助空间O(1)、不稳定)
4. 归并排序(merging)
5. 分配式排序(distribution sort)。计数排序(Counting sort)、桶排序(Bucket sort)、基数排序(Radix sort)
八、串行排序 还是 并行排序
九、可适应性。预先排序性 是否会影响到 该排序算法的运行时间。
考虑到“预先排序性”因素的算法,称为是 可适应性(adaptive)算法
四、稳定性(Stability)
(略)。请参考原文
简单理解就是:利用某个排序算法排序时,若两个元素key相同,则排序后,两个元素的相对位置 不发生 变化,则,该排序算法是稳定的。
五、基于比较的排序算法(Comparison sorts)
给出 时间复杂度 时,我们的前提是
给定list中,所有待排序元素的key长度 是恒定的,正是由于这个前提
1. comparison
2. swap
3. other needed operations
上述所有操作,都可以 在常量时间内完成
Again,基于比较的排序算法 时间复杂度 最好能达到:O(n log n)
下表中,时间复杂度和空间复杂度 用是的大O表示法,因此,对数的底并不重要。此外,log2 n意思是(log n)^2
wikipedia上这个表格非常好,点击表头还可以排序。表格见:
https://en.wikipedia.org/wiki/Sorting_algorithm#Comparison_sorts
六、非比较的排序算法(Non-comparison sorts)
该表描述的是 “non-comparison排序 算法”,包括:“整数排序算法(integer sorting)” 及 其他 non-comparison排序 算法
non-comparison排序 算法 并不限制在 O(n logn)之内,即:会更快,更好(当然,需要更多的空间啦)
下面讨论复杂度时,假设:
1. 待排序元素个数 n
2. key的大小 k
3. 数字大小 d
4. 待排序的数字的范围 r
下面复杂度都基于这个前提:k足够大 以至于 所有待排序的元素 都拥有 unique key,因此,元素的个数n << 2^k
对于 随机访问机器模型 来说,运行时间为 n*(k/d) 的算法,例如 基数排序(radix sort),其时间依然和 O(n logn)成比例,因为n不会超过(2^(k/d))。要排序更多的元素,为了将它们存入内存,则需要更大的k。
表格见:
https://en.wikipedia.org/wiki/Sorting_algorithm#Non-comparison_sorts
七、其他排序算法(Others)
(略)。请参考原文
这些排序算法比上面列出的算法要慢。这些排序算法经常是用于教学目的,用来展示如何估计算法的运行时间
表格见:
https://en.wikipedia.org/wiki/Sorting_algorithm#Others
八、流行的排序算法(Popular sorting algorithms)
尽管存在大量的排序算法,现实中使用的则是有限的几种。
1. insertion sort 广泛应用于 小数据集;
2. 对于大数据集,则使用 渐进高效的(asymptotically efficient)排序算法,主要是:快排、heap sort、merge sort
高效的实现 一般使用 混合算法(hibrid algorithm):整体排序使用 渐进高效的排序算法,然后,到达递归底部的小数据集时再使用insertion sort
高度调优的实现则使用更为精巧的变体,例如
1. java, android, python中使用的:Timsort(merge sort, insertion sort和其他逻辑的混合体)
2. C++, .NET中使用的:introsort(quick sort和heap sort的混合体)
对于有限制的数据,例如:具有固定间隔(fixed interval)的数字,则使用 分配式排序(distribution sort):计数排序(counting sort)、基数排序(radix sort)。
Bubble sort及其变体 在实践中较少使用,它更常出现在 教学和理论讨论中
1、简单排序(Simple sorts)
最简单的两种排序:insertion sort和selection sort。
共同点:它俩 在小数据集上,都很高效(因为overhead很小),但,在大数据集上都不高效。
insertion sort的优点:实践中,insertion sort更快(Shell sort),因为它的比较次数更少;在差不多已排序好的数据集上也是insertion sort有更好的性能。所以,实践中更倾向于使用insertion sort。正是由于insertion sort在 小或差不多已排序好的 数据集上 相对高效,因此,insertion sort经常作为 混合算法的一部分
selection sort的优点:selection sort的好处是写的次数少(use fewer writes),所以,存在写性能是受限的因素时则使用selection sort
2、高效排序(Efficient sorts)
1. Merge sort
合并两个已排序的list是很容易的,merge sort正是利用了这一点,它把已排序的list合并成一个新的有序list。首先比较2个元素(1和2、3和4、5和6......),如果第一个更大,则交换顺序,这样得到了具有2个元素的已排序的多个list,再把具有2个元素的list合并成4个元素的list,再把4个元素的list合并成8个元素的list......最终,把最后的两个list合并成最终的有序list。
Merge sort有很好的可伸缩性,可用来排序极大的list,它worst-case时间复杂度是O(n logn)。除了应用于array,它还可以很容易的应用于list,因为它只需要顺序访问(sequential access),无需随机访问(random access)。但,它需要O(n)的空间复杂度。"too young too simple, sometimes naive"实现的简单merge sort会有大量copy出现。
最近些年,merge sort在实际使用的排序算法中很是流行,因为它被用到了精巧的Timsort中。而Timsort在Python和Java(从JDK7开始)中作为标准的排序算法被提供。在Perl中,和其他算法一样merge sort本身也作为单独的标准排序算法。Merge sort出现在Java中,则是从2000 JDK1.3开始的
2. Heap sort
Heap sort是selection sort的一个更为高效的版本。它的工作方式是 “决定list中最大(或最小)的元素,将该元素放置在list的末尾(或首部),然后,继续处理list中余下部分”,但,使用heap的数据结构高效完成该任务(heap是一种特殊类型的二叉树),很是高效。
一旦数据list已经被构建成了heap,root节点确保就是最大(或最小)元素,当root节点的元素被移除或放置到list末尾时,再重新组织heap,从而确保最大的元素依然移动到root的位置。
使用heap,查找下一个最大的元素,其时间复杂度是O(logn),作为对比,线性扫描的简单selection sort需要O(n)。
Heap sort的时间复杂度是O(n logn),这也是worst-case的时间复杂度
3. Quick sort
Quick sort是分治算法(divide and conquer algorithm),它依赖于分区操作(partition operation):要分区一个array,需要选出一个称为pivot的元素。< pivot的元素放到pivot前,>pivot的元素放到pivot后,这可以在线性时间内高效完成,且只需要O(logn)辅助空间。大和小的两个sublist再递归的进行排序。
quick sort average是O(n logn),且,很低的overhead,因此,quick sort很是流行。
quick sort的高效实现一般都是 不稳定排序,且比较复杂,但,它是实践中最快的排序算法之一。再加上quick sort的空间复杂度也较小O(logn),因此,它是最流行的排序算法之一,在很多的标准编程库中都有提供
使用quick sort最大的一个警告是,worst-case是O(n^2),尽管较为少见,在"too young, too simple, sometimes naive"的实现中(例如:选择第一或最后一个元素作为pivot),对于已排序的数据,时间复杂度就是O(n^2),而这是很常见的情况。
对于quick sort,最复杂的问题就是选择一个好的pivot,因为低劣的pivot会导致性能急剧的降低到O(n^2),而好的pivot则是O(n logn),这是渐进高效算法中最好的性能。例如,如果每一步都选择的是中位数,则就是O(n logn)。而,找到中位数却是O(n)的,这带来了显著的overhead。
实践中,选择随机的pivot几乎总能产生O(n logn)的性能。
4. Shell sort
Shell sort是Donald Shell于1959年发明的。它在insertion sort上改进而来,在移动乱序的元素时每次移动多个位置。Shell sort背后的概念是insertion sort需要执行O(kn)次,k是两个乱序元素之间的最大距离。这意味着,一般性情况下其时间复杂度是O(n^2),对大部分已排序的数据集,该算法执行很快。所以,首先排序较远的元素,然后,逐渐收缩待排序元素间的gap,最终的后面的排序会更快。Shell sort的一种实现可以描述为在二维数组中安排数据序列,然后,使用insertion sort排序二维数组的column。
Shell sort的worst-case时间复杂度是一个开放问题,取决于所使用的gap sequence,该复杂度的范围是O(n^2)到O(n^(4/3))和O(n (logn)^2)。
Shell sort需要的辅助空间为O(1),且只需较少代码,无需call stack,因此,Shell sort适用于内存金贵的地方,例如嵌入式系统或OS内核
3、冒泡排序及其变体(Bubble sort and variants)
Bubble sort是简单但非常低效的排序算法。它经常见于教学或理论分析,实践中很少使用
4、分配式排序(Distribution sort)
把待排序数据分配到多个中间结构中,然后,再从中间结构中收集结果,放进输出。任何这样实现的算法都是分配式排序算法。例如,bucket sort和flash sort都是分配式的算法。分配式排序算法运行在一个处理器上,也可以作成分布式算法,分割的不同数据集运行在不同的处理器上,然后再合并结果。这可以对 无法装入内存超大数据 进行外部排序
1. Counting sort
当每个输入都已知属于某一个特定的集合时S,可以使用计数排序(Counting sort),其:
时间复杂度O(|S|+n)
空间复杂度O(|S|)
其中n为输入的长度
创建一个 integer array,其长度为|S|,使用第 ith 个位置 来计数 S中的 第 ith 个元素的出现次数。通过增加相应位置的数字来记录每一个输入的次数。然后,遍历integer array,对数据进行按序排练。这种排序算法通常无法使用,因为S需要足够小才能使算法高效,但是它非常快,并且随着n的增加表现出极大的渐近行为。还可以对其进行修改变成稳定排序
2. Bucket sort
Bucket sort是分治算法(divide and conquer),通过分区array为有限数量个bucket,它一般化了counting sort。每一个bucket再单独排序,或者使用另外一种不同的排序算法,或者递归的应用bucket算法。
当 数据集的元素 平均的分布到 所有桶中时,bucket sort工作的最好
3. Radix sort
Radix sort通过处理每一个单独的数字(individual digits)来进行排序。n个number,每一个number由k个数字组成,则时间复杂度为O(n*k)。
Radix sort可以从“最低位数位LSD”,也可以从“最高位数位MSD”开始处理每一个number的数字。
LSD首先根据最低位数字排序list,同时使用稳定排序保留元素的相对位置,然后,再根据下一个数字排序list,一直进行下去直到最高位数字,最终得到排序的list。
LSD需要使用稳定排序,而,MSD无需稳定排序(当然,如果使用MSD且期待最终是稳定排序,此时可以使用稳定排序)。in-place MSD radix sort是不稳定的。
Radix sort内部经常使用counting sort。混合排序方式,例如使用insertion sort排序更小的bins,显著的提高了Radix sort的性能
九、内存使用模式和索引排序(Memory usage patterns and index sorting)
如果待排序array的大小接近或超过内存时,就要使用速度会慢的多的磁盘或swap,此时,排序算法的内存使用模式就变的很重要,当array完全塞入内存时工作的非常高效的排序算法可能变的完全不可用。在这种场景下,比较的总次数变的相对不重要,是内存块和磁盘间copy的次数决定了算法的性能。因此,内存块和磁盘间copy的次数和定位元素的花费 比 比较的次数更为重要,因为元素间的比较发生在系统总线(system bus)或者如果存在cache时比较的速度甚至就是CPU速度,相比磁盘速度,元素间比较简直就是瞬时的
例如:著名的递归quick sort,当内存足够时,提供了非常好的性能,但,由于需要递归地复制数组的区域的,当元素无法完全装入内存时,这种算法就变的不实际啦,因为,这会导致磁盘间的大量慢copy或移动操作。在这种情形下,其他算法可能更合适,尽管比起quick sort这种算法需要更多次的比较
规避该问题的一种方式是为待排序的array创建index,然后,对index而不是整个array进行排序。在排序复杂记录,且,复杂记录的key相对较小,例如排序关系数据库的数据行时,这种方式工作的很好。整个array排序则通过一遍读取index来生成(one pass read)。因为index比array要小的多,索引可以完全装入内存,而整个array并不能完全装入内存,从而有效地剔除了磁盘交换问题。有时,这种方式也被称为“标记排序(tag sort)”
应对内存问题的另外一种方式是使用“外部排序(external sorting)”,例如,其中一种方式就是糅合两种排序算法,采用两种算法的优点,提高整体性能。举个例子,把array分割成可装入内存的小块,对每个小块使用高效排序算法(例如快排),然后,对每个小块排序得到的结果,再使用“多路归并”算法(例如类似于merge sort的k-way merge)。比起直接在整个array上使用merge sort或quick sort,糅合两种算法的方式会更快
还可以多种算法糅合使用。排序大大超出系统内存的极大数据集时,即使对index进行排序,也需要使用设计为有效使用虚拟内存的算法或算法组合,用以降低磁盘交换的次数
十、相关算法(Related algorithms)
相关的问题包括
1. 部分排序(partial sorting)
排序list中前k个最小元素,或找出list中前k个最小元素但无需排序
2. 选择(selection)
找出第kth小的元素
这两种问题都可以使用全序解决,但,这种方式不够高效,存在更高效的算法,这些算法通常是将排序算法一般化而得到的。最著名的例子是和quick sort相关的“快速选择(quick select)”。相反的,有些排序算法则可以通过重复应用选择算法而得到。quick sort和quick select都可以看做同样的pivot移动,不同点只在于是否在两侧进行递归。Quick sort在两侧进行递归,是分治思想的算法(divide and conquer);quick select则只在一侧进行递归,是减治思想的算法(decrease and conquer)
和排序算法相反的一种算法则是洗牌算法(shuffling algorithm)。这些算法和排序算法存在根本不同,因为洗牌算法需要随机数源(source of random numbers)。洗牌也可以使用排序算法实现,也就是随机排序:为list中每个元素分配一个随机数,然后,对分配的随机数进行排序。实践中,一般不这么做,但,存在一个有名的简单且高效洗牌算法,称为是Fisher-Yates shuffle
完。
哦,one more thing,在大学时好好学习专业课,看起来,没有比这个收益更大的啦
![](https://i-blog.csdnimg.cn/blog_migrate/387e31a1c39cbecc893f3aeed46d5a05.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2da2b97f2a36b9b42df202a362749f22.png)
Enjoy