算法 -- 几种基本排序深入探究
笔记摘要:
这里介绍了三种排序方式:冒泡排序,选择排序和插入排序。对排序的原理进行了详细分析,并对于各种排序的效率进行了
分析和总结,最后通过一个插入排序的容器类对Soilder对象数组按照姓名进行了插入排序来进行总结。
一、冒泡排序
1.“编程复杂度”很低,很容易写出代码;
2.具有稳定性,这里的稳定性是指原序列中相同元素的相对顺序仍然保持到排序后的序列,而堆排序、快速排序均不具有稳定性。
比较规则:
1.比较相邻两个元素
2.如果左边的大,则两个元素交换位置
3.指针向右移动一个位置,继续比较剩下的元素
4.当碰到第一个排定的队员后,就返回到队列的左端重新开始下一趟排序
按照该规则,一直比较到队列的最右端,可以确定的是最大的元素已经在最右边了,这就向最大的数据项每次都冒泡一样,跑到最右边,这样一趟为一次遍历。
每一次遍历进行了N-1次比较。进行最少0次,最多N-1次的交换。下次遍历,最右端的数据项就不用再遍历了,第二次遍历到N-2的位置即可依次类推。
冒泡排序的两种书写方式
方式一
public void bubbleSort(int[] arr){
for(int x=0; x<arr.length-1; x++){ //共进行N-1次遍历
for(int y=0; y<arr.length-x-1; y++){//每一次遍历后-1,这样右边都是排好序的
if(arr[y]<arr[y+1]){
int temp = arr[y];
arr[y] = arr[y+1];
arr[y+1] = temp;
}
}
}
}
方式二
public void bubbleSort(int[] arr){
int out,in;
for(out=arr.length-1;out>1;out--){//计数器从数组右端开始,每次遍历后左移一位
for(in=0;in<out;in++){//每次从最左端进行遍历
if(a[in]>a[in+1]){//比较相邻两元素
swap(in,in+1);
}
}
}
}
笔者认为我们更常见的是上面的排序方式,简单,不过我觉得下面的变量定义和取值方式,更能体现冒泡排序的思想:最右边总是已经排好序的
算法的思路
要将最小的数据项放在数组的最开始,并将最大的数据项放在数据的最后。外层for循环的计数器out从数组的最后开始,即out等于nElems-1,每经过一次循环out减一,下标大于out的数据项都已经是排序好的了。变量out在每完成依次内部循环后就左移一位,因此就不再处理那些已经排好序的数据了。
swap()方法性能影响
为了清晰起见,使用了一个独立的swap()方法来执行交换操作。实际上,使用一个独立的swap()方法不一定好,因为方法调用会增加一些额外的消耗,如果写自己使用的排序程序,最好将交换操作这段代码直接放到程序中,这样可以提高一些速度。
冒泡排序的容器类
/*
* 更复杂的程序中,数据很可能由对象组成,这里为了简单起见,使用long类型的数组a[].
* */
public class ArrayBub {
private long[] a;
private int nElems;
public ArrayBub(int max){
a = new long[max];
nElems = 0;
}
public void insert(long value){
a[nElems] = value;
nElems++;
}
public void display(){
for(int j=0;j<nElems;j++){
System.out.print(a[j]+" ");
}
System.out.println();
}
public void bubbleSort(){
int out,in;
for(out=nElems-1;out>1;out--){
for(in=0;in<out;in++){
if(a[in]>a[in+1]){
swap(in,in+1);
}
}
}
}
public void swap(int one,int two){
long temp = a[one];
a[one] = a[two];
a[two] = temp;
}
public static void main(String[] args) {
int maxSize = 100;
ArrayBub arr;
arr = new ArrayBub(maxSize);
arr.insert(33);
arr.insert(14);
arr.insert(97);
arr.insert(67);
arr.insert(11);
arr.insert(86);
arr.insert(20);
arr.display();
arr.bubbleSort();
arr.display();
}
}
冒泡排序的效率
第一趟进行了9次比较,第二趟8次……
所以对于N个数据项的比较有:
(N-1)+(N-2)+(N-3)+……+1=N*(N-1)/2
算法进行了约N^2/2次比较
因为两个数据项只有在需要的时候才交换,所以交换的次数少于比较的次数,如果数据是随机的,那么大概有一半数据需要交换,则交换次数为N^2/4
冒泡排序运行需要O( N^2)时间级别,所以这种排序算法是很慢的。
无论何时,只要看到一个循环嵌套在另一个循环里,就可以怀疑这个算法的运行时间为O(N^2)级别,外层循环执行N次,内部循环对于每一次外层循环都执行N次(或者几分之N次)。这就意味着将大约需要执行N次某个基本操作。
不变性
有些条件在算法执行时是不变的,这些条件就称为不变性。认识不变性对理解算法是有用的。
在一定情况下它们对调试也有用,可以反复地检查不变性是否为真,如果不是的话,标记就出错。
在冒泡排序中,不变性是指out右边的所有数据项为有序,在算法的整个运行过程中这个条件始终为真。
二、选择排序
选择排序改进了冒泡排序,将必要的交换次数从O(N^2)减少到O(N),不幸的是比较次数仍保持为O(N^2)。然而,选择排序仍然为大记录量的数据提出来一个非常重要的改进,因为这些大量的记录需要在内存中移动,这就使交换的时间和比较的时间相比起来,交换的时间更为重要(在java中只是改变了引用的位置,而实际对象的位置并没有发生改变)
排序规则
从最左边数据项0号开始,记下最左端的数据项的大小和下标,然后把后面的所有数据项扫描一遍,选出一个最小的数据项和最左边的数据项进行比较,如果更小,则交换位置。现在最左端的队员是有序的了,不需要再交换位置了。
下次从1号开始,进行同样的操作,依次进行,直到所有的数据项都排定。
所以选择排序是保持最左边的元素有序,而冒泡排序则是保持最右边的元素有序
public void seleceionSort(){
int out,in,min;
for(out=0;out<nElems-1;out++){
min = out;
for(in=out+1;in<nElems;in++){
if(a[in]<a[min])
min = in;
swap(out,min);
}
}
}
public void swap(int one,int two){
long temp = a[one];
a[one] = a[two];
a[two] = temp;
}
不变性
下标小于或等于out的位置的数据项总是有序的。
选择排序的效率
对于N个数据项,选择排序和冒泡排序执行了相同的比较次数:N*(N-1)/2。但是只需要少于N次的交换。N值很大时,比较次数是主要的,所以结论是选择排序和冒泡排序一样运行了O(N^2)时间,但是,选择排序无疑更快,因为它进行的交换少得多。当N值较小时,特别是如果交换的时间级比比较的时间级大得多时,选择排序实际是相当快的。
三、插入排序
插入排序仍然需要O(N^2)时间,但是在一般情况下,它要比冒泡排序快一倍,比选择排序还要快一点。它经常被用在比较复杂的排序算法的最好阶段,例如快速排序。
在插入排序中,一组数据仅仅是局部有序的。另一部分是无序的,等待插入局部有序中。
排序规则:
将无序的队列的第一个数据项标记为A,取出(在程序中,这个数据项被存储在一个临时变量中),将A与局部有序的最右端那个最大数据项进行比较,从右向左,依次比较,直到找到比自己小的数据项,然后插入在它右边,这样插入了一个数据项之后仍是有序的,按照此规则依次将无序的数据项插入。
public void insertionSort(){
int in,out;
for(out=1;out<nElems;out++){
long temp = a[out]; // remove marked item
in = out; // start shifts at out
while(in>0 && a[in-1] >= temp){ // until one is smaller
a[in] = a[in-1]; // shift item right
--in; // go left one position
}
a[in] = temp; // insert marked item
}
}
在外层循for环中,out变量从1开始,向右移动。它标记了未排序部分的最左端的数据项。而在内层的while循环中,in变量从out变量开始,向左移动,直到temp变量小于in所指的数据数据项,或者它已经不能再往左移动为止。While循环的每一趟都向右移动了一个已排序的数据项。
插入排序的不变性
在每趟结束时,在将temp位置的项插入后,比out变量下标号小的数据项都是局部有序的
插入排序的效率
在第一趟排序中,最多比较一次,第二趟最多比较两次,以此类推,最后一次最多,比较N-1次,所以有:
1+2+3+……+N-1=N*(N-1)/2
然而,在每一次排序法先插入点之前,平均只有全体数据项的一半真的进行了比较,我们除以2得到:N*(N-1)/4
复制的次数大致等于比较的次数。而然,一次复制与一次交换的时间耗费不同,所以相对于随机数据,这个算法比冒泡排序快一倍,比选择排序略快。
对于已经有序或者基本有序的数据来说,插入排序要好得多。当数据有序的时候,while循环条件总是假,所以它变成了外层循环中的一个简单语句,执行N-1次。在这种情况下,算法运行需要O(N)的时间。如果数据基本有序,插入排序几乎只需要O(N)的时间,这对把一个基本有序文件进行排序是一个简单而有效的方法。
然而,对于逆序排列的数据,每次比较和移动都会执行,所以插入排序不比冒泡排序快。
四、几种简单排序之间的比较
三种算法的时间复杂度都是O(n^2)
冒泡排序法一般不太使用,效率最差,但写法简单,当数据很小的时候会有些应用价值。
选择排序虽然把交换次数降到了最低,但比较次数仍然很大,当数据量很小,并且交换次数相对于比较数据更加好事的情况下,可以应用选择排序。
大多数数情况下,对于数据量比较小或者基本有序时,插入排序算法是三种简单排序算法中最好的选择。对于更大数据量的排序来说,快速排序通常是最快的方法。
五、通过插入排序方式对对象数组进行排序
在插入排序中通过 name.compareTo()的方式进行比较
class Solider{ private String lastName; private String firstName; private int age; Solider(String last,String first,int a){ lastName = last; firstName = first; age = a; } public void displayStudent(){ System.out.println("lastName: "+lastName+"\t firstName:"+firstName+"\t age:"+age); } public String getLastName(){ return lastName; } } //插入排序的容器类,内部封装了插入,打印,的方法 class ArrayInsertObj{ private Solider[] soilders; private int nElems; ArrayInsertObj(int max){ soilders = new Solider[max]; nElems = 0; } public void insert(String last,String first,int age){ soilders[nElems] = new Solider(last,first,age); nElems++; } //对象数组的打印 public void display(){ for (int i = 0; i < nElems; i++) { soilders[i].displayStudent(); } System.out.println(); } //插入排序 public void insertionSort(){ int in,out; for(out=1;out<nElems;out++){ in = out; Solider temp = soilders[out]; while(in>0&& soilders[in-1].getLastName().compareTo(temp.getLastName())>0){ soilders[in] = soilders[in-1]; in--; } soilders[in] = temp; } } } //主程序 public class ObjSortApp { public static void main(String[] args){ int maxSize = 100; ArrayInsertObj arr = new ArrayInsertObj(maxSize); arr.insert("Micheal","Nash",15); arr.insert("Yee","Tom",16); arr.insert("Allen","Iverson",25); arr.insert("Wodwo","Henry",12); arr.insert("Evans","Jose",19); arr.insert("Lemb","Minh",11); arr.insert("Cris","Paul",17); System.out.println("Before sorting:"); arr.display(); arr.insertionSort(); System.out.println("After sorting:"); arr.display(); } } /* Before sorting: lastName: Micheal firstName:Nash age:15 lastName: Yee firstName:Tom age:16 lastName: Allen firstName:Iverson age:25 lastName: Wodwo firstName:Henry age:12 lastName: Evans firstName:Jose age:19 lastName: Lemb firstName:Minh age:11 lastName: Cris firstName:Paul age:17 After sorting: lastName: Allen firstName:Iverson age:25 lastName: Cris firstName:Paul age:17 lastName: Evans firstName:Jose age:19 lastName: Lemb firstName:Minh age:11 lastName: Micheal firstName:Nash age:15 lastName: Wodwo firstName:Henry age:12 lastName: Yee firstName:Tom age:16 */