排序演算法總結(基於Java實現)

林加欣發表於2017-11-29

前言

    下面會講到一些簡單的排序演算法(均基於java實現),並給出實現和效率分析。

    使用的基類如下:

    注意:抽象函式應為public的,我就不改程式碼了

  1. public abstract class Sortable {
  2. protected String LABLE="排序演算法";
  3. //比較兩個數(使用了Integer中sort的原始碼)
  4. protected int compare(int x, int y) {
  5. return (x < y) ? -1 : ((x == y) ? 0 : 1);
  6. }
  7. //同上,不過返回改為bool
  8. protected boolean less(int x,int y){
  9. return compare(x,y) <0;
  10. }
  11. //交換陣列中的兩個值
  12. protected void exch(int[] a,int i,int j){
  13. Integer temp = a[i];
  14. a[i] = a[j];
  15. a[j] = temp;
  16. }
  17. //子類需要實現的排序演算法
  18. public abstract void sort(int[] a);
  19. public String getLABLE() {
  20. return LABLE;
  21. }
  22. }

氣泡排序

    最常見的,畢竟老師教給我們的的第一種排序演算法。實現起來很簡單,不過實際應用很少(正常情況下),複雜度O(n²)。

    原理

    趟一趟的比,每一趟中,迴圈剩餘的數,和後一個進行比較,若比它小則交換。這樣一趟下來最小的在第一個,最大的在最後一個。總共比n-1趟。

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 最簡單的氣泡排序
  4. *
  5. * @author anxpp.com
  6. * 原理:比較相鄰兩個元素,從第一對開始比較一直到最後一對,若順序不對就交換(感覺就像冒泡一樣)。
  7. * 一趟比較後,最大(或最小)的會位於最後的位置,然後再以類似方式比較前面的元素。
  8. */
  9. public class BubbleSort extends Sortable {
  10. public BubbleSort(){
  11. super.LABLE = "氣泡排序";
  12. }
  13. @Override
  14. public void sort(int[] a) {
  15. for(int i=0;i<a.length-1;i++){
  16. for(int j=0;j<a.length-1-i;j++){
  17. if(less(a[j+1],a[j])){
  18. exch(a,j,j+1);
  19. }
  20. }
  21. }
  22. }
  23. }

    優化

    上面的演算法,無論的你的資料怎麼樣,始終都要比n²次,效率很低。若你的資料區域性有序,經過幾趟交換以後,已經有序,則不用繼續往下比。效率會高很多(絕大多數情況下)。優化程式碼如下:

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 設定標誌優化後的氣泡排序
  4. * @author anxpp.com
  5. *
  6. * 原理:比較相鄰兩個元素,從第一對開始比較一直到最後一對,若順序不對就交換(感覺就像冒泡一樣)。
  7. * 一趟比較後,最大(或最小)的會位於最後的位置,然後再以類似方式比較前面的元素。
  8. * 優化:傳統的氣泡排序,總是要比較那麼多次,如果在某趟完成後,並無交換表示資料已經有序,所以設定
  9. * 一個標誌,如某趟比較完成後沒有發生,則不再繼續後面的運算直接返回即可,其實,有時候效率反而會比傳統的低!
  10. * 其他:據說分而治之也能有用到冒泡,這裡就不深究了...
  11. */
  12. public class BetterBubbleSort extends Sortable {
  13. public BetterBubbleSort(){
  14. super.LABLE = "氣泡排序優化";
  15. }
  16. @Override
  17. public void sort(int[] a) {
  18. boolean didSwap;
  19. for(int i=0;i<a.length-1;i++){
  20. didSwap = false;
  21. for(int j=0;j<a.length-1-i;j++){
  22. if(less(a[j+1],a[j])){
  23. exch(a,j,j+1);
  24. didSwap = true;
  25. }
  26. }
  27. if(!didSwap) return ;
  28. }
  29. }
  30. }

選擇排序

    和冒泡複雜度一樣O(n²),但是時間上可能會比冒泡稍微快一點,因為交換的次數比冒泡少。

    原理

    選擇排序可以說是最好理解的演算法。就是每次遍歷一趟,找出最小的數,放到最前端。(這裡說的是最前,是指無序的佇列中的最前)

1

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 選擇排序
  4. * @author anxpp.com
  5. *
  6. */
  7. public class SelectionSort extends Sortable {
  8. public SelectionSort(){
  9. super.LABLE = "選擇排序";
  10. }
  11. @Override
  12. public void sort(int[] a) {
  13. for(int i=0;i<a.length;i++){
  14. int min=i;
  15. for(int j=i+1;j<a.length;j++){
  16. if(less(a[j],a[min])){
  17. min = j;
  18. }
  19. }
  20. exch(a,i,min);
  21. }
  22. }
  23. }

