1. 程式人生 > >程式設計師進階-八大演算法攻略

程式設計師進階-八大演算法攻略

常見的八大排序演算法,以及它們之間的關係如下所示:


一、插入排序-直接插入排序

      1.演算法思想:直接插入排序是一種簡單插入排序,基本思想是:把n個待排序的元素看成為一個有序表和一個無序表。開始時有序表中只包含1個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,將它插入到有序表中的適當位置,使之成為新的有序表,重複n-1次可完成排序過程 。

        很簡單吧,接下來,我們要將這個演算法轉化為程式語言。

         假設有一組無序序列 R0, R1, ... , RN-1。

         (1) 我們先將這個序列中下標為 0 的元素視為元素個數為 1 的有序序列。

        (2) 然後,我們要依次把 R1, R2, ... , RN-1 插入到這個有序序列中。所以,我們需要一個外部迴圈,從下標 1 掃描到 N-1 。

        (3) 接下來描述插入過程。假設這是要將 Ri 插入到前面有序的序列中。由前面所述,我們可知,插入Ri時,前 i-1 個數肯定已經是有序了。

        所以我們需要將Ri 和R0 ~ Ri-1 進行比較,確定要插入的合適位置。這就需要一個內部迴圈,我們一般是從後往前比較,即從下標 i-1 開始向 0 進行掃描。

    2.核心程式碼

public void insertSort(int[] list) {
    // 列印第一個元素
    System.out.format("i = %d:\t", 0);
    printPart(list, 0, 0);
 
    // 第1個數肯定是有序的,從第2個數開始遍歷,依次插入有序序列
    for (int i = 1; i < list.length; i++) {
        int j = 0;
        int temp = list[i]; // 取出第i個數,和前i-1個數比較後,插入合適位置
 
        // 因為前i-1個數都是從小到大的有序序列,所以只要當前比較的數(list[j])比temp大,就把這個數後移一位
        for (j = i - 1; j >= 0 && temp < list[j]; j--) {
            list[j + 1] = list[j];
        }
        list[j + 1] = temp;
 
        System.out.format("i = %d:\t", i);
        printPart(list, 0, i);
    }
}

    3.演算法時間、空間複雜度、穩定性分析


二、插入排序-希爾排序

1.演算法思想:把記錄按步長 gap分組,對每組記錄採用直接插入排序方法進行排序。

       隨著步長逐漸減小,所分成的組包含的記錄越來越多,當步長的值減小到1時,整個資料合成為一組,構成一組有序記錄,則完成排序。


在上面這幅圖中:

初始時,有一個大小為 10 的無序序列。

第一趟排序中,我們不妨設 gap1 = N / 2 = 5,即相隔距離為 5 的元素組成一組,可以分為 5 組。

接下來,按照直接插入排序的方法對每個組進行排序。

第二趟排序中,我們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離為 2 的元素組成一組,可以分為 2 組。

按照直接插入排序的方法對每個組進行排序。

第三趟排序中,再次把 gap 縮小一半,即gap3 = gap2 / 2 = 1。 這樣相隔距離為 1 的元素組成一組,即只有一組。

按照直接插入排序的方法對每個組進行排序。此時,排序已經結束

2.核心程式碼

public void shellSort(int[] list) {
    int gap = list.length / 2;
 
    while (1 <= gap) {
        // 把距離為 gap 的元素編為一個組,掃描所有組
        for (int i = gap; i < list.length; i++) {
            int j = 0;
            int temp = list[i];
 
            // 對距離為 gap 的元素組進行排序
            for (j = i - gap; j >= 0 && temp < list[j]; j = j - gap) {
                list[j + gap] = list[j];
            }
            list[j + gap] = temp;
        }
 
        System.out.format("gap = %d:\t", gap);
        printAll(list);
        gap = gap / 2; // 減小增量
    }
}

3.演算法時間、空間複雜度、穩定性分析



三、選擇排序-簡單選擇排序

1.演算法思想:

(1)從待排序序列中,找到關鍵字最小的元素;

(2)如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換;

(3)從餘下的 N - 1 個元素中,找出關鍵字最小的元素,重複(1)(2)步,直到排序結束。

