前言:
排序是将一组对象按照某种逻辑顺序重新排序的过程,比如信用卡账单中的交易是按照日期排序—这种排序很可能使用了某种排序算法,计算机早期,大家认为30%的计算周期是用在排序上,如果这种比例降低了,可能的原因之一是如今的排序算法更加高效了,而非排序的重要性降低了,现在计算机的广发使用使得数据无所不在,而整理数据的第一步通常就是排序;
即使你只使用标准库中的排序函数,学习排序算法依然有三大实际的意义;
- 对排序算法的分析,有助于理解和比较算法性能的方法;
- 类似的技术也有助于解决其他类型的问题;
- 排序通常是解决问题的第一步;
初级排序算法
下列代码展示了我们的习惯约定,将排序代码放在sort()方法中,该类还包含了辅助函数less()和exch();
public class Example {
public static void sort(Comparable[] a){
}
private static boolean less(Comparable v,Comparable w){
return v.compareTo(w)<0;
}
private static void exch(Comparable[] a,int i ,int j){
Comparable t = a[i];
a[i]=a[j];
a[j]=t;
}
private static void show(Comparable[] a){
for (int i = 0; i < a.length; i++) {
System.out.println(a[i]+" ");
}
System.out.println();
}
public static boolean isSorted(Comparable[] a){
for (int i = 1; i < a.length; i++) {
if (less(a[i],a[i-1])) return false;
}
return true;
}
}
时间复杂度分析:
结论:对于长度为n的数组,选择排序大约需要n2/2次比较和n次交换
比较 (n-1)+(n-2)+(n-3)+…+2+1=n(n-1)/2 ~n2/2次比较;n次交换,即每个索引上的数组都需要交换;
选择排序
思想:找到数组中最小的元素,其次将它和数组的第一个元素交换伪只,再次找到剩下的元素中最小的,和数组的第二个元素交换位置,如此往复,直到整个数组排序,这种方法叫选择排序;
特点:(简单数组长度为n)
- 运行时间和输入无关;一个已经有序后者主键全部相等的数组和一个元素随机排列的数组所用的排序时间一样长;
- 数据移动次数最少;只会进行n次交换,这是其他任何算法都不具备的这个特征;(大部分的增长数量级都是线性对数或是平方级别)
public static void sort(Comparable[] a){
int n = a.length;
for (int i = 0; i < n; i++) {
int min = i;
for (int j = i+1; j <n ; j++) {
if (less(a[j],a[min])) min = j;
}
exch(a,min,i);
}
}
插入排序
思想:将买一张牌插入到其他已经有序的牌中的适当位置,在计算机实现中,默认第一位是有序的,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位;这种算法叫插入排序;
public static void sort(Comparable[] a){
int n = a.length;
for (int i = 1; i < n; i++) {
for (int j = i; j < n && less(a[j],a[j-1]); j--) {
exch(a,j,j-1);
}
}
}
时间复杂度分析:最坏的情况是所有的元素都需要移动位置,最好的情况下是都不需要,即该数组已经排好序;
最坏情况下,同选择排序。n2/2次比较,最好情况下,进行n-1次比较,即不会forj这个循环;
考虑部分有序的数组;倒置是指两个顺序颠倒的元素,比如E X A M P L E 中有11对倒置,E-A X-A X-M X-P X-L M-L M-E P-L P-E L-E;如果数组中倒置的数量小于数组大小的某个背书,那么我们说这个数组是部分有序的;下面几个典型的部分有序的数组:
- 数组中的每个元素距离它的最终位置都不远;
- 一个有序的大数组接一个小数组;
- 数组中只有几个元素的位置不正确;
插入排序对这样的数组很有效,而选择排序则不然,事实上,当倒置的数量很少时,插入排序可能比其他任何算法都要快;
希尔排序
基于插入排序的快速的排序算法;对于大规模乱序数组,插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端,例如如果主键最小的元素恰好在数组的末尾,要将它挪到正确的位置就需要n-1次移动,希尔排序为了加快速度简单有效的改进了插入排序,交换不相邻的元素以及对数组的局部进行排序,并最终用插入排序将局部有序的数组排序;
核心思想,改变倒置对的数量!
如何改变倒置对的数量?–使数组任意间隔为h(自定义,如3,间隔3个)的元素是有序,这样的数组称为h有序数组;
数组:h=4
L E E A M H L E P S O L T S X R
L----------------------M----------------------P----------------------T
E----------------------H----------------------S----------------------S
E----------------------L----------------------O----------------------X
A----------------------E----------------------L----------------------R
一个数组即一个有由4个有序子数组组成的数组;
实现希尔排序的一种方法:将这h个子数组进行独立排序(算法选择插入排序);然后逐步递减h的值,最终h=1;完成排序;
代码如下:
public static void sort(Comparable[] a) {
int n = a.length;
int h = 1;
while (h < n / 3) h = 3 * h + 1;
while (h >= 1) {
for (int i = h; i < n; i++) {
for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) {
exch(a, j, j - h);
}
}
h = h / 3;
}
}
核心代码:
for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) {
exch(a, j, j - h);
}
每次比较j和j-h的值,进行了h个小组的排序后,将会h=h/3;h值缩小后,再来一轮插入排序,因倒置对数量的减少,所以对于数量过大,且随机分布的数组来说,性能将远超过简单插入排序的性能;
同时算法的性能不仅取决于h,还取决于h之间的数学性质;对于h如何选择目前尚未定论,什么是最好的;
有经验的程序员有时会选择希尔排序,因此对于中等大小的数组,它的运行时间是可以接受的;它的代码量很小,且不需要额外的内存空间;对于其他高加高效的算法,除对于很大的N,它们可能只会比希尔排序快两倍,如果你需要解决一个排序问题,而又没有系统排序的函数可用时,可以先用希尔排序,实现业务后,再考虑是否值得将其替换为其他更加发砸的排序算法;