插入排序

    時間複雜度O(n²)。

    原理

    遍歷未排序序列。把未排序數列的第一個數和已排序數列的每一個數比較,若比它大則交換。經典的理解方式就是理解成摸牌時候理牌的順序。我上面的實現是直接互動數字,若是把大的數直接往後移效率還會更高。

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 插入排序
  4. * @author anxpp
  5. *
  6. */
  7. public class InsertionSort extends Sortable {
  8. public InsertionSort(){
  9. super.LABLE = "插入排序";
  10. }
  11. @Override
  12. public void sort(int[] a) {
  13. for(int i=1;i<a.length;i++){
  14. for(int j=i;j>0;j--){
  15. if(less(a[j],a[j-1])){
  16. exch(a,j,j-1);
  17. }
  18. else break;
  19. }
  20. }
  21. }
  22. }

    適合插入排序的資料

    當你的資料是基本有序的時候且資料量小,利用插入排序的時候,效率會很高。若資料為逆序的話,效率很低。

希爾排序

    可以看出是插入排序的一種優化,或者是預處理。希爾排序就是先進行h-sort,也就是讓間隔為h的元素都是有序的。普通的插入排序就是1-sort。

    原理

    主要就是選定一個h的有序陣列來進行預排序。這樣最後進行插入排序的時候,能使資料區域性有序。就算交換的話,交換的次數也不會很多。這樣h序列稱為遞增序列。希爾的效能很大部分取決於遞增序列.一般來說我們使用這個序列3x + 1.

2

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 希爾排序
  4. * @author anxpp.com
  5. *
  6. */
  7. public class ShellSort extends Sortable {
  8. public ShellSort(){
  9. super.LABLE = "希爾排序";
  10. }
  11. @Override
  12. public void sort(int[] a) {
  13. int h=1;
  14. while(h<a.length/3){
  15. h=3*h+1;
  16. }
  17. while(h>=1){
  18. for(int i=h;i<a.length;i++){
  19. for(int j=i;j>=h;j=j-h){
  20. if(less(a[j],a[j-h])){
  21. exch(a,j,j-h);
  22. }
  23. else break;
  24. }
  25. }
  26. h=h/3;
  27. }
  28. }
  29. }

    效能

    對於希爾排序的效能其實無法準確表示。介於O(nlogn)和O(n²)之間,大概在n的1.5次冪左右。

    希爾排序對於中大型資料的排序效率是很高的,而且佔用空間少,程式碼量短。而且就算是很大的資料,用類似快排這種高效能的排序方法,也僅僅只比希爾快兩倍或者不到。

歸併排序

    複雜度O(nlogn).

    核心思想就是採用分而治之的方法,遞迴的合併兩個有序的陣列。效率比較高,缺點是空間複雜度高,會用到額外的陣列。

    原理

    核心程式碼是合併的函式。合併的前提是保證左右兩邊的陣列分別有序,在合併之前和之後在Java中我們可以用斷言來保證陣列有序。合併的原理其實也很簡單,先把a陣列中的內容複製到額外儲存的temp陣列中去。分別用兩個index指向a陣列的起始位置和中間位置,保證a陣列左右兩邊有序,比如i,j。現在開始從頭掃描比較左右兩個陣列,若a[i]<=a[j],則把a[i]放到temp陣列中去,且i向前走一步。反正則放a[j],且j走一步。若其中一個陣列走完了,則把另一個陣列剩餘的數直接放到temp陣列中。我們用遞迴的方式來實現左右兩邊有序。遞迴到陣列只有1個數時肯定是有序的,再合併2個數,再退出來合併4個數,以此類推。

3

4

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 歸併排序
  4. * @author anxpp.com
  5. *
  6. */
  7. public class MergeSort extends Sortable {
  8. public MergeSort(){
  9. super.LABLE = "歸併排序";
  10. }
  11. int[] temp ;
  12. private void merge(int[] a, int l, int m, int h){
  13. for(int i=l;i<=h;i++){
  14. temp[i]=a[i];
  15. }
  16. int i=l;
  17. int j=m+1;
  18. for(int k=l;k<=h;k++){
  19. if(i>m) a[k]=temp[j++];
  20. else if(j>h) a[k]=temp[i++];
  21. else if(less(temp[i],temp[j])) a[k]=temp[i++];
  22. else a[k] = temp[j++];
  23. }
  24. }
  25. private void sort(int[] a,int l,int h) {
  26. if(l<h){
  27. int mid = (l+h)/2;
  28. sort(a,l,mid);
  29. sort(a,mid+1,h);
  30. if (!less(a[mid+1], a[mid])) return;
  31. merge(a,l,mid,h);
  32. }
  33. }
  34. @Override
  35. public void sort(int[] a) {
  36. temp = new int[a.length];
  37. sort(a,0,a.length-1);
  38. }
  39. }

    優化

    歸併排序對小陣列排序時,由於會有多重的遞迴呼叫,所以速度沒有插入排序快。可以在遞迴呼叫到小陣列時改採用插入排序。小陣列的意思是差不多10個數左右。

    如果遞迴時判斷已經有序則不用繼續遞迴。也可以增加效率。

  1. private void sort(int[] a,int l,int h) {
  2. if(l<h){
  3. int mid = (l+h)/2;
  4. sort(a,l,mid);
  5. sort(a,mid+1,h);
  6. if (!less(a[mid+1], a[mid])) return;
  7. merge(a,l,mid,h);
  8. }
  9. }

    另外在合併時互動兩個陣列的順序,能節省複製陣列到輔助陣列的時間,但節省不了空間。

