概述
本文对比较常用且比较高效的排序算法进行了总结和解析,并贴出了比较精简的实现代码,包括选择排序、插入排序、归并排序、希尔排序、快速排序等。算法性能比较如下图所示:
选择排序
种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者。
总的来说,选择排序是一种很容易理解和实现的简单排序算法,它有两个很鲜明的特点:
● 运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶地发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!我们将会看到,其他算法会更善于利用输入的初始状态。
● 数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了N次交换一一交换次数和数组的大小是线性关系。我们将研究的其他任何算法都不具备这个特征(大部分的增长数量级都是线性对数或是平方级别)。
● 运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶地发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!我们将会看到,其他算法会更善于利用输入的初始状态。
● 数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了N次交换一一交换次数和数组的大小是线性关系。我们将研究的其他任何算法都不具备这个特征(大部分的增长数量级都是线性对数或是平方级别)。
package sorting.algorithms; /** * 选择排序 * @author Kevin * */ public class SelectedSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void selectSorting(Comparable[] array){ //将数组按升序排列 int len = array.length; //计算数组长度 for(int i=0;i<len;i++){ //将a[i]和a[i+1...N]中最小的元素交换 int min = i; //最小元素的索引 for(int j=i+1;j<len;j++){ if(less(array[j], array[min])) min = j; exch(array, i, min); } show(array); } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; selectSorting(array); show(array); } }
冒泡排序
冒泡排序(Bubble Sort,译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故名。
1、比较相邻的元素。如果第一个比第二个大(小),就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大(小)的数。
3、针对所有的元素重复以上的步骤,除了最后已经选出的元素(有序)。
4、持续每次对越来越少的元素(无序元素)重复上面的步骤,直到没有任何一对数字需要比较,则序列最终有序。
package sorting.algorithms; /** * 冒泡排序 * @author Kevin * */ public class BubbleSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void sort(Comparable[] array){ //将数组按升序排列 //外层循环控制趟数,总趟数为len-1 int len = array.length; for(int i=0;i<len-1;i++){ //内层循环为当前i趟数 所需要比较的次数 for(int j=0;j<len-i-1;j++){ if(less(array[j+1], array[j])) exch(array, j, j+1); } // show(array); } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
改进后的冒泡排序
package sorting.algorithms; /** * 冒泡排序 * @author Kevin * */ public class BubbleSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void sort(Comparable[] array){ //将数组按升序排列 //记录是否发生了置换, 0 表示没有发生置换、 1 表示发生了置换 int isChange; //外层循环控制趟数,总趟数为len-1 int len = array.length; for(int i=0;i<len-1;i++){ //内层循环为当前i趟数 所需要比较的次数 //每比较一趟就重新初始化为0 isChange = 0; for(int j=0;j<len-i-1;j++){ if(less(array[j+1], array[j])){ exch(array, j, j+1); //如果进到这里面了,说明发生置换了 isChange = 1; } } //如果比较完一趟没有发生置换,那么说明已经排好序了,不需要再执行下去了 if (isChange == 0) break; show(array); } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
插入排序
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。这种算法叫做插入排序。
与选择排序一样,当前索引左边的所有元素都是有序的,但它们的最终位置还不确定,为了给更小的元素腾出空间,它们可能会被移动。但是当索引到达数组的右端时,数组排序就完成了。和选择排序不同的是,插人排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
与选择排序一样,当前索引左边的所有元素都是有序的,但它们的最终位置还不确定,为了给更小的元素腾出空间,它们可能会被移动。但是当索引到达数组的右端时,数组排序就完成了。和选择排序不同的是,插人排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
package sorting.algorithms; /** * 插入排序 * @author Kevin * */ public class InsertSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void sort(Comparable[] array){ //将数组按升序排列 int len = array.length; for(int i=1;i<len;i++){ //将a[i]插入到a[i-1]、a[i-2]、a[i-3]、a[i-4]...之中 for(int j=i;j>0 && less(array[j], array[j-1]);j--){ exch(array, j, j-1); } show(array); } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
希尔排序
为了展示初级排序算法性质的价值,接下来我们将学习一种基于插入排序的快速的排序算法。对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能够将数组排序。这就是希尔排序。已下算法的实现使用了序列1/2(3-1),从N3开始递减至1。我们把这个序列称为递增序列。下面的算法实时计算了它的递增序列,另一种方式是将递增序列存储在一个数组中。
希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能够将数组排序。这就是希尔排序。已下算法的实现使用了序列1/2(3-1),从N3开始递减至1。我们把这个序列称为递增序列。下面的算法实时计算了它的递增序列,另一种方式是将递增序列存储在一个数组中。
package sorting.algorithms; /** * 希尔排序 * @author Kevin * */ public class ShellSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void sort(Comparable[] array){ //将数组按升序排列 int len = array.length; int h = 1; while(h < len/3) h = 3*h+1; //1,4,13,40,121,364,1093... while(h >= 1){ //将数组变为h有序 for(int i=h;i<len;i++){ //将a[i]插入到a[i-h]、a[i-2*h]、a[i-3*h]...之中 for(int j=i;j>=h && less(array[j], array[j-h]);j-=h) exch(array, j, j-h); } h = h/3; } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
归并排序
实现归并的一种直截了当的办法是将两个不同的有序数组归并到第三个数组中,两个数组中的元素应该都实现了 Comparable接口。实现的方法很简单,创建一个适当大小的数组然后将两个输入数组中的元素一个个从小到大放入这个数组中。
但是,当用归并将一个大数组排序时,我们们需要进行很多次归并,因此在每次归并时都创建个新数组来存储排序结果会带来问题。我们更希望有一种能够在原地归并的方法,这样就可以先将前半部分排序,再将后半部分排序,然后在数组中移动元素而不需要使用额外的空间。你可以先停下来想想应该如何实现这一点,年一看很容易做到,但实际上已有的实现都非常复杂,尤其是和使用额外空间的方法相比。
尽管如此,将原地归并抽象化仍然是有帮助的。与之对应的是我们的方法签名 merge(a,1o,mid,hi),它会将子数组a[1o .. mid]和a[mid+1 .. hi]归并成一个有序的数组并将结果存放在
a[1o .. hi]中。下面的代码只用几行就实现了这种归并。它将涉及的所有元素复制到一个辅助数组中,再把归并的结果放回原数组中。
但是,当用归并将一个大数组排序时,我们们需要进行很多次归并,因此在每次归并时都创建个新数组来存储排序结果会带来问题。我们更希望有一种能够在原地归并的方法,这样就可以先将前半部分排序,再将后半部分排序,然后在数组中移动元素而不需要使用额外的空间。你可以先停下来想想应该如何实现这一点,年一看很容易做到,但实际上已有的实现都非常复杂,尤其是和使用额外空间的方法相比。
尽管如此,将原地归并抽象化仍然是有帮助的。与之对应的是我们的方法签名 merge(a,1o,mid,hi),它会将子数组a[1o .. mid]和a[mid+1 .. hi]归并成一个有序的数组并将结果存放在
a[1o .. hi]中。下面的代码只用几行就实现了这种归并。它将涉及的所有元素复制到一个辅助数组中,再把归并的结果放回原数组中。
自顶向下的归并排序
package sorting.algorithms; /** * 自顶向下的归并排序 * @author Kevin * */ public class MergeSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void merge(Comparable[] array, int lo, int mid, int hi){ //将a[lo..mid]和a[mid+1..hi]归并 int i = lo,j = mid+1; for(int k=lo;k<=hi;k++) //将a[lo..hi]复制到aux[lo..hi] aux[k] = array[k]; for(int k=lo;k<=hi;k++) //归并回到a[lo..hi] if(i>mid) array[k] = aux[j++]; else if(j>hi) array[k] = aux[i++]; else if(less(aux[j], aux[i])) array[k] = aux[j++]; else array[k] = aux[i++]; } //归并所需的辅助数组 private static Comparable[] aux; public static void sort(Comparable[] array){ aux = new Comparable[array.length]; //一次性分配空间 sort(array,0,array.length-1); } public static void sort(Comparable[] array, int lo, int hi){ //将数组按a[lo..hi]排列 if(hi <= lo) return ; int mid = lo+(hi - lo)/2; sort(array,lo,mid); //将左半边排序 sort(array, mid+1, hi); //将右半边排序 merge(array, lo ,mid, hi); //归并结果 } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
自底向上的归并排序
package sorting.algorithms; /** * 自底向上的归并排序 * @author Kevin * */ public class MergeBUSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } public static void merge(Comparable[] array, int lo, int mid, int hi){ //将a[lo..mid]和a[mid+1..hi]归并 int i = lo,j = mid+1; for(int k=lo;k<=hi;k++) //将a[lo..hi]复制到aux[lo..hi] aux[k] = array[k]; for(int k=lo;k<=hi;k++) //归并回到a[lo..hi] if(i>mid) array[k] = aux[j++]; else if(j>hi) array[k] = aux[i++]; else if(less(aux[j], aux[i])) array[k] = aux[j++]; else array[k] = aux[i++]; } //归并所需的辅助数组 private static Comparable[] aux; public static void sort(Comparable[] array){ //进行lgN次两两归并 int N = array.length; aux = new Comparable[N]; for(int sz=1;sz<N;sz+=sz) //sz子数组大小 for(int lo=0;lo<N-sz;lo+=sz+sz) //lo:子数组索引 merge(array, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1)); } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
快速排序
快速排序可能是应用最广泛的排序算法了。快速排序流行的原因是它实现简单、适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。快速排序引引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需的时间和MgN成正比。我们已经学习过的排序算法都无法将这两个优点结合起来。另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。它的主要缺点是非常脆弱,在实现
时要非常小心才能避免低劣的性能。已经有无数例子显示许多种错误都能致使它在实际中的性能只有平方级别。幸好我们将会看到,由这些错误中学到的教训也大大改进了快速排序算法,使它的应用更加广泛。
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分为两半;在快速排序中,切分(partition)的位置取决于数组的内容。
时要非常小心才能避免低劣的性能。已经有无数例子显示许多种错误都能致使它在实际中的性能只有平方级别。幸好我们将会看到,由这些错误中学到的教训也大大改进了快速排序算法,使它的应用更加广泛。
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分为两半;在快速排序中,切分(partition)的位置取决于数组的内容。
package sorting.algorithms; /** * 快速排序 * @author Kevin * */ public class QuickSorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } //快速排序的切分 private static int partition(Comparable[] array, int lo, int hi){ //将数组切分为a[lo..i-1],a[i],a[i+1..hi] int i = lo,j = hi+1; //左右扫描指针 Comparable v = array[lo]; //切分元素 while(true){ //扫描左右,检查扫描是否结束并交换元素 while(less(array[++i], v)) if(i == hi) break; while(less(v, array[--j])) if(j == lo) break; if(i >= j) break; exch(array, i, j); } exch(array,lo,j); //将v = a[j]放入正确的位置 return j; //a[lo..j-1] <= a[j] <= a[j+1..hi]达成 } public static void sort(Comparable[] array){ sort(array,0,array.length-1); } public static void sort(Comparable[] array,int lo,int hi){ if(hi <= lo) return ; int j = partition(array, lo, hi); //切分 sort(array,lo,j-1); //将左半部分a[lo..j-1]排序 sort(array,j+1,hi); //将右半部分a[j+1..hi]排序 } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }
改进后的快速排序
package sorting.algorithms; /** * 改进快速后的排序 * @author Kevin * */ public class Quick3waySorting { //交换位置 public static void exch(Comparable[] array,int i,int j){ Comparable temp =array[i]; array[i] = array[j]; array[j] = temp; } //比较v是否小于w public static boolean less(Comparable v,Comparable w){ return v.compareTo(w)<0; } //打印数组 public static void show(Comparable[] array){ for (Comparable num : array) { System.out.print(num+" "); } System.out.println(); } //快速排序的切分 private static int partition(Comparable[] array, int lo, int hi){ //将数组切分为a[lo..i-1],a[i],a[i+1..hi] int i = lo,j = hi+1; //左右扫描指针 Comparable v = array[lo]; //切分元素 while(true){ //扫描左右,检查扫描是否结束并交换元素 while(less(array[++i], v)) if(i == hi) break; while(less(v, array[--j])) if(j == lo) break; if(i >= j) break; exch(array, i, j); } exch(array,lo,j); //将v = a[j]放入正确的位置 return j; //a[lo..j-1] <= a[j] <= a[j+1..hi]达成 } public static void sort(Comparable[] array){ sort(array,0,array.length-1); } public static void sort(Comparable[] array,int lo,int hi){ if(hi <= lo) return ; int lt = lo,i = lo+1,gt = hi; Comparable v = array[lo]; while(i <= gt){ int cmp = array[i].compareTo(v); if(cmp < 0) exch(array, lt++, i++); else if(cmp > 0) exch(array, i, gt--); else { i++; } //此时a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]成立 sort(array,lo,lt-1); //将左半部分a[lo..j-1]排序 sort(array,gt+1,hi); //将右半部分a[j+1..hi]排序 } } public static void main(String[] args) { // TODO Auto-generated method stub Comparable[] array = {5, 69, 12, 3, 56, 79, 2, 58, 23}; sort(array); show(array); } }