1. 程式人生 > >經典內部排序演算法學習總結(演算法思想、視覺化、Java程式碼實現、改進、複雜度分析、穩定性分析)

經典內部排序演算法學習總結(演算法思想、視覺化、Java程式碼實現、改進、複雜度分析、穩定性分析)

一、什麼是排序演算法?

排序,顧名思義,就是按照一定的規則排列事物,使之彼此間有序
而排序演算法所要做的工作,就是將資料按照人為制定的比較規則排列好,使資料處於彼此間有序的狀態。

二、為什麼要進行排序?

那為什麼要將資料排序呢?計算機處理速度這麼快,會不會有點多此一舉。現在考慮手上有一本目錄亂序的詞典,假設有1w個單詞,如果想要查apple這個單詞,每次都要從頭開始找,一個個的確定是不是apple,忽略心力交瘁和砸字典的衝動,那麼假設每次查詢都需要12個小時。好,現在手上有一本有序的牛津詞典,就是當今經常看到的這種,每次我們查apple這個詞時,就可以根據字母的順序,不到一分鐘就可以找出apple的釋義了。這樣一看,有序與無序就相差了12個小時,而且是每次查詢都節省12個小時,則讓每個人都可以節省更多的時間去做其他的事情。類比到計算機也是一樣的道理,節省下來的時間資源是巨大的,這種規模效益無疑值得我們去排序。

有時候,告訴一個人怎樣去做一件事不如告訴他為什麼要這麼做。例如,告訴一個新手程式設計師怎樣去優化一段程式碼,他有可能會拖延,但是告訴他說,一旦完成優化,每個使用者瀏覽所花費的時間都會節省5秒。這樣出來的效果是不一樣的。——觀點來源於網路

因此,覺得網路上很多文章一上來就直接說排序演算法的思想以及如何實現是不夠的。先要弄清楚為什麼要排序,再去了解排序演算法的細枝末節,畢竟所有排序演算法,都是為了一個目的服務的——節約時間。

三、經典內部排序演算法思想、視覺化、Java程式碼實現、改進方法、時間複雜度、空間複雜度、穩定性

演算法視覺化網站:
VisuAlgo
書籍:Data Structures and Algorithm Analysis in Java (Thrid Edition)

1.氣泡排序BubbleSort

介紹:
氣泡排序是一種較為容易理解的排序演算法,因為它就是相鄰數兩兩比較,符合條件就交換位置而已,如果從小到大排的話,就像水中升起的泡泡一樣越來越大

演算法步驟:基於交換
假設陣列a[n]有N個整數
第一趟,第一個數與第二個數比較,符合條件就交換位置,然後第二個數和第三個數比較,符合條件就交換位置,以此類推。如此,最後一個數字為最大數;
第二趟,除去第一趟最後一個數字,第一個數與第二個數比較,符合條件就交換位置,如此類推,此時最後一個數字為本趟最大數
第三趟,如上類推
……
第N-1趟,如上類推,所有交換完成後陣列元素有序

視覺化:


氣泡排序

Java程式碼實現:

public class BubbleSort {
    public static void sort(int[] a){
        int temp;//定義用於交換的臨時變數
        int length = a.length;//定義遞減長度變數

        //外迴圈,氣泡排序進行的趟數,取a.length-1是因為最後一趟只有一個數字,沒有必要排序
        for(int i=0;i<a.length-1;i++){
            //內迴圈,實際進行兩兩比較
            for(int j=0;j<length-1;j++){
                if(a[j]>a[j+1]){
                    temp = a[j+1];
                    a[j+1] = a[j];
                    a[j] = temp;
                }
            }
            length--;//每趟結束後,最後一個數字有序且為該趟最大,下一趟排序不必進行比較

            //用於在控制檯輸出每一趟的結果
            System.out.println();
            System.out.print("第"+(i+1)+"趟排序");
            for(int k = 0;k<a.length;k++){
                System.out.print(a[k]+" ");
            }
        }
    }