5

    適用範圍

    如果你對空間要求不高,且想要一個穩定的演算法。那麼可以使用歸併排序。

快速排序

    傳說中最快的排序演算法,聽說能裸寫快排,月薪可上10k...

    快排平均情況下時間複雜度O(nlogn),最糟糕情況O(n²)。O(n²)主要是因為選定的主元是極端值造成的,比如說最大值,最小值。不過這種情況一般很少出現,所以在進行快排之前我們需要對陣列進行亂序,儘量避免這種情況的發生。

    原理

    第一步打亂陣列。

    然後也是分治法。歸併是先分再合併。快排是先排序再分別排序兩邊。

    排序過程核心思想是為了選出一個數,把陣列分成左右兩邊,左邊比主元小,右邊比主元大。

    選定第一個數作為主元。然後設定兩個index指向陣列首尾,比如i,j。接著從兩邊向中間掃描,分別用a[i]和a[j]和主元比較。若兩邊位置不對則交換a[i]和a[j],比如說a[i]在掃描過程中遇到a[i]>主元,那麼則停止掃描,因為我們需要左邊的數小於主元,反正右邊也一樣等到a[j]也停下來,則交換a[i]和a[j]。

    得到中間的位置之後再分別左右遞迴排序。

6

7

    實現

  1. import com.anxpp.sort.base.Sortable;
  2. /**
  3. * 快速排序
  4. * @author u
  5. *
  6. * 原理:選擇一個基準元素,通過一趟掃描,將資料分成大於和不大於基準元素的兩部分(分別在基準元素的兩邊),此時
  7. * 基準元素就在未來排好後的正確位置,然後遞迴使用類似的方法處理這個基準元素兩邊的部分。
  8. * 既然用了遞迴,難免在空間上的效率不高...
  9. * 平均效能通常被認為是最好的
  10. */
  11. public class quickSort extends Sortable {
  12. public quickSort(){
  13. super.LABLE = "快速排序";
  14. }
  15. /**
  16. *
  17. * @param a 要排序的列表
  18. * @param low 左邊位置
  19. * @param high 右邊位置
  20. */
  21. private void sort(int[] a,int low,int high){
  22. //左
  23. int l =low;
  24. //右
  25. int h = high;
  26. //基準值
  27. int k = a[low];
  28. //判斷一趟是否完成
  29. while(l<h){
  30. //若順序正確就比較下一個
  31. while(l<h&&a[h]>=k)
  32. h--;
  33. if(l<h){
  34. int temp = a[h];
  35. a[h] = a[l];
  36. a[l] = temp;
  37. l++;
  38. }
  39. while(l<h&&a[l]<=k)
  40. l++;
  41. if(l<h){
  42. int temp = a[h];
  43. a[h] = a[l];
  44. a[l] = temp;
  45. h--;
  46. }
  47. }
  48. if(l>low) sort(a,low,l-1);
  49. if(h<high) sort(a,l+1,high);
  50. }
  51. @Override
  52. public void sort(int[] a) {
  53. sort(a,0,a.length-1);
  54. }
  55. }

    優化

    第一步的隨機打亂陣列,雖然會耗費一定時間,但卻是必要的。同樣的小陣列的排序,快排不如插入排序。所以小陣列可以直接採用插入排序。主元的選擇方式可以有多種,比如隨機選擇主元。或者選取三個數,取中位數為主元,但是會耗費一定時間。

    適用範圍

    雖然快速排序是不穩定的。但快速排序通常明顯比其他Ο(nlogn)演算法更快,因為它的內部迴圈很小。快速排序在對重複資料的排序時,會重複劃分資料進行排序。雖然效能也還行,但這裡可以進行改進,就是下面介紹的三向切分排序。

