1.选择排序
选择排序步骤:
- 从左至右遍历,找到最小(大)的元素,然后与第一个元素交换。
- 从剩余未排序元素中继续寻找最小(大)元素,然后与第二个元素进行交换。
- 以此类推,直到所有元素均排序完毕。
2.插入排序
插入排序步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素小于前面的元素(已排序),则依次与前面元素进行比较如果小于则交换,直到找到大于该元素的就则停止;
- 如果该元素大于前面的元素(已排序),则重复步骤2
- 重复步骤2~4 直到所有元素都排好序 。
插入排序需要的交换操作和数组中倒置的数量相同,需要比较的次数大于等于倒置数量,小于等于倒置的数量加上数组的大小再减一。
事实上,当倒置的数量很少的时候,插入排序可能比我们接触到的其他任何算法都要快。
插入排序比较慢的原因是每次只移动一个位置,所以我们提出了希尔排序,它是插入排序的一个改进。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素对数组的局部进行排序。
3.希尔排序
希尔排序原理:
希尔排序也称之为递减增量排序,他是对插入排序的改进。在第二部插入排序中,我们知道,插入排序对于近似已排好序的序列来说,效率很高,可以达到线性排序的效率。但是插入排序效率也是比较低的,他一次只能将数据向前移一位。比如如果一个长度为N的序列,最小的元素如果恰巧在末尾,那么使用插入排序仍需一步一步的向前移动和比较,要N-1次比较和交换。
希尔排序通过将待比较的元素划分为几个区域来提升插入排序的效率。这样可以让元素可以一次性的朝最终位置迈进一大步,然后算法再取越来越小的步长进行排序,最后一步就是步长为1的普通的插入排序的,但是这个时候,整个序列已经是近似排好序的,所以效率高。
如下图,我们对下面数组进行排序的时候,首先以4为步长,这是元素分为了LMPT,EHSS,ELOX,AELR几个序列,我们对这几个独立的序列进行插入排序,排序完成之后,我们减小步长继续排序,最后直到步长为1,步长为1即为一般的插入排序,他保证了元素一定会被排序。
希尔排序的增量递减算法可以随意指定,可以以N/2递减,只要保证最后的步长为1即可。
希尔排序的分析比较复杂,使用Hibbard’s 递减步长序列的时间复杂度为O(N3/2),平均时间复杂度大约为O(N5/4) ,具体的复杂度目前仍存在争议。
4.归并排序
归并排序步骤:
- 申请空间,使其大小为两个已经排序序列之和,然后将待排序数组复制到该数组中。
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较复制数组中两个指针所指向的元素,选择相对小的元素放入到原始待排序数组中,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到原始数组末尾
归并排序有两种方式:自顶向下和自底向上。自顶向下是分治思想,采用递归。自底向上是非递归的方式。归并排序时间复杂度:O(nlgn),归并排序需要额外的长度为N的辅助空间来完成排序,现在也有人已经做出了归并排序的就地排序算法,但是实现比较复杂。归并排序是一种稳定性排序,也就是说排序关键字相等的两个元素在整个序列排序的前后,相对位置不会发生变化,这一特性使得归并排序是稳定性排序中效率最高的一个。在Java中对引用对象进行排序,Perl、C++、Python的稳定性排序的内部实现中,都是使用的归并排序。
归并排序的改进:
- 当划分到较小的子序列时,通常可以使用插入排序替代合并排序
- 如果已经排好序了就不用合并了
- 并行化
5.快速排序
快速排序步骤:
- 对数组进行随机化。
- 从数列中取出一个数作为中轴数(pivot)。
- 将比这个数大的数放到它的右边,小于或等于它的数放到它的左边。
- 再对左右区间重复第三步,直到各区间只有一个数。
如上图所示快速排序的一个重要步骤是对序列进行以中轴数进行划分,左边都小于这个中轴数,右边都大于该中轴数,然后对左右的子序列继续这一步骤直到子序列长度为1。
下面来看某一次划分的步骤,如下图:
上图中的划分操作可以分为以下5个步骤:
- 获取中轴元素
- i从左至右扫描,如果小于基准元素,则i自增,否则记下a[i]
- j从右至左扫描,如果大于基准元素,则i自减,否则记下a[j]
- 交换a[i]和a[j]
- 重复这一步骤直至i和j交错,然后和基准元素比较,然后交换。
快速排序为什么快?
——《算法艺术与信息学竞赛》
——《算法 第四版》p185
——《算法第四版》p187
——《算法第四版》p219
算法改进:
1.切换到插入排序
小数组(5~15之间任意数值在大部分情况下都挺令人满意的)时应该切换到插入排序:
1.)对于小数组,快速排序比插入排序慢;
2.)因为递归,快速排序的sort()方法在小数组中也会调用自己。
2.三取样切分
为了避免最坏的情况发生,我们取样大小设为3,并用大小居中的元素来切分,我们还可以将取样元素放在数组末尾作为“哨兵”来去掉数组边界测试。
3.熵最优排序
为了硬度大量重复元素的数组,比如我们可能需要将大量人员资料按照生日进行排序,在这样的情况下,一个元素全部重复的数组就不需要排序了,但我们的算法仍然进行切分排序。我们的想法是将数组切分为三部分,分别对应小于、等于和大于切分元素的数组元素。
4.并行化
和前面讨论对合并排序的改进一样,对所有使用分治法解决问题的算法其实都可以进行并行化。
6.堆排序
堆排序步骤:
1. 使用序列的所有元素,创建一个最大堆。
2. 然后重复删除最大元素。
在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。堆排序就是优先队列的一个好的实现方式。
数组[0]的位置我们不存放数据,元素k的父节点所在的位置为[k/2],元素k的子节点所在的位置为2k和2k+1。
堆排序优点:
1.就地排序,并且其最坏情况下时间复杂度为NlogN。经典的归并排序不是就地排序,它需要线性长度的额外空间,
2.最坏情况也是NlogN,而快速排序其最坏时间复杂度为N2。
堆排序缺点:
1. 其内部循环要比快速排序要长。
2. 并且其操作在N和N/2之间进行比较和交换,当数组长度比较大的时候,对CPU缓存利用效率比较低。
3. 非稳定性排序。
二叉堆的堆排序,他是一种就地的非稳定排序,其最好和平均时间复杂度和快速排序相当,但是最坏情况下的时间复杂度要优于快速排序。但是由于他对元素的操作通常在N和N/2之间进行,所以对于大的序列来说,两个操作数之间间隔比较远,对CPU缓存利用不太好,故速度没有快速排序快。
总结:
经典练习题:
1.从一个含有10万个不同数据且任意存放的数组中找出10个最大的数据
答:从一个大数组里面找出N个最大的数据其实是通过稍稍修改后的快速排序来实现,当比对照元素大的那一边有10个元素的时候就结束排序,实际上每次排序只是针对于对照元素的一边,而完整的快速排序是针对对照元素的两边。以下是从一个大数组中找出N个最大的数,注意这个大数组中没有重复的数据。
答案参考:http://blog.csdn.net/vinckyliu/article/details/38377205
2.找出数组的中位数
答:与题目1类似,使用快速排序,切分大的一边。参见《算法 第四版》p221
参考:http://www.cnblogs.com/yangecnu/p/Introduction-Insertion-and-Selection-and-Shell-Sort.html
《算法 第四版》 Robert Sedgewick
https://www.toptal.com/developers/sorting-algorithms/