补充几个基本概念:
冒泡排序的思想是:每次比较两个元素,如果他们的顺序错误就把他们交换过来,则可以理解,每一次遍历都可以把最小或最大的元素通过慢慢交换从而“浮”到数组的一端。时间复杂度为O(n2)(从一个公众号“小黑格子屋”的一篇文章里盗来了演示图,包括下面所介绍的其他算法的动态演示图都来源于此),
算法步骤:
最好情况:当数组已经是正序的时候,速度最快;
最坏情况:当数组是反序的时候,速度最慢。
public class Bubble {
public static void sort(Comparable[] a) {
int N = a.length;
for(int i = 1; i < N; i++) {
for(int j = 0; j < N - i; j++)
if(a[j].compareTo(a[j + 1]) > 0) {
Comparable temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
这是一种比较简单的排序算法,遍历一次,把最小的数放在第一个位置;遍历第二次,把第二小的放在第二个位置……无论什么数据进去都是O(n2)的时间复杂度,所以数据规模越小越好,选择排序唯一的好处就是不占用额外的内存了。
算法步骤:
public class Selection {
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 (a[j].compareTo(a[min]) < 0)
min = j;
Comparable temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
插入排序的思想是一次扫描每一个元素,将它们放到合适的位置,这样说似乎有点抽象,那举个例子吧,就想打扑克牌一样,我们在整理自己手牌的时候采用的就是这样的插入排序。插入排序与选择排序不同,插入排序所需的时间取决于输入数组的初始顺序,平均时间复杂度为O(n2)。
算法步骤:
public class Insertion {
public static void sort(Comparable[] a) {
int N = a.length;
for(int i = 1; i < N; i++) {
int j = i;
while(j > 0 && a[j].compareTo(a[j - 1]) < 0) {
Comparable temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
j--;
}
}
}
}
上面的三种排序都是基本的初级排序算法,现在我们讲的这种是基于插入排序的快速地排序算法,希尔排序比插入排序更加高效,但希尔排序是一种非稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序高效的原因是它权衡了子数组的规模和有序性。
希尔排序的基本思想是:先将整个待排序的数组分割成若干个序列分别进行直接插入排序,带整个序列已经基本有序时,再对全体进行一次插入排序。
算法步骤:
举个例子:
下图显示了增量为4时对包含10个数组元素进行排序的第一个步骤,首先对下标为 0,4,8 的元素进行排序,完成排序之后,算法右移一步,对 1,5,9 号元素进行排序,依次类推,直到所有的元素完成一趟排序,也就是说间隔为4的元素都已经排列有序。
希尔的原稿中,他建议间隔选为N/2,也就是每一趟都将排序分为两半,因此对于N=100的数组,逐渐减小的间隔序列为:50,25,12,6,3,1。这个方法的好处是不需要在开始排序前为找到初始序列的间隔而计算序列,只需要用2整除N。但是这已经被证明并不是最好的序列。
还有一种很常用的间隔序列:knuth 间隔序列 3h+1。
但是无论是什么间隔序列,最后必须满足一个条件,就是逐渐减小的间隔最后一定要等于1,因此最后一趟排序一定是简单的插入排序。
下面这段代码就是通过knuth间隔序列来实现希尔排序:
public class Shell {
public static void sort(Comparable[] a) {
int N = a.length;
int h = 1; //h为插入排序的间隔
while(h < N/3)
h = 3*h + 1;
while(h >= 1) {
for(int i = h; i < N; i++) {
int j = i;
while(j >= h && a[j].compareTo(a[j-h]) < 0) {
Comparable temp = a[j];
a[j] = a[j-h];
a[j-h] = temp;
j = j - h;
}
}
h = h/3;
}
}
}
归并排序是建立在归并操作上的一种高效的排序算法,归并操作是指将两个有序的数组归并成一个更大得有序数组。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
算法步骤:
public class Merge {
private static Comparable[] aux;
public static void sort(Comparable[] a) {
aux = new Comparable[a.length];
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if(hi <= lo)
return;
int mid = lo + (hi - lo)/2;
sort(a, lo, mid);
sort(a, mid+1, hi);
merge(a, lo, mid, hi);
}
private static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid+1;
for(int k = lo; k <= hi; k++)
aux[k] = a[k];
for(int k = lo; k <= hi; k++)
if(i > mid)
a[k] = aux[j++];
else if(j > hi)
a[k] = aux[i++];
else if(aux[j].compareTo(aux[i]) < 0)
a[k] = aux[j++];
else
a[k] = aux[i++];
}
}
快速排序是一种分而治之的思想在排序算法上的典型应用。从本质上看,快速排序应该算是在冒泡排序基础上的递归分治法。
算法步骤:
再稍微解释一下代码中的切分操作(partition),我刚开始看也是云里雾里的,后来和同门交流讨论了一下才弄清楚,下面的代码来自于《算法(第四版)》,切分中用到了双指针技术,先将待排序数组的第一个元素作为基准元素 v,再设置两个指针(这个指针不是C/C++的那个能指向地址的指针,Java中不能直接操作指针,这里就简单理解为数组中两个元素的位置就好),一个从前往后开始扫描,找到一个大于v的元素后,记录当前 i 的值,跳出循环;另一个从后往前开始扫描,找到一个小于v的元素后,记录当前 j 的值,跳出循环;然后交换 i 和 j 上的两个元素,最后再将基准元素 v 放到正确的位置上,一次切分就完成了。
如果每次能正好地将数组对半分,这是快速排序的最好算法,,因为这种情况下快速排序的比较次数正好能满足分治递归的公式。
虽然最坏情况下的时间复杂度达到了 O(n²),平均复杂度为O(nlogn),但是在大多数情况下都比平均时间复杂度为 O(nlogn) 的排序算法表现要更好。
public class Quick {
private static void sort(Comparable[] a) {
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if(hi <= lo)
return;
int j = partition(a, lo, hi);
sort(a, lo, j-1);
sort(a, j+1, hi);
}
private static int partition(Comparable[] a, int lo, int hi) {
int i = lo, j = hi+1;
Comparable v = a[lo];//v作为基准
while(true) {
while(a[++i].compareTo(v) < 0)//从头开始,找到一个大于v的元素后,记录当前i的值,跳出循环
if(i == hi)
break;
while(v.compareTo(a[--j]) < 0)//从后开始,找到一个小于v的元素后,记录当前j的值,跳出循环
if(j == lo)
break;
if(i >= j)
break;
Comparable temp = a[i];//交换i和j上的元素
a[i] = a[j];
a[j] = temp;
}
Comparable temp = a[lo];//最后一次交换,将基准放到正确的位置
a[lo] = a[j];
a[j] = temp;
return j;//将基准的位置返回
}
}
还有几个排序算法没总结,未完待续……