三向切分

    快速排序的一種改進,使快排在有大量重複元素的資料,同樣能保持高效。

    原理

    基本原理和快排差不多。三向切分的時候在劃分陣列時不是分為兩組,而是分成三組。

  •     小於主元
  •     和主元相等
  •     大於主元

8

    實現

  1. public class ThreeWaySort extends Sortable {
  2. public void sort(int[] a,int l ,int h) {
  3. if(l>=h) return;
  4. int v = a[l];
  5. int i=l;
  6. int lv=l;
  7. int gh=h;
  8. while(i<=gh){
  9. int cmpIndex = compare(a[i],v);
  10. if(cmpIndex<0) exch(a,i++,lv++);
  11. else if(cmpIndex>0) exch(a,i,gh--);
  12. else i++;
  13. }
  14. sort(a,l,lv-1);
  15. sort(a,gh+1,h);
  16. }
  17. @Override
  18. void sort(int[] a) {
  19. sort(a,0,a.length-1);
  20. }
  21. }

堆排序

    時間複雜度O(nlogn),堆排序主要用二叉堆實現,在講堆排序之前我們可以要先了解下二叉堆。

    二叉堆

    所謂的二叉堆用一顆二叉樹表示,也就是每一個節點都大於它的左右子節點。也就是說根節點是最大的。

    二叉樹用陣列儲存,可以用下標來表示節點。比如i這個節點的父節點為i/2,左兒子為2*i,右兒子為2*i+1.

    堆的操作主要有兩種上浮和下沉。主要對應兩種情況,比如在陣列末尾新增節點,此時需要上浮節點,保證二叉堆的特點。反之在替換根節點是則需要下沉操作。

    原理

    分為兩步。

  •     把陣列排成二叉堆的順序
  •     調換根節點和最後一個節點的位置,然後對根節點進行下沉操作。

9

    實現

    適用範圍

    堆排序也是不穩定的。

    堆排序在空間和時間上都是O(nlogn),且沒有最糟情況,但在平均情況下比快排慢。

    所以現在大部分應用都是用的快排,因為它的平均效率很高,幾乎不會有最糟情況發生。

    但如果你的應用非常非常重視效能的保證,比如一些醫學上的監控之類的。

    那麼可以使用堆排序。還有一個堆排序的缺點,是它無法利用快取,幾乎很少和相鄰元素的比較。

執行時間比較

    使用下面的程式碼測試以上排序演算法:

  1. import java.util.Random;
  2. import com.anxpp.sort.base.Sortable;
  3. /**
  4. * 測試排序演算法
  5. * @author anxpp.com
  6. *
  7. */
  8. public class TestSort {
  9. //需要排序的數字長度為LEN
  10. private final static int LEN = 30000;
  11. //最大值為MAX
  12. private final static int MAX = 99999;
  13. public static void main(String[] args){
  14. //初始化排序演算法
  15. Sortable[] sortables = {
  16. new BubbleSort(),new BetterBubbleSort(),new SelectionSort(),
  17. new InsertionSort(),new ShellSort(),new MergeSort(),
  18. new BetterMergeSort(),new quickSort(),new ThreeWayQuickSort()};
  19. //產生源資料
  20. Random random = new Random();
  21. random.setSeed(System.currentTimeMillis());
  22. int[][] a = new int[sortables.length][LEN];
  23. int i = 0;
  24. while(i++ < LEN-1){
  25. int num = random.nextInt(MAX);
  26. int j = 0;
  27. while(j<sortables.length)
  28. a[j++][i] = num;
  29. }
  30. //排序
  31. for(i = 0;i<sortables.length;i++){
  32. System.out.println(sortables[i].getLABLE()+":");
  33. // print(a[i]);
  34. // sortTime(a[i],sortables[i]);
  35. System.out.println(sortTime(a[i],sortables[i]));
  36. // print(a[i]);
  37. }
  38. }
  39. public static int sortTime(int[] a,Sortable sortable){
  40. long start = System.currentTimeMillis();
  41. sortable.sort(a);
  42. return (int) (System.currentTimeMillis()-start);
  43. }
  44. public static void print(int[] a){
  45. for(int i = 0;i<a.length;i++)
  46. System.out.print(a[i] + ",");
  47. System.out.println();
  48. }
  49. }

    下面是本人的一次執行結果:

  1. 氣泡排序:
  2. 2348
  3. 氣泡排序優化:
  4. 2660
  5. 選擇排序:
  6. 250
  7. 插入排序:
  8. 907
  9. 希爾排序:
  10. 12
  11. 歸併排序:
  12. 7
  13. 歸併排序優化:
  14. 5
  15. 快速排序:
  16. 6
  17. 三向切分快速排序:
  18. 15

10

    推薦一個很好的網站,對各種演算法進行了總結,和動畫描述:sorting-algorithms

相關文章