    public static void main(String[] args) {
        int[] a = {99,89,76,66,54,47,32,20,18,5};
        System.out.print("原陣列:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
        sort(a);
        System.out.println();
        System.out.print("氣泡排序結束後:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
    }
}

執行結果:

改進方法:
(1)設定標誌位,每一趟迴圈開始預設有序,當發生交換時,則陣列無序,仍需繼續迴圈
參考部落格白話經典算法系列之一 氣泡排序的三種實現

public static void improveBubbleSort(int[] a){
        int temp;//定義用於交換的臨時變數
        int length = a.length;//length儲存陣列長度,可以避免每次迴圈中呼叫a.length方法的消耗
        boolean flag = true;//設定標誌位為ture,假設初始狀態無序

        //若無序,則進行迴圈
        while(flag){
            flag = false;//假設元素正序
            for(int i = 0;i<length-1;i++){
                if(a[i]>a[i+1]){
                    temp = a[i+1];
                    a[i+1] = a[i];
                    a[i] = temp;
                    //若發生交換操作,則為無序狀態,仍需進行下一次迴圈
                    //若不發生交換操作,則此步不會執行,則為正序,只需執行一趟迴圈
                    flag=true;
                }
            }
        }
    }

(3)同樣是設定標誌位,不過這個標誌位用於記錄最後一次發生交換的位置,則下一次迴圈只需要執行到該位置即可,因為後面的元素已經有序。

時間複雜度:與比較次數、逆序數有關,一次交換減少一個逆序
最好情況為O(N)。按照最開始的思路,假設元素一開始全部有序,但即使不需要交換,都需要不斷迴圈比較,N個元素兩兩比較共需要N(N1)2次,再加上其他賦值等操作,所以時間複雜度為O(N2);改進方法一,若元素一開始全部有序,則一次迴圈即可比較次數為N-1,再加上其他賦值等操作,所以時間複雜度為O(N)。

最壞情況為O(N2)。元素一開始全部逆序,則逆序數有N(N1)2個,按照最開始的思路,則需要經歷N-1趟,共N(N1)2次比較與交換,再加上其他賦值等操作,所以時間複雜度為O(N2);採用改進一的演算法,也需要進行N-1次迴圈,所以時間複雜度為O(N2)。

平均情況為O(N2)。書上定理:N個互異數的陣列的平均逆序數是N(N1)4個;通過交換相鄰元素進行排序的任何演算法平均都需要O(N2)時間。為了消除對應的逆序數,所以時間複雜度為O(N2)。

空間複雜度:由於需要使用一個用於交換的臨時變數temp,與陣列規模N無關,所以空間複雜度為O(1)
穩定性分析:由於兩相等元素在氣泡排序前後的相對位置不變(相等不發生交換),所以是穩定的

2.選擇排序SelectionSort

介紹:和氣泡排序一樣通俗易懂,顧名思義,選擇排序就是在待排序陣列中選擇出最小(最大)的元素,放到最前面;然後再在待排序陣列中選出最小(最大)的元素放到前面,以此類推。

演算法步驟:基於選擇
假設陣列a[n]有N個整數,
第一趟,從陣列中找出陣列中最小的元素,將其與第一位的元素交換位置
第二趟,除去陣列中的第一個元素,從剩下的元素中,找出最小的元素,將其與剩下的元素中的第一位交換位置
……
以此類推,
第N-1趟,完成上述選擇排序,陣列有序
也可以找出最大元素放在陣列最後面,以此類推

public class SelectionSort {
    public static void sort(int[] a){
        int temp;//定義用於交換的臨時變數
        int length = a.length;//儲存漸變陣列長度
        int maxPos = 0;//定義最大元素位置

        for(int i = 0;i<a.length-1;i++){
            //找出最大元素的位置
            for(int j = 1;j<length;j++){
                if(a[maxPos]<a[j]){
                    maxPos = j;
                }
            }
            //交換陣列元素的位置
            temp = a[length-1];
            a[length-1] = a[maxPos];
            a[maxPos] = temp;

            length--;//除去最後那位最大的元素
            maxPos = 0;//最大元素位置置0

            //用於在控制檯輸出每一趟的結果
            System.out.println();
            System.out.print("第"+(i+1)+"趟排序");
            for(int k = 0;k<a.length;k++){
                System.out.print(a[k]+" ");
            }
        }

    }

    public static void main(String[] args) {
        int[] a = {99,74,25,88,54,63,41,33,4,17};
        System.out.print("原陣列:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
        sort(a);
        System.out.println();
        System.out.print("選擇排序結束後:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
    }
}

執行結果:
這裡寫圖片描述
改進方法:
(1)同時找最大的元素位置和最小元素的位置
參考部落格:排序演算法(二)——選擇排序及改進

public static void improveSort(int[] a){
        int maxTemp;//定義用於交換的臨時變數
        int minTemp;//定義用於交換的臨時變數
        int length = a.length;//儲存漸變陣列長度
        int maxPos;//定義最大元素位置
        int minPos;//定義最小元素位置

        //由於一趟確定兩個數,所以趟數為原來的1/2
        for(int i = 0;i<a.length/2;i++){
            maxPos = i;
            minPos = i;
            //同時找出最大元素位置及最小元素位置
            for(int j = i+1;j<length;j++){
                if(a[maxPos]<a[j]){
                    maxPos = j;
                }
                if(a[minPos]>a[j]){
                    minPos = j;
                }
            }
            //儲存最大最小元素數值,最後用於覆蓋首尾位置
            maxTemp = a[maxPos];
            minTemp = a[minPos];
            //如果第一個元素不是最大最小值,就是需要保護的數值,需要找一個不是首尾位置的地方儲存,優先考慮maxPos和minPos
            if(a[i]!=maxTemp&&a[i]!=minTemp){
                if(maxPos!=i&&maxPos!=length-1){//如果最大元素位置不是首尾,則可以覆蓋
                    a[maxPos]=a[i];
                }
                if(minPos!=i&&minPos!=length-1){//如果最小元素位置不是首尾,則可以覆蓋
                    a[minPos]=a[i];
                }
            }
            //如果最後一個元素不是最大最小值,就是需要保護的數值,需要找一個不是首尾位置的地方儲存,優先考慮maxPos和minPos
            if(a[length-1]!=maxTemp&&a[length-1]!=minTemp){
                if(maxPos!=i&&maxPos!=length-1){//如果最大元素位置不是首尾,則可以覆蓋
                    a[maxPos]=a[length-1];
                }
                if(minPos!=i&&minPos!=length-1){//如果最小元素位置不是首尾,則可以覆蓋
                    a[minPos]=a[length-1];
                }
            }
            //最後覆蓋首尾位置
            a[i] = minTemp;
            a[length-1] = maxTemp;

            length--;//除去最後那位最大的元素
        }
    }

注意,在這種同一個迴圈中,既要交換最大位置元素,又要交換最小位置元素的情況。如果先後交換,如
swap(a[maxPos],a[length-1])
swap(a[minPos],a[i])
則最先的那個交換有可能會改變後一次交換最小位置原本的數值,如minPos==length-1

時間複雜度:最好情況、最壞情況、平均情況均為O(N2)
採用改進前的演算法,無論陣列元素全部有序還是全部無序,都需要執行N-1趟共N(N1)2次比較來確定最大(小)值,再加上其他賦值等操作,平均也需要時間複雜度O(N2)。即時採用改進後的演算法,趟數減少一半,但是一趟的比較次數增加一倍,時間複雜度還是O(N2)。

空間複雜度:需要的額外輔助空間(用於交換等)與資料規模大小無關,所以為O(1)
穩定性分析:由於先掃描先排序,從左到右進行的話,相等的元素a左有可能被置於右邊,相等的元素a右有可能被置於左邊,所以是不穩定的

3.插入排序InsertiontSort

介紹:插入排序也是比較容易的排序演算法之一,主要是將待排序的陣列的每一個元素插入到有序陣列的對應位置上
演算法步驟:基於插入
假定陣列a[n]有N個元素,同時假設第一個元素a[0]位於有序區,剩餘元素皆位於無序區
第一趟,將無序區的第一個元素a[1],插入到有序區中,若比a[0]小,則將a[0]的數右移到a[1]處,將a[1]的元素插入到a[0]的位置上,有序區為a[0]、a[1]
第二趟,將無序區的第一個元素a[2],插入到有序區對應的位置上,相應的元素移動使有序區維持有序,
……
第N-1趟,完成插入操作,陣列有序

視覺化:
插入排序
Java程式碼實現:

public class InsertionSort {
    public static void sort(int[] a){
        int temp;//定義臨時變數用於儲存待插入元素
        int length = a.length; //儲存陣列a的長度
        int pos;//定義要插入的位置

        //開始遍歷,無序區從a[1]開始
        for(int i = 1;i<length;i++){
            temp = a[i];
            for(pos = i;pos>0&&temp<a[pos-1];pos--){
                a[pos] = a[pos-1];
            }
            a[pos] = temp;

            //用於在控制檯輸出每一趟的結果
            System.out.println();
            System.out.print("第"+i+"趟排序");
            for(int k = 0;k<a.length;k++){
                System.out.print(a[k]+" ");
            }
        }

    } 

    public static void main(String[] args) {
        int[] a = {99,74,25,88,54,63,41,33,4,17};
        System.out.print("原陣列:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
        sort(a);
        System.out.println();
        System.out.print("插入排序結束後:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
    }
}

執行結果
這裡寫圖片描述

改進方法:
(1)直接插入排序通過順序比較來確定插入點,比較次數較多,而這可以通過二分查詢的方法在有序區中確定插入點來減少比較次數,快速定位插入點。
參考部落格:排序演算法(三)——插入排序及改進

時間複雜度:
最好情況為O(N)。如果陣列一開始全部有序,則只需要進行一遍外迴圈即可,內迴圈因條件不成立無法開始,此時時間複雜度O(N)
最壞情況為O(N2)。如果陣列一開始反序,則需要經過N-1趟共N(N1)2次比較和移動,再加上其他操作,則時間複雜度為O(N2)
平均情況為O(N2)。從某種意義上看,插入排序隱含地通過交換相鄰元素完成排序(待插入元素與被移動元素逐一交換),因此也可看做與逆序數相關,平均逆序數N(N1)4個,因此時間複雜度為O(N2)。

空間複雜度:由於所需額外輔助空間與資料規模N無關(只需要一個臨時儲存待插入元素空間),因此空間複雜度為O(1)
穩定性分析:因為相等的元素,位於左邊的先進入有序區,右邊的進入有序區後也不會插入等左的左邊,因此是穩定的

4.希爾排序ShellSort

介紹:插入排序的升級版,使用不同的增量依次把原陣列分割成不同的子序列,對每個子序列進行插入排序,隨著演算法的進行,增量逐漸減少,直到比較相鄰元素的最後一趟排序為止,因此希爾排序也叫做縮減增量排序。因為插入排序對有序的情況效率較高,而隨著演算法的進行,陣列元素趨於有序,所以希爾排序的效率也比較高

演算法步驟:基於插入
假設陣列a[n]有N個元素,選擇增量為{1,3,5}
第一趟排序,根據增量為5,將原陣列分割為5個子序列,如{a[0]、a[5]、a[15]、……}、{a[1]、a[6]、a[11]、……}、{a[2]、a[7]、……}、……,依次對處於這些位置的序列進行插入排序
第二趟排序,根據增量為3,將第一趟排序結果分割為3個子序列,如{a[0]、a[3]、a[6]、……}、{a[1]、a[4]、a[7]、……}、……依次對處於這些位置的序列進行插入排序
第三趟排序,因為增量為1,對上一趟排序結果進行插入排序,演算法結束

視覺化:
希爾排序
Java程式碼實現:

public class ShellSort {
    //以N/2、N/4、……為增量序列
    public static void sort(int[] a){
        int pos;//定義要插入的位置
        int temp;////定義臨時變數用於儲存待插入元素
        int length = a.length;

        for(int gap = length/2;gap>0;gap/=2){//根據增量確定趟數
            for(int i = gap;i<length;i++){//執行插入排序,從0到gap-1為每子序列的有序區
                temp = a[i];
                for(pos = i;pos>=gap&&temp<a[pos-gap];pos-=gap){
                    a[pos] = a[pos-gap];
                }
                a[pos] = temp;
            }
            //用於在控制檯輸出每一趟的結果
            System.out.println();
            System.out.print(gap+"排序後:");
            for(int i = 0;i<a.length;i++){
                System.out.print(a[i]+" ");
            }
        }
    }

    public static void main(String[] args) {
        int[] a = {99,74,25,88,54,63,41,33,4,17};
        System.out.print("原陣列:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
        sort(a);
        System.out.println();
        System.out.print("插入排序結束後:");
        for(int i = 0;i<a.length;i++){
            System.out.print(a[i]+" ");
        }
    }
}

執行結果
這裡寫圖片描述

改進方法:
(1)希爾排序的執行時間依賴於增量序列的選擇,通過改變增量序列,來改進演算法的執行時間。

時間複雜度:目前只有最壞情況時間複雜度,其他情況尚無結論,由於過於複雜,所以一般很少用希爾排序
使用希爾增量時,最壞情況時間複雜度為O(N2)
使用Hibbard增量時,最壞情況時間複雜度為O(N32)
空間複雜度:與插入排序類似,額外輔助空間與陣列元素規模N無關,所以空間複雜度為O(1)
穩定性分析:由於不同趟之前會彼此打亂相等元素的相對位置,所以希爾排序是不穩定