2.核心程式碼

public void selectionSort(int[] list) {
    // 需要遍歷獲得最小值的次數
    // 要注意一點,當要排序 N 個數,已經經過 N-1 次遍歷後,已經是有序數列
    for (int i = 0; i < list.length - 1; i++) {
        int temp = 0;
        int index = i; // 用來儲存最小值得索引
 
        // 尋找第i個小的數值
        for (int j = i + 1; j < list.length; j++) {
            if (list[index] > list[j]) {
                index = j;
            }
        }
 
        // 將找到的第i個小的數值放在第i個位置上
        temp = list[index];
        list[index] = list[i];
        list[i] = temp;
 
        System.out.format("第 %d 趟:\t", i + 1);
        printAll(list);
    }
}

3.演算法時間、空間複雜度、穩定性分析


四、選擇排序-堆排序

1.演算法思想:

首先,按堆的定義將陣列R[0..n]調整為堆(這個過程稱為建立初始堆),交換R[0]和R[n];

然後,將R[0..n-1]調整為堆,交換R[0]和R[n-1];

如此反覆,直到交換了R[0]和R[1]為止。

以上思想可歸納為兩個操作:

(1)根據初始陣列去構造初始堆(構建一個完全二叉樹,保證所有的父結點都比它的孩子結點數值大)。

(2)每次交換第一個和最後一個元素,輸出最後一個元素(最大值),然後把剩下元素重新調整為大根堆。 

當輸出完最後一個元素後,這個陣列已經是按照從小到大的順序排列了。

2.核心程式碼

public void HeapAdjust(int[] array, int parent, int length) {

    int temp = array[parent]; // temp儲存當前父節點

    int child = 2 * parent + 1; // 先獲得左孩子

 

    while (child < length) {

        // 如果有右孩子結點,並且右孩子結點的值大於左孩子結點,則選取右孩子結點

        if (child + 1 < length && array[child] < array[child + 1]) {

            child++;

        }

 

        // 如果父結點的值已經大於孩子結點的值,則直接結束

        if (temp >= array[child])

            break;

 

        // 把孩子結點的值賦給父結點

        array[parent] = array[child];

 

        // 選取孩子結點的左孩子結點,繼續向下篩選

        parent = child;

        child = 2 * child + 1;

    }

 

    array[parent] = temp;

}

 

public void heapSort(int[] list) {

    // 迴圈建立初始堆

    for (int i = list.length / 2; i >= 0; i--) {

        HeapAdjust(list, i, list.length);

    }

 

    // 進行n-1次迴圈,完成排序

    for (int i = list.length - 1; i > 0; i--) {

        // 最後一個元素和第一元素進行交換

        int temp = list[i];

        list[i] = list[0];

        list[0] = temp;

 

        // 篩選 R[0] 結點,得到i-1個結點的堆

        HeapAdjust(list, 0, i);

        System.out.format("第 %d 趟: \t", list.length - i);

        printPart(list, 0, list.length - 1);

    }

}

3.演算法時間、空間複雜度、穩定性分析


五、交換排序-氣泡排序

1.演算法思想:假設有一個大小為 N 的無序序列。氣泡排序就是要每趟排序過程中通過兩兩比較,找到第 i 個小(大)的元素,將其往上排

以上圖為例,演示一下氣泡排序的實際流程:

假設有一個無序序列  { 4. 3. 1. 2, 5 }

第一趟排序:通過兩兩比較,找到第一小的數值 1 ,將其放在序列的第一位。

第二趟排序:通過兩兩比較,找到第二小的數值 2 ,將其放在序列的第二位。

第三趟排序:通過兩兩比較,找到第三小的數值 3 ,將其放在序列的第三位。

至此,所有元素已經有序,排序結束。 

要將以上流程轉化為程式碼,我們需要像機器一樣去思考,不然編譯器可看不懂。

假設要對一個大小為 N 的無序序列進行升序排序(即從小到大)。 

(1) 每趟排序過程中需要通過比較找到第 i 個小的元素。

