如何排序?
这一章中主要是三个比较简单的算法:冒泡排序、选择排序和插入排序。计算机程序不能像人一样通览所有的数据。它只能根据计算机的“比较”操作原理,在同一时间内对来个数据项进行比较。
这三种算法都包括如下两个步骤,这两部循环执行,直到全部数据有序为止;
1、比较两个数据项;
2、交换两个数据项,或复制其中一项。
但是,每种算法具体实现的细节有所不同。
冒泡排序
冒泡排序算法运行起来非常慢,但在概念上它是排序算法中最简单的,因此冒泡排序算法在刚开始研究排序技术时时一个非常好的算法。
冒泡排序的Java代码:
public class ArrayTest { public static void main(String[] args) { //冒牌排序法 int[] a = {34,21,5,2,3,12,56,13,37,22}; for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } System.out.println(""); for (int i = a.length-1; i >1; i--) { for (int j = 0; j < i; j++) { if(a[j]>a[j+1]){ int temp=a[j]; a[j] = a[j+1]; a[j+1] = temp; } } } for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } } } //输出: 34,21,5,2,3,12,56,13,37,22, 2,3,5,12,13,21,22,34,37,56,
这个算法的思路是要将最小的数据项放在数组的最开始(数组的下标为0),并将最大的数据项放在数组的最后(数组下标为nElems-1)。外层for循环的计数器 i 从数组的最后开始,即i等于i等于nEmels-1,没经过一次循环i减一。小标大于i的数据项都已经排好序的了。变量i在每完成一次内部循环(计数器为j)后左移一位,因此算法就不再处理那些已经排好序的数据了。
内层for循环计数器j从数组的最开始算起,即j=0,没完成一次内部循环体加一,当它等于out时结束一次循环。在内层for循环体中,数组小标为j和j+1的两个数据项进行比较,如果小标为j的数据项大于小标为j+1的数据项项,则交换两个数据项。
不变性
在许多算法中,有些条件在算法执行时时不变的。这些条件被称为不变性。这些条件被称为不变性。认识不变性对理解算法是有用的。在一定情况先他们对调试也有用;可以反复检查不变性为否为真,如果不是的话就标记出错。
在上述代码中不变性是指i右边的数据项为有序。在算法的整个运行过程中这个条件始终为真。(在第一次排序开始前,尚未排序,因为i开始时在数据项的最左边,没有数据项在i的右边。)
冒泡排序法的效率
通过分析10分数据项的数组,第一趟排序时进行了9次比较,第二趟排序进行了8次比较。如此类推,知道最后一趟进行了一次比较。对于10个数据项就是
9+8+7+6+5+4+3+2+1=45
一般来说数组中有N个数据项,则第一趟排序中有N-1次比较,第二趟中有N-2次比较,如此类推公式如下
(N-1)+(N-2)+(N-3)+…+1=N(N-1)/2
当N为10时,N*(N-1)/2等于45 (10*9/2)。
这样,算法作了约N²/2次比较(忽略减一,不会有很大的差别,特别是当N很大时)。
选择排序
选择排序改进了冒泡排序,将必要的交换次数从O(N²)减少到O(N)。不幸的是比较次数仍保持为O(N²)。然而,选择排序仍然为大记录量的排序提出了一个非常重要的改进,因为这些大量的记录需要在内存中移动,这就使交换的时间和比较的时间相比起来,交换的时间更为重要。(在Java中只是改变了引用位置,而实际对象的位置并没有发生改变。)
选择排序的Java代码
public static void main(String[] args) { //选择排序法 int[] a = {34,21,5,2,3,12,56,13,37,22}; for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } System.out.println(""); int out,in,min; for (out = 0; out < a.length-1; out++) { min = out; for (in = out+1; in < a.length; in++) { if(a[in]<a[min]){ min=in; } int temp = a[out]; a[out] = a[min]; a[min] = temp; } } for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } } //输出: //34,21,5,2,3,12,56,13,37,22, //2,5,12,3,13,21,22,37,34,56,
外层循环用循环变量out,从数组开头开始(数组下标为0)向高位增长。内层循环变量in,从out指位置开始,同样是向右移位。
在每一个in的新位置,数据项a[in]和a[min]进行比较,如果a[in]更小,则min被赋值为in的值,在内层循环的最后,min指向最小的数据项,然后out和min指向的数组数据项。
不变性
在上述代码程序中,下标小于或等于out的位置的数据项总是有序的。
选择排序的效率
选择排序和冒泡排序执行了相同次数的比较:N*(N-1)/2。对于10个数据项,需要4次比较。然而10个数据项只需要少于10次交换。对于100个数据项,需要4950次比较,但只进行了不到100次的交换。N值很大时,比较的次数是主要的,所以结论是选择排序和冒泡排序一样运行了O(N²)时间。但是,选择排序无疑更快,因为它进行交换少得多。当N值较小时,特别是如果交换的时间比比较的时间级大得多时,选择排序实际上是相当快的。
插入排序
插入排序的Java代码
public static void main(String[] args) { //插入排序 int[] a = {34,21,5,2,3,12,56,13,37,22}; for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } System.out.println(""); for (int i = 1; i < a.length; i++) { int temp = a[i]; int j = i; while(j>0&&a[j-1]>=temp){ a[j] = a[j-1]; --j; } a[j] = temp; } for (int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } } //输出: //34,21,5,2,3,12,56,13,37,22, //2,5,12,3,13,21,22,37,34,56,
在外层的for循环总,i变量从1开始,向右移动。标记了未排序部分的最左端的数据。而内层while循环中,j变量从i变量开始向左移动,直到temp变量小于j所指的数组数据项,或者它已经不能再往左移动为止。while循环的每一趟都向右移动了一个已排序的数据项。
插入排序中的不变性:
在每趟结束时,在将temp位置的项插入后,比i变量下标号小的数据项都是局部有序的。
插入排序的效率
这个算法需要多少次比较和复制呢?在第一趟排序中,他最多比较一次,第二趟最多比较两次,以此类推。最后一趟最多,比较N-1次。一次有
1+2+3+... +N-1=N*(N-1)/2
然而,因为在每一趟排序发现插入点之前,平均只有全体数据项的一半真的进行了比价,我们除以2得到
N*(N-1)/ 4
复制的次数大致等于比较的次数。然而,一次复制与一次交换的时间耗费不同,所以相对于随机数据,这个算法比冒泡排序快一倍,比选择排序略快。
对于已经有序或基本有序的数据来说,插入排序要好得多。当数据有序的时候,while循环的条件总是false所以它变成了外层循环中一个简单语句,执行N-1次。在这种情况下,算法运行只需要O(N)的时间。如果数据基本有序,插入排序几乎只需要O(N)的时间,这对把一个基本有序进行排序是一个简单而有效的方法。
然而对于逆序排列的数据,每次比较和移动都会执行,所以插入排序不比冒泡排序快。
对象排序
上述都是简单的数值排序,排序例程却更多地应用于对象排序,而不是对基本数据类型的排序。
对象排序的Java代码
public void insertSort(){ for (int i = 1; i < a.length; i++) { Person temp = a[i]; int j = i; while(j>0 && a[j-1].getLast().compareTo(temp.getLast())>0){ a[j]=a[j-1]; --j; } a[j] = temp; } }
单词排序
compareTo()方法更具两个String的字典顺序(字母序)返回给调用者不同的整数值,这两String一个是方法的调用者,一个是这个方法的参数。
稳定性
有些时候,排序要考虑数据项拥有相同关键字的情况。例如,雇员数据按雇员的姓的字典排序(排序以姓为关键字),现在又想按邮政编码排序,并希望具有相同邮政编码的数据仍然按姓排序。这宗情况下,则值需要算法对需要排序的数据进行排序,让不需要排序的数据保持原来的顺序。某些算法满足这样的要求,它们就可以成为稳定的算法。
几种简单排序之间的比较
除非手边没有算法书可以参考,一般情况下几乎不太实用冒泡排序算法。它太过于简单,以至于可以毫不费力地写出来。然而当数据量很小的时候它会有些应用价值。
选择排序当数据量很小,并且交换数据相对于比较数据更加消耗的情况下,可以应用选择排序。
当数据量比较小或基本上有序时,插入排序是这三种最好的选择。
小 结
本章提到的排序算法都假定了数组作为数据存储机构。
排序包括比较数组中数据项的关键字和移动相应数据项直到它们排好序为止。
本章所有算法的时间复杂度都是O(n²)。不过,某些情况下某个算法可以比其他算法快很多。
不变性是指在算法运行时保持不变的条件。
冒泡排序算法是效率最差的算法,但他很简单。
插入算法是本章上述介绍的O(n²)排序算法中应用最多的。
如果具有相同关键字的数据项,经过排序它们的顺序保持不变,这样的排序就是稳定的。
上述介绍的所有排序算法除了需要初始数组之外,都只需要一个临时变量