一、介紹
排序是我們工作中經常碰到的一件事,基本每個項目都涉及到排序運算。一般,排序操作在數據處理過程中要話費許多時間。爲了提高計算機的運行效率,人們提出不斷改進各種各樣的排序算法,而這些算法也從不同角度展示了算法設計的某些重要原則和技巧。
排序就是將一組對象按照規定的次序重新排列的過程,排序往往是爲檢索服務的。例如,學生檔案系統裏面的學生成績信息就是按照學號、年齡或入學成績等排序後的結果,在排好序的結果裏面檢索學生成績信息效率就高很多了。如下表1-1就是按照年齡升序排列的學生信息列表。
表1-1 按學號升序排序學生成績表
學號 |
姓名 |
性別 |
年齡 |
成績 |
20060206 |
吳三 |
男 |
18 |
523 |
20060207 |
李四 |
男 |
19 |
450 |
20060208 |
王五 |
女 |
18 |
470 |
20060209 |
趙柳 |
女 |
17 |
485.5 |
表1-2 按學號升序排序學生成績表
學號 |
姓名 |
性別 |
年齡 |
成績 |
20060209 |
趙柳 |
女 |
17 |
485.5 |
20060208 |
王五 |
女 |
18 |
470 |
20060206 |
吳三 |
男 |
18 |
523 |
20060207 |
李四 |
男 |
19 |
450 |
可以看出學號爲20060206和20060208的兩位同學年齡都爲18歲,表1-2按照年齡升序後相對位置發生變化了。對於這種相同鍵值的兩個記錄在排序前後相對位置變化情況是排序算法研究中經常關注的一個問題,該問題成爲排序算法的穩定性。需要注意的是:穩定性是算法本身的特性,與數據無關。
排序算法分爲內部排序和外部排序。內部排序是將待排序的記錄全部放在計算機內存中進行的排序過程,其排序實現方法很多,主要有插入排序、選擇排序、交換排序和歸併排序等;如果待排序的記錄數量很多,內存不能存儲全部記錄,需要對外存進行訪問排序的過程,本次總結的是內部排序。
評價一個算法的優劣,通常是用時間複雜度和空間複雜度這兩個指標。由於排序算法的多樣性,很難確定一種公認的最好方法,應當根據實際應用選擇不同的方法。例如,當待排序序列已基本有序時,插入排序和交換排序比較有效;當待排記錄數量較多時,選擇排序有效。
二、比較
對20000條記錄進行排序:
名稱 耗時(毫秒) 插入排序(直接插入) 1469 交換排序(冒泡排序) 5828 交換排序(快速排序) 1078 選擇排序(直接選擇排序) 2796 選擇排序(堆排序) 16 歸併排序(二路歸併) 0 |
可以看出,在記錄較多的情況下歸併排序是效率最高的,其次是堆排序,再次到快速排序,而算法相對簡單的排序方法反而效率很低,尤其是冒泡排序效率最低。
三、內部排序
(一)插入排序
它的基本思想是將記錄分爲有序區和無序區,將無序區中的記錄依次插入到有序區中,並保持有序。常用的插入排序方法有直接插入排序、拆半插入排序、表插入排序和希爾排序等。如果待排記錄較少且基本有序的話,可以考慮使用插入排序算法。
1.直接插入排序
根據插入排序算法的基本思想,圖3-1便是直接插入排序算法的過程,中括號內是有序區,中括號外面是無序區。從無序區的前面或後面依次取出一個元素,在有序區找到一個合適的位置插入。這裏所說的合適位置要看是升序還是降序。
圖3-1 直接插入排序示意圖
/** * 插入排序——直接插入排序 * @param arrays 待排序序列 * @dateTime 2014-2-16 上午09:15:08 * @author wst * @return void */ public static void insertSort(int[] arrays){ int i,j,n=arrays.length-1; for(i=1;i<=n;i++){//第一個元素無須排序 int temp=arrays[i];//當前比較元素 //在已排好序的序列裏找到一個合適的位置存放temp for(j=i;j>0&&temp<arrays[j-1];j--){ arrays[j]=arrays[j-1];//將大的元素後移 } arrays[j]=temp; } }
(二)交換排序
交換排序的基本思想是兩兩比較待排序的記錄,當記錄之間值出現逆序時,則交換兩個記錄。常用的交換排序有冒泡排序和快速排序等。
1.冒泡排序
冒泡排序的過程是首先將第一個記錄和第二個記錄進行比較,若爲逆序,則將這兩個記錄交換,然後繼續比較第二個和第三個記錄。依次類推,直到完成第n-1個記錄和第n個記錄比較交換爲止。上述過程稱爲第一趟起泡,其結果使最大的記錄已到了第n個位置上。重複以上起泡過程,當在一趟起泡過程中沒有進行記錄交換的操作時,整個排序過程終止。因爲每趟起泡都有一個最大的記錄沉到水底,所以整個排序過程最多需要進行n-1趟起泡。例如,圖3-2是冒泡排序過程的示意圖,圖中只給出了第一趟起泡的過程,後面直接給出各趟起泡結果。在起泡結果裏面,有黃色背景的記錄已經排好序。
圖3-2 冒泡排序示意圖
從圖3-2中可以看出,當第4趟起泡結束後已經沒有需要交換的氣泡了,第5、6趟起泡純屬多餘,因爲此時序列已經有序,因此在第4趟起泡結束後可以終止循環。起泡次數=4<n-1,符合上面的推理。
/** * 交換排序——冒泡排序 * @param arrays * @dateTime 2014-1-25 上午10:41:58 * @author wst * @return void */ public static void upBubbleSort(int[] arrays){ int i,j,n=arrays.length; for(i=0;i<n-1;i++){ boolean endsort=true;//是否需要交換 for(j=0;j<n-i-1;j++){ if(arrays[j]>arrays[j+1]){ int temp=arrays[j]; arrays[j]=arrays[j+1]; arrays[j+1]=temp; endsort = false; } } if(endsort){ break; } } }
2.快速排序
快速排序實質上是對冒泡排序的一種改進。其基本思想是:以選定的記錄爲基準,將待排序表劃分爲左、右兩段,其中左邊所有記錄小於等於右邊所有記錄,然後,對左、右兩段記錄分別進行快速排序。如下圖3-3是快速排序的示意圖,下圖給出了第一趟快速排序的完整過程,後面相繼只給出各趟排序結果。紅色記錄表示各趟記錄劃分出的關鍵字,中括號內的記錄表示根據關鍵字劃分的無序區。
圖3-3 快速排序示意圖
/** * 交換排序——快速排序 * 不穩定。O(nlog2n)~O(n2)。 * 待排序列基本有序時效率低 * @param arrays * @param low * @param high * @dateTime 2014-2-16 上午09:34:24 * @author wst * @return int[] */ public static int[] quickSort(int[] arrays,int low,int high){ if(low<high){ //劃分區域,找出關鍵字 int temp=quickPartition(arrays,low,arrays.length-1); //在關鍵字左側進行排序 quickSort(arrays,low,temp-1); //在關鍵字右側進行排序 quickSort(arrays,temp+1,high); } return arrays; } /** * 對子序列進行一趟快速排序 * @param arrays 待排序序列 * @param low 當前排序序列的首指針 * @param high 當前排序序列的末指針 * @dateTime 2014-2-16 上午09:38:01 * @author wst * @return int */ private static int quickPartition(int[] arrays,int low,int high){ int x=arrays[low];//初始值 while(low<high){ while(low<high&&(arrays[high]>=x)){ high--;//從末端找出一個比x小的數 } //將找到的比x小的元素移到low位置 arrays[low]=arrays[high]; while(low<high&&(arrays[low]<=x)){ low++;//從首端找出一個比x大的數 } //將找到的比x大的元素移到high位置 arrays[high]=arrays[low]; } //一趟快速排序結束後,將x置入low位置 arrays[low]=x; return low; }
(三)選擇排序
選擇排序的基本思想:每次從待排序列中選取記錄最小或最大的放到適當位置。常用的選擇排序法有直接選擇排序和堆排序法等。選擇排序適合待排記錄較大的情況。
1.直接選擇排序
直接選擇排序算法的基本思想:在待排序的無序區中選擇最小(大)的記錄,並將該記錄放到有序區的最前(後)端。圖3-4是直接選擇排序過程的示意圖。中括號內是有序區,每次從中括號外面選擇出一個最小或最大的記錄放到有序區。該算法簡單容易實現。
圖3-4 直接選擇排序示意圖
/** * 選擇排序——直接選擇 * 不穩定。O(n2)。待排記錄較多效率非常低下 * @param arrays * @dateTime 2014-2-16 上午09:57:13 * @author wst * @return void */ public static void selectSort(int[] arrays){ int n=arrays.length,minIndex=0,temp=0; if(arrays==null||n==0){ return; } for(int i=0;i<n;i++){ //每一趟都選擇出一個最小值 minIndex=i; //待排區的最小元素下標 for(int j=i+1;j<n;j++){ //在待排區中找出最小元素 if(arrays[j]<arrays[minIndex]){ minIndex=j; } } if(minIndex!=i){//將找到的最小元素置入有序區 temp=arrays[i]; arrays[i]=arrays[minIndex]; arrays[minIndex]=temp; } } }
2.堆排序
堆排序是利用堆來選擇最小(大)記錄。對直接選擇排序的分析我們可以知道,在n個記錄中選出最小值,至少要進行n-1次比較。然而繼續在剩餘的n-1個記錄中選出次小記錄是否一定要進行n-2次比較呢?若能利用前n-1次比較所得信息,是否可以減少以後各次選擇中的比較次數呢?答案是肯定的。堆有最小堆和最大堆,簡單定義如下:
序列{k1,k2,…,kn}滿足
其中,i=1,2,…,n/2,則稱這個n個記錄的序列{k1,k2,…,kn}爲最小堆(或最大堆)。根據上述定義可以知道,最小堆可以看成是一棵以k1爲根的完全二叉樹,在這課二叉樹中,任一結點的值都不大於它的兩個孩子的值(若孩子存在的話);最大堆可以看成是一棵以k1爲根的完全二叉樹,在這棵二叉樹中,任一結點的值都不小於它的兩個孩子的值(若存在孩子的話)。
由此可知,依次輸出堆頂元素就可以得到升序或降序的序列。因此,實現堆排序需要解決兩個問題:
(1)如何由一個初始序列建成一個堆?
(2)如何在輸出堆頂元素之後調整剩餘元素成爲一個新堆?
如圖3-5可以回答第一個問題,圖3-6則回答了第二個問題。以下是由初始序列{65,88,55,34,93,28,18,40}建立堆和調整堆的全排序過程。
圖3-5將初始序列建立一個堆
圖3-6調整剩餘元素成爲一個新堆
/** * 堆排序 * @param a * @dateTime 2014-3-5 下午07:12:31 * @author wst * @return void */ public static void heapSort(int[] a){ int n=a.length; int temp=0; //由初始序列建立一個堆 for(int i=n/2;i>0;i--){ sift(a,i-1,n); } //輸出堆頂元素,調整剩餘元素成爲一個新堆 for(int i=n-2;i>=0;i--){ temp=a[i+1]; a[i+1]=a[0]; a[0]=temp; sift(a,0,i+1); } } /** * 執行一次篩選 * @param a 待排序列 * @param k 根元素下標 * @param n 待排序序列長度 * @dateTime 2014-3-5 下午07:07:34 * @author wst * @return void */ public static void sift(int[] a,int k,int n){ int temp=a[k]; //根 int j=2*k+1; //左孩子 while(j<=n-1){ if(j<n-1&&a[j]>=a[j+1]){ j++; } if(temp<a[j]){ break; //篩選結束 } a[(j-1)/2]=a[j];//根與左孩子交換 j=2*j+1; //繼續從左孩子篩選 } a[(j-1)/2]=temp; }
(四)歸併排序
歸併排序是將兩個或兩個以上的有序表合併成一個有序表。歸併排序法有二路歸併排序等。
1.二路歸併
二路歸併的基本思想:假設序列中有n個記錄,可看成是n個有序的子序列,每個序列的長度爲1。首先將每相鄰的兩個記錄合併,得到[n/2]個較大的有序子序列,每個子序列包含2個記錄,再將上述序列兩兩合併,得到[[n/2]/2]個有序子序列,如此反覆,直至得到一個長度爲n的有序序列爲止,排序結束。如圖3-7是二路歸併排序過程的示意圖。
圖3-7二路歸併排序示意圖
/** * 歸併排序——二路歸併 * @param a * @param n * @dateTime 2014-3-4 下午09:15:21 * @author wst * @return void */ static void margeSort(int[] a,int n){ int[] b=new int[n+1]; int h=1; while(h<=n){ mergePass(a,b,h,n); h=2*h; mergePass(b,a,h,n); h=2*h; } } /** * 執行一次歸併 * 在含有n個記錄的序列a中, * 將長度各爲h的相鄰兩個有序子序列合併爲長度2h的一個有序序列 * @param a 待排序列 * @param b 合併後的序列 * @param h 子序列長度 * @param n 總長度 * @dateTime 2014-3-4 下午09:02:39 * @author wst * @return void */ static void mergePass(int[] a,int[] b,int h,int n){ int i=0; while(i<n-2*h+1){ merge(a,b,i,i+h-1,i+2*h-1); i+=2*h; } if(i+h-1<n){ merge(a,b,i,i+h-1,n); }else{ for(int t=i;t<=n;t++){ b[t]=a[t]; } } } /** * 有序序列的合併 * 將a[h],...,a[m]和a[m+1],...,a[n]兩個有序序列合併爲一個有序序列 * @param a 待排序序列 * @param b 合併後的序列 * @param h 子序列長度 * @param m 序列1的結束位置 * @param n 序列2的結束位置 * @dateTime 2014-3-4 下午09:07:38 * @author wst * @return void */ static void merge(int[] a,int[] b,int h,int m,int n){ int k=h,j=m+1;//序列1的起始位置和序列2的起始位置 while((h<=m)&&(j<=n)){ if(a[h]<a[j]){ b[k]=a[h]; h++; }else{ b[k]=a[j]; j++; } k++; } while(h<=m){//將a[h],...,a[m]剩餘序列插入末尾 b[k]=a[h]; h++; k++; } while(j<=n){//將a[h],...,a[m]剩餘序列插入末尾 b[k]=a[j]; j++; k++; } }
輔助類:
package com.wusongti.util; import java.util.Random; /** * * @Utils.java * @description 工具類 * @date 2014-1-21 * @time 下午07:14:26 * @author wst * */ public class Utils { /** * 產生n個不重複的隨機數 * @param n 產生n個隨機數 * @param start 起始值 * @dateTime 2014-1-21 下午07:13:41 * @author wst * @return int[] 返回不重複的n個隨機數 */ public static int[] getRandomNumber(int n,int start){ int[] arrays=new int[n]; Random rand=new Random(); boolean flag=false; for(int i=0;i<n;i++){ int num; do{ flag=false; num=rand.nextInt(n)+start; for(int j=0;j<i;j++){ if(num==arrays[j]){ flag=true; break; } } }while(flag); arrays[i]=num; } return arrays; } /** * 打印數組 * @param arrays * @dateTime 2014-1-21 下午07:23:25 * @author wst * @return void */ public static void printSort(int[] arrays){ int n=arrays.length; for(int i=0;i<n;i++){ if(i%15==0){ System.out.println(); } System.out.print(arrays[i]+" "); } } }
四、心得
有總結纔會有反思,有反思纔會有提高!
個人郵箱:[email protected]
五、參考文獻
鄭誠.數據結構導論[M].外語教學與研究出版社,2012