所以,我們需要一個外部迴圈,從陣列首端(下標 0) 開始,一直掃描到倒數第二個元素(即下標 N - 2) ,剩下最後一個元素,必然為最大。

(2) 假設是第 i 趟排序,可知,前 i-1 個元素已經有序。現在要找第 i 個元素,只需從陣列末端開始,掃描到第 i 個元素,將它們兩兩比較即可。

所以,需要一個內部迴圈,從陣列末端開始(下標 N - 1),掃描到 (下標 i + 1)。

2.核心程式碼

public void bubbleSort(int[] list) {

    int temp = 0; // 用來交換的臨時數

 

    // 要遍歷的次數

    for (int i = 0; i < list.length - 1; i++) {

        // 從後向前依次的比較相鄰兩個數的大小,遍歷一次後,把陣列中第i小的數放在第i個位置上

        for (int j = list.length - 1; j > i; j--) {

            // 比較相鄰的元素,如果前面的數大於後面的數,則交換

            if (list[j - 1] > list[j]) {

                temp = list[j - 1];

                list[j - 1] = list[j];

                list[j] = temp;

            }

        }

 

        System.out.format("第 %d 趟:\t", i);

        printAll(list);

    }

}

3.演算法時間、空間複雜度、穩定性分析


六、交換排序-快速排序

1.演算法思想:

通過一趟排序將要排序的資料分割成獨立的兩部分:分割點左邊都是比它小的數,右邊都是比它大的數

然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

詳細的圖解往往比大堆的文字更有說明力,所以直接上圖:

上圖中,演示了快速排序的處理過程:

初始狀態為一組無序的陣列:2、4、5、1、3。

經過以上操作步驟後,完成了第一次的排序,得到新的陣列:1、2、5、4、3。

新的陣列中,以2為分割點,左邊都是比2小的數,右邊都是比2大的數。

因為2已經在陣列中找到了合適的位置,所以不用再動。

2左邊的陣列只有一個元素1,所以顯然不用再排序,位置也被確定。(注:這種情況時,left指標和right指標顯然是重合的。因此在程式碼中,我們可以通過設定判定條件left必須小於right,如果不滿足,則不用排序了)。

而對於2右邊的陣列5、4、3,設定left指向5,right指向3,開始繼續重複圖中的一、二、三、四步驟,對新的陣列進行排序。

2.核心程式碼
public int division(int[] list, int left, int right) {

    // 以最左邊的數(left)為基準

    int base = list[left];

    while (left < right) {

        // 從序列右端開始,向左遍歷,直到找到小於base的數

        while (left < right && list[right] >= base)

            right--;

        // 找到了比base小的元素,將這個元素放到最左邊的位置

        list[left] = list[right];

 

        // 從序列左端開始,向右遍歷,直到找到大於base的數

        while (left < right && list[left] <= base)

            left++;

        // 找到了比base大的元素,將這個元素放到最右邊的位置

        list[right] = list[left];

    }

 

    // 最後將base放到left位置。此時,left位置的左側數值應該都比left小;

    // 而left位置的右側數值應該都比left大。

    list[left] = base;

    return left;

}

 

private void quickSort(int[] list, int left, int right) {

 

    // 左下標一定小於右下標,否則就越界了

    if (left < right) {

        // 對陣列進行分割,取出下次分割的基準標號

        int base = division(list, left, right);

 

        System.out.format("base = %d:\t", list[base]);

        printPart(list, left, right);

 

        // 對“基準標號“左側的一組數值進行遞迴的切割,以至於將這些數值完整的排序

        quickSort(list, left, base - 1);

 

        // 對“基準標號“右側的一組數值進行遞迴的切割,以至於將這些數值完整的排序

        quickSort(list, base + 1, right);

    }

}

3.演算法時間、空間複雜度、穩定性分析


七、歸併排序

1.演算法思想:

將待排序序列R[0...n-1]看成是n個長度為1的有序序列,將相鄰的有序表成對歸併,得到n/2個長度為2的有序表;將這些有序序列再次歸併,得到n/4個長度為4的有序序列;如此反覆進行下去,最後得到一個長度為n的有序序列。

綜上可知:

歸併排序其實要做兩件事:

(1)“分解”——將序列每次折半劃分

(2)“合併”——將劃分後的序列段兩兩合併後排序

2.核心程式碼

(1)如何合併

在每次合併過程中,都是對兩個有序的序列段進行合併,然後排序。

這兩個有序序列段分別為 R[low, mid] 和 R[mid+1, high]。

先將他們合併到一個區域性的暫存陣列R2中,帶合併完成後再將R2複製回R中。

為了方便描述,我們稱 R[low, mid] 第一段,R[mid+1, high] 為第二段。

每次從兩個段中取出一個記錄進行關鍵字的比較,將較小者放入R2中。最後將各段中餘下的部分直接複製到R2中。

經過這樣的過程,R2已經是一個有序的序列,再將其複製回R中,一次合併排序就完成了。


public void Merge(int[] array, int low, int mid, int high) {

    int i = low; // i是第一段序列的下標

    int j = mid + 1; // j是第二段序列的下標

    int k = 0; // k是臨時存放合併序列的下標

    int[] array2 = new int[high - low + 1]; // array2是臨時合併序列



    // 掃描第一段和第二段序列,直到有一個掃描結束

    while (i <= mid && j <= high) {

        // 判斷第一段和第二段取出的數哪個更小,將其存入合併序列,並繼續向下掃描

        if (array[i] <= array[j]) {

            array2[k] = array[i];

            i++;

            k++;

        } else {

            array2[k] = array[j];

            j++;

            k++;

        }

    }



    // 若第一段序列還沒掃描完,將其全部複製到合併序列

    while (i <= mid) {

        array2[k] = array[i];

        i++;

        k++;

    }



    // 若第二段序列還沒掃描完,將其全部複製到合併序列

    while (j <= high) {

        array2[k] = array[j];

        j++;

        k++;

    }



    // 將合併序列複製到原始序列中

    for (k = 0, i = low; i <= high; i++, k++) {

        array[i] = array2[k];

    }

}

(2)如何分解

在某趟歸併中,設各子表的長度為gap,則歸併前R[0...n-1]中共有n/gap個有序的子表:R[0...gap-1], R[gap...2*gap-1], ... , R[(n/gap)*gap ... n-1]。

呼叫Merge將相鄰的子表歸併時,必須對錶的特殊情況進行特殊處理。

若子表個數為奇數,則最後一個子表無須和其他子表歸併(即本趟處理輪空):若子表個數為偶數,則要注意到最後一對子表中後一個子表區間的上限為n-1。 


public void MergePass(int[] array, int gap, int length) {

    int i = 0;



    // 歸併gap長度的兩個相鄰子表

    for (i = 0; i + 2 * gap - 1 < length; i = i + 2 * gap) {

        Merge(array, i, i + gap - 1, i + 2 * gap - 1);

    }



    // 餘下兩個子表,後者長度小於gap

    if (i + gap - 1 < length) {

        Merge(array, i, i + gap - 1, length - 1);

    }

}



public int[] sort(int[] list) {

    for (int gap = 1; gap < list.length; gap = 2 * gap) {

        MergePass(list, gap, list.length);

        System.out.print("gap = " + gap + ":\t");

        this.printAll(list);

    }

    return list;

}

3.演算法時間、空間複雜度、穩定性分析


八、基數排序

1.演算法思想:

不妨通過一個具體的例項來展示一下,基數排序是如何進行的。 

設有一個初始序列為: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。

我們知道,任何一個阿拉伯數,它的各個位數上的基數都是以0~9來表示的。

所以我們不妨把0~9視為10個桶。 

我們先根據序列的個位數的數字來進行分類,將其分到指定的桶中。例如:R[0] = 50,個位數上是0,將這個數存入編號為0的桶中。

分類後,我們在從各個桶中,將這些數按照從編號0到編號9的順序依次將所有數取出來。

這時,得到的序列就是個位數上呈遞增趨勢的序列。 

按照個位數排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。

接下來,可以對十位數、百位數也按照這種方法進行排序,最後就能得到排序完成的序列。

排序演算法對比