1. 程式人生 > >演算法與資料結構之排序演算法的相關知識,簡單易懂。

演算法與資料結構之排序演算法的相關知識,簡單易懂。

一、    氣泡排序

1)  概要

本章介紹排序演算法中的氣泡排序,重點講解氣泡排序的思想。

目錄

1. 氣泡排序介紹

2. 氣泡排序圖文說明

3. 氣泡排序的時間複雜度和穩定性

4. 氣泡排序實現

4.1 氣泡排序C實現

4.2 氣泡排序C++實現

4.3 氣泡排序Java實現

2)  氣泡排序介紹

氣泡排序(Bubble Sort),又被稱為氣泡排序或泡沫排序。

它是一種較簡單的排序演算法。它會遍歷若干次要排序的數列,每次遍歷時,它都會從前往後依次的比較相鄰兩個數的大小;如果前者比後者大,則交換它們的位置。這樣,一次遍歷之後,最大的元素就在數列的末尾!採用相同的方法再次遍歷時,第二大的元素就被排列在最大元素之前。重複此操作,直到整個數列都有序為止!

3)  氣泡排序圖文說明

氣泡排序C實現一

void bubble_sort1(int a[], int n)

{

   int i,j;

   for (i=n-1; i>0; i--)

    {

       // 將a[0...i]中最大的資料放在末尾

       for (j=0; j<i; j++)

       {

           if (a[j] > a[j+1])

                swap(a[j], a[j+1]);

       }

    }

}

下面以數列{20,40,30,10,60,50}為例,演示它的氣泡排序過程(如下圖)。

我們先分析第1趟排序

當i=5,j=0時,a[0]<a[1]。此時,不做任何處理!

當i=5,j=1時,a[1]>a[2]。此時,交換a[1]和a[2]的值;交換之後,a[1]=30,a[2]=40。

當i=5,j=2時,a[2]>a[3]。此時,交換a[2]和a[3]的值;交換之後,a[2]=10,a[3]=40。

當i=5,j=3時,a[3]<a[4]。此時,不做任何處理!

當i=5,j=4時,a[4]>a[5]。此時,交換a[4]和a[5]的值;交換之後,a[4]=50,a[3]=60。

於是,第1趟排序完之後,數列{20,40,30,10,60,50}變成了{20,30,10,40,50,60}。此時,數列末尾的值最大。

根據這種方法:

第2趟排序完之後,數列中a[5...6]是有序的。

第3趟排序完之後,數列中a[4...6]是有序的。

第4趟排序完之後,數列中a[3...6]是有序的。

第5趟排序完之後,數列中a[1...6]是有序的。

第5趟排序之後,整個數列也就是有序的了。

氣泡排序C實現二

觀察上面氣泡排序的流程圖,第3趟排序之後,資料已經是有序的了;第4趟和第5趟並沒有進行資料交換。

下面我們對氣泡排序進行優化,使它效率更高一些:新增一個標記,如果一趟遍歷中發生了交換,則標記為true,否則為false。如果某一趟沒有發生交換,說明排序已經完成!

void bubble_sort2(int a[], int n)

{

   int i,j;

   int flag;                 // 標記

   for (i=n-1; i>0; i--)

    {

       flag = 0;            // 初始化標記為0

       // 將a[0...i]中最大的資料放在末尾

       for (j=0; j<i; j++)

       {

           if (a[j] > a[j+1])

           {

                swap(a[j], a[j+1]);

                flag = 1;    // 若發生交換,則設標記為1

           }

       }

       if (flag==0)

           break;            // 若沒發生交換,則說明數列已有序。

    }

}

4)  氣泡排序的時間複雜度和穩定性

氣泡排序時間複雜度

氣泡排序的時間複雜度是O(N2)。

假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?N-1次!因此,氣泡排序的時間複雜度是O(N2)。

氣泡排序穩定性

氣泡排序是穩定的演算法,它滿足穩定演算法的定義。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

5)  氣泡排序實現

實現程式碼(BubbleSort.java)

/**

 * 氣泡排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class BubbleSort {

   /*

    * 氣泡排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void bubbleSort1(int[] a, int n) {

       int i,j;

       for (i=n-1; i>0; i--) {

           // 將a[0...i]中最大的資料放在末尾

           for (j=0; j<i; j++) {

                if (a[j] > a[j+1]) {

                    // 交換a[j]和a[j+1]

                    int tmp = a[j];

                    a[j] = a[j+1];

                    a[j+1] = tmp;

               }

           }

       }

    }

   /*

    * 氣泡排序(改進版)

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void bubbleSort2(int[] a, int n) {

       int i,j;

       int flag;                 // 標記

       for (i=n-1; i>0; i--) {

           flag = 0;            // 初始化標記為0

           // 將a[0...i]中最大的資料放在末尾

           for (j=0; j<i; j++) {

                if (a[j] > a[j+1]) {

                    // 交換a[j]和a[j+1]

                    int tmp = a[j];

                    a[j] = a[j+1];

                    a[j+1] = tmp;

                    flag = 1;    // 若發生交換,則設標記為1

                }

           }

           if (flag==0)

                break;            // 若沒發生交換,則說明數列已有序。

       }

    }

   public static void main(String[] args) {

       int i;

       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

       bubbleSort1(a, a.length);

       //bubbleSort2(a, a.length);

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

上面3種實現的原理和輸出結果都是一樣的。下面是它們的輸出結果:

before sort:20 40 30 10 60 50

after sort:10 20 30 40 50 60

二、    快速排序

1)  概要

本章介紹排序演算法中的快速排序。

目錄

1. 快速排序介紹

2. 快速排序圖文說明

3. 快速排序的時間複雜度和穩定性

4. 快速排序實現

4.1 快速排序C實現

4.2 快速排序C++實現

4.3 快速排序Java實現

2)  快速排序介紹

快速排序(Quick Sort)使用分治法策略。

它的基本思想是:選擇一個基準數,通過一趟排序將要排序的資料分割成獨立的兩部分;其中一部分的所有資料都比另外一部分的所有資料都要小。然後,再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

快速排序流程:

(1) 從數列中挑出一個基準值。

(2) 將所有比基準值小的擺放在基準前面,所有比基準值大的擺在基準的後面(相同的數可以到任一邊);在這個分割槽退出之後,該基準就處於數列的中間位置。

(3) 遞迴地把"基準值前面的子數列"和"基準值後面的子數列"進行排序。

3)  快速排序圖文說明

快速排序程式碼

/*

 * 快速排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     l-- 陣列的左邊界(例如,從起始位置開始排序,則l=0)

 *     r-- 陣列的右邊界(例如,排序截至到陣列末尾,則r=a.length-1)

 */

void quick_sort(int a[], int l, int r)

{

   if (l < r)

    {

       int i,j,x;

       i = l;

       j = r;

       x = a[i];

       while (i < j)

       {

           while(i < j && a[j] > x)

                j--; // 從右向左找第一個小於x的數

           if(i < j)

                a[i++] = a[j];

           while(i < j && a[i] < x)

                i++; // 從左向右找第一個大於x的數

            if(i < j)

                a[j--] = a[i];

       }

       a[i] = x;

       quick_sort(a, l, i-1); /* 遞迴呼叫 */

       quick_sort(a, i+1, r); /* 遞迴呼叫 */

    }

}

下面以數列a={30,40,60,10,20,50}為例,演示它的快速排序過程(如下圖)。

上圖只是給出了第1趟快速排序的流程。在第1趟中,設定x=a[i],即x=30。

(01) 從"右 --> 左"查詢小於x的數:找到滿足條件的數a[j]=20,此時j=4;然後將a[j]賦值a[i],此時i=0;接著從左往右遍歷。

(02) 從"左 --> 右"查詢大於x的數:找到滿足條件的數a[i]=40,此時i=1;然後將a[i]賦值a[j],此時j=4;接著從右往左遍歷。

(03) 從"右 --> 左"查詢小於x的數:找到滿足條件的數a[j]=10,此時j=3;然後將a[j]賦值a[i],此時i=1;接著從左往右遍歷。

(04) 從"左 --> 右"查詢大於x的數:找到滿足條件的數a[i]=60,此時i=2;然後將a[i]賦值a[j],此時j=3;接著從右往左遍歷。

(05) 從"右 --> 左"查詢小於x的數:沒有找到滿足條件的數。當i>=j時,停止查詢;然後將x賦值給a[i]。此趟遍歷結束!

按照同樣的方法,對子數列進行遞迴遍歷。最後得到有序陣列!

4)  快速排序的時間複雜度和穩定性

快速排序穩定性

快速排序是不穩定的演算法,它不滿足穩定演算法的定義。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

快速排序時間複雜度

快速排序的時間複雜度在最壞情況下是O(N2),平均的時間複雜度是O(N*lgN)。

這句話很好理解:假設被排序的數列中有N個數。遍歷一次的時間複雜度是O(N),需要遍歷多少次呢?至少lg(N+1)次,最多N次。

(01) 為什麼最少是lg(N+1)次?快速排序是採用的分治法進行遍歷的,我們將它看作一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的定義,它的深度至少是lg(N+1)。因此,快速排序的遍歷次數最少是lg(N+1)次。

(02) 為什麼最多是N次?這個應該非常簡單,還是將快速排序看作一棵二叉樹,它的深度最大是N。因此,快讀排序的遍歷次數最多是N次。

5)  快速排序實現

實現程式碼(QuickSort.java)

/**

 * 快速排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class QuickSort {

   /*

    * 快速排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     l -- 陣列的左邊界(例如,從起始位置開始排序,則l=0)

    *     r -- 陣列的右邊界(例如,排序截至到陣列末尾,則r=a.length-1)

    */

   public static void quickSort(int[] a, int l, int r) {

       if (l < r) {

           int i,j,x;

           i = l;

           j = r;

           x = a[i];

           while (i < j) {

                while(i < j && a[j]> x)

                    j--; // 從右向左找第一個小於x的數

                if(i < j)

                    a[i++] = a[j];

                while(i < j && a[i]< x)

                    i++; // 從左向右找第一個大於x的數

                if(i < j)

                    a[j--] = a[i];

           }

           a[i] = x;

           quickSort(a, l, i-1); /* 遞迴呼叫 */

           quickSort(a, i+1, r); /* 遞迴呼叫 */

       }

    }

   public static void main(String[] args) {

       int i;

       int a[] = {30,40,60,10,20,50};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

       quickSort(a, 0, a.length-1);

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

上面實現原理和輸出結果都是一樣的。下面是它們的輸出結果:

before sort:30 40 60 10 20 50

after sort:10 20 30 40 50 60

三、    直接插入排序

1)  概要

本章介紹排序演算法中的直接插入排序。內容包括:

1. 直接插入排序介紹

2. 直接插入排序圖文說明

3. 直接插入排序的時間複雜度和穩定性

4. 直接插入排序實現

4.1  直接插入排序C實現

4.2  直接插入排序C++實現

4.3  直接插入排序Java實現

2)  直接插入排序介紹

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

3)  直接插入排序圖文說明

直接插入排序程式碼

/*

 * 直接插入排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列的長度

 */

void insert_sort(int a[], int n)

{

   int i, j, k;

   for (i = 1; i < n; i++)

    {

       //為a[i]在前面的a[0...i-1]有序區間中找一個合適的位置

       for (j = i - 1; j >= 0; j--)

           if (a[j] < a[i])

                break;

       //如找到了一個合適的位置

       if (j != i - 1)

       {

           //將比a[i]大的資料向後移

           int temp = a[i];

           for (k = i - 1; k > j; k--)

                a[k + 1] = a[k];

           //將a[i]放到正確位置上

           a[k + 1] = temp;

       }

    }

}

下面選取直接插入排序的一箇中間過程對其進行說明。假設{20,30,40,10,60,50}中的前3個數已經排列過,是有序的了;接下來對10進行排列。示意圖如下:

圖中將數列分為有序區和無序區。我們需要做的工作只有兩個:(1)取出無序區中的第1個數,並找出它在有序區對應的位置。(2)將無序區的資料插入到有序區;若有必要的話,則對有序區中的相關資料進行移位。

4)  直接插入排序的時間複雜度和穩定性

直接插入排序時間複雜度

直接插入排序的時間複雜度是O(N2)。

假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?N-1!因此,直接插入排序的時間複雜度是O(N2)。

直接插入排序穩定性

直接插入排序是穩定的演算法,它滿足穩定演算法的定義。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

5)  直接插入排序實現

實現程式碼(InsertSort.java)

/**

 * 直接插入排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class InsertSort {

   /*

    * 直接插入排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void insertSort(int[] a, int n) {

       int i, j, k;

       for (i = 1; i < n; i++) {

           //為a[i]在前面的a[0...i-1]有序區間中找一個合適的位置

           for (j = i - 1; j >= 0; j--)

                if (a[j] < a[i])

                    break;

           //如找到了一個合適的位置

           if (j != i - 1) {

                //將比a[i]大的資料向後移

                int temp = a[i];

                for (k = i - 1; k > j; k--)

                    a[k + 1] = a[k];

                //將a[i]放到正確位置上

                a[k + 1] = temp;

           }

       }

    }

   public static void main(String[] args) {

       int i;

       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

            System.out.printf("%d ",a[i]);

       System.out.printf("\n");

       insertSort(a, a.length);

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

上面3種實現的原理和輸出結果都是一樣的。下面是它們的輸出結果:

before sort:20 40 30 10 60 50

after sort:10 20 30 40 50 60

四、    希爾排序

1)  概要

本章介紹排序演算法中的希爾排序。內容包括:

1. 希爾排序介紹

2. 希爾排序圖文說明

3. 希爾排序的時間複雜度和穩定性

4. 希爾排序實現

4.1 希爾排序C實現

4.2 希爾排序C++實現

4.3 希爾排序Java實現

2)  希爾排序介紹

希爾排序(Shell Sort)是插入排序的一種,它是針對直接插入排序演算法的改進。該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。

希爾排序實質上是一種分組插入方法。它的基本思想是:對於n個待排序的數列,取一個小於n的整數gap(gap被稱為步長)將待排序元素分成若干個組子序列,所有距離為gap的倍數的記錄放在同一個組中;然後,對各組內的元素進行直接插入排序。 這一趟排序完成之後,每一個組的元素都是有序的。然後減小gap的值,並重復執行上述的分組和排序。重複這樣的操作,當gap=1時,整個數列就是有序的。

3)  希爾排序圖文說明

希爾排序程式碼(一)

/*

 * 希爾排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列的長度

 */

void shell_sort1(int a[], int n)

{

   int i,j,gap;

   // gap為步長,每次減為原來的一半。

   for (gap = n / 2; gap > 0; gap /= 2)

    {

       // 共gap個組,對每一組都執行直接插入排序

       for (i = 0 ;i < gap; i++)

       {

           for (j = i + gap; j < n; j += gap)

           {

                // 如果a[j]< a[j-gap],則尋找a[j]位置,並將後面資料的位置都後移。

                if (a[j] < a[j - gap])

                {

                    int tmp = a[j];

                    int k = j - gap;

                    while (k >= 0 &&a[k] > tmp)

                    {

                        a[k + gap] = a[k];

                        k -= gap;

                    }

                    a[k + gap] = tmp;

               }

           }

       }

    }

}

在上面的希爾排序中,首先要選取步長gap的值。選取了gap之後,就將數列分成了gap個組,對於每一個組都執行直接插入排序。在排序完所有的組之後,將gap的值減半;繼續對數列進行分組,然後進行排序。重複這樣的操作,直到gap<0為止。此時,數列也就是有序的了。

為了便於觀察,我們將希爾排序中的直接插入排序獨立出來,得到程式碼(二)。

希爾排序程式碼(二)

/*

 * 對希爾排序中的單個組進行排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列總的長度

 *     i-- 組的起始位置

 *    gap -- 組的步長

 *

 *  組是"從i開始,將相隔gap長度的數都取出"所組成的!

 */

void group_sort(int a[], int n, int i,intgap)

{

   int j;

   for (j = i + gap; j < n; j += gap)

    {

       // 如果a[j] < a[j-gap],則尋找a[j]位置,並將後面資料的位置都後移。

       if (a[j] < a[j - gap])

       {

           int tmp = a[j];

           int k = j - gap;

           while (k >= 0 && a[k] > tmp)

           {

                a[k + gap] = a[k];

                k -= gap;

           }

           a[k + gap] = tmp;

       }

    }

}

/*

 * 希爾排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列的長度

 */

void shell_sort2(int a[], int n)

{

   int i,gap;

   // gap為步長,每次減為原來的一半。

   for (gap = n / 2; gap > 0; gap /= 2)

    {

       // 共gap個組,對每一組都執行直接插入排序

       for (i = 0 ;i < gap; i++)

           group_sort(a, n, i, gap);

    }

}

4)  演示希爾排序過程

下面以數列{80,30,60,40,20,10,50,70}為例,演示它的希爾排序過程。

第1趟:(gap=4)

當gap=4時,意味著將數列分為4個組: {80,20},{30,10},{60,50},{40,70}。對應數列: {80,30,60,40,20,10,50,70}

對這4個組分別進行排序,排序結果:{20,80},{10,30},{50,60},{40,70}。 對應數列:{20,10,50,40,80,30,60,70}

第2趟:(gap=2)

當gap=2時,意味著將數列分為2個組:{20,50,80,60}, {10,40,30,70}。 對應數列: {20,10,50,40,80,30,60,70}

注意:{20,50,80,60}實際上有兩個有序的數列{20,80}和{50,60}組成。

         {10,40,30,70}實際上有兩個有序的數列{10,30}和{40,70}組成。

對這2個組分別進行排序,排序結果:{20,50,60,80},{10,30,40,70}。 對應數列: {20,10,50,30,60,40,80,70}

第3趟:(gap=1)

當gap=1時,意味著將數列分為1個組:{20,10,50,30,60,40,80,70}

注意:{20,10,50,30,60,40,80,70}實際上有兩個有序的數列{20,50,60,80}和{10,30,40,70}組成。

對這1個組分別進行排序,排序結果:{10,20,30,40,50,60,70,80}

5)  希爾排序的時間複雜度和穩定性

希爾排序時間複雜度

希爾排序的時間複雜度與增量(即,步長gap)的選取有關。例如,當增量為1時,希爾排序退化成了直接插入排序,此時的時間複雜度為O(N2),而Hibbard增量的希爾排序的時間複雜度為O(N3/2)。

希爾排序穩定性

希爾排序是不穩定的演算法,它滿足穩定演算法的定義。對於相同的兩個數,可能由於分在不同的組中而導致它們的順序發生變化。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

6)  希爾排序實現

實現程式碼(ShellSort.java)

/**

 * 希爾排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class ShellSort {

   /**

    * 希爾排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void shellSort1(int[] a, int n) {

       // gap為步長,每次減為原來的一半。

       for (int gap = n / 2; gap > 0; gap /= 2) {

           // 共gap個組,對每一組都執行直接插入排序

           for (int i = 0 ;i < gap; i++) {

                for (int j = i + gap; j < n;j += gap) {

                    // 如果a[j] < a[j-gap],則尋找a[j]位置,並將後面資料的位置都後移。

                    if (a[j] < a[j - gap]) {

                        int tmp = a[j];

                        int k = j - gap;

                        while (k >= 0&& a[k] > tmp) {

                            a[k + gap] = a[k];

                            k -= gap;

                        }

                        a[k + gap] = tmp;

                    }

                }

           }

       }

    }

   /**

    * 對希爾排序中的單個組進行排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列總的長度

    *     i -- 組的起始位置

    *     gap -- 組的步長

    *

    *  組是"從i開始,將相隔gap長度的數都取出"所組成的!

    */

   public static void groupSort(int[] a, int n, int i,int gap) {

       for (int j = i + gap; j < n; j += gap) {

           // 如果a[j] < a[j-gap],則尋找a[j]位置,並將後面資料的位置都後移。

           if (a[j] < a[j - gap]) {

                int tmp = a[j];

                int k = j - gap;

                while (k >= 0 &&a[k] > tmp) {

                    a[k + gap] = a[k];

                    k -= gap;

                }

                a[k + gap] = tmp;

           }

       }

    }

   /**

    * 希爾排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void shellSort2(int[] a, int n) {

       // gap為步長,每次減為原來的一半。

       for (int gap = n / 2; gap > 0; gap /= 2) {

           // 共gap個組,對每一組都執行直接插入排序

           for (int i = 0 ;i < gap; i++)

                groupSort(a, n, i, gap);

       }

    }

   public static void main(String[] args) {

       int i;

       int a[] = {80,30,60,40,20,10,50,70};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

       shellSort1(a, a.length);

       //shellSort2(a, a.length);

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

上面實現的原理和輸出結果都是一樣的。下面是它們的輸出結果:

before sort:80 30 60 40 20 10 50 70

after sort:10 20 30 40 50 60 70 80

五、    選擇排序

1)  概要

本章介紹排序演算法中的選擇排序。

目錄

1. 選擇排序介紹

2. 選擇排序圖文說明

3. 選擇排序的時間複雜度和穩定性

4. 選擇排序實現

4.1 選擇排序C實現

4.2 選擇排序C++實現

4.3 選擇排序Java實現

2)  選擇排序介紹

選擇排序(Selection sort)是一種簡單直觀的排序演算法。

它的基本思想是:首先在未排序的數列中找到最小(or最大)元素,然後將其存放到數列的起始位置;接著,再從剩餘未排序的元素中繼續尋找最小(or最大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

3)  選擇排序圖文說明

選擇排序程式碼

/*

 * 選擇排序

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列的長度

 */

void select_sort(int a[], int n)

{

   int i;        // 有序區的末尾位置

    intj;        // 無序區的起始位置

   int min;    // 無序區中最小元素位置

   for(i=0; i<n; i++)

    {

       min=i;

       // 找出"a[i+1] ... a[n]"之間的最小元素,並賦值給min。

       for(j=i+1; j<n; j++)

       {

           if(a[j] < a[min])

                min=j;

       }

       // 若min!=i,則交換 a[i] 和 a[min]。

       // 交換之後,保證了a[0] ... a[i] 之間的元素是有序的。

       if(min != i)

           swap(a[i], a[min]);

    }

}

下面以數列{20,40,30,10,60,50}為例,演示它的選擇排序過程(如下圖)。

排序流程

第1趟:i=0。找出a[1...5]中的最小值a[3]=10,然後將a[0]和a[3]互換。 數列變化:20,40,30,10,60,50-- > 10,40,30,20,60,50

第2趟:i=1。找出a[2...5]中的最小值a[3]=20,然後將a[1]和a[3]互換。 數列變化:10,40,30,20,60,50-- > 10,20,30,40,60,50

第3趟:i=2。找出a[3...5]中的最小值,由於該最小值大於a[2],該趟不做任何處理。

第4趟:i=3。找出a[4...5]中的最小值,由於該最小值大於a[3],該趟不做任何處理。

第5趟:i=4。交換a[4]和a[5]的資料。 數列變化:10,20,30,40,60,50-- > 10,20,30,40,50,60

4)  選擇排序的時間複雜度和穩定性

選擇排序時間複雜度

選擇排序的時間複雜度是O(N2)。

假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?N-1!因此,選擇排序的時間複雜度是O(N2)。

選擇排序穩定性

選擇排序是穩定的演算法,它滿足穩定演算法的定義。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

5)  選擇排序實現

實現程式碼(SelectSort.java)

/**

 * 選擇排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class SelectSort {

   /*

    * 選擇排序

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void selectSort(int[] a, int n) {

       int i;        // 有序區的末尾位置

       int j;        // 無序區的起始位置

       int min;    // 無序區中最小元素位置

       for(i=0; i<n; i++) {

           min=i;

           // 找出"a[i+1] ... a[n]"之間的最小元素,並賦值給min。

            for(j=i+1; j<n; j++) {

                if(a[j] < a[min])

                    min=j;

           }

           // 若min!=i,則交換 a[i] 和 a[min]。

           // 交換之後,保證了a[0] ... a[i] 之間的元素是有序的。

           if(min != i) {

                int tmp = a[i];

                a[i] = a[min];

                a[min] = tmp;

           }

       }

    }

   public static void main(String[] args) {

       int i;

       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

       selectSort(a, a.length);

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

上面3種實現的原理和輸出結果都是一樣的。下面是它們的輸出結果:

before sort:20 40 30 10 60 50

after sort:10 20 30 40 50 60

六、    堆排序

1)  概要

本章介紹排序演算法中的堆排序。

目錄

1. 堆排序介紹

2. 堆排序圖文說明

3. 堆排序的時間複雜度和穩定性

4. 堆排序實現

4.1 堆排序C實現

4.2 堆排序C++實現

4.3 堆排序Java實現

2)  堆排序介紹

堆排序(Heap Sort)是指利用堆這種資料結構所設計的一種排序演算法。

因此,學習堆排序之前,有必要了解堆!若讀者不熟悉堆,建議先了解堆(建議可以通過二叉堆,左傾堆,斜堆,二項堆或斐波那契堆等文章進行了解),然後再來學習本章。

我們知道,堆分為"最大堆"和"最小堆"。最大堆通常被用來進行"升序"排序,而最小堆通常被用來進行"降序"排序。

鑑於最大堆和最小堆是對稱關係,理解其中一種即可。本文將對最大堆實現的升序排序進行詳細說明。

最大堆進行升序排序的基本思想:

① 初始化堆:將數列a[1...n]構造成最大堆。

② 交換資料:將a[1]和a[n]交換,使a[n]是a[1...n]中的最大值;然後將a[1...n-1]重新調整為最大堆。 接著,將a[1]和a[n-1]交換,使a[n-1]是a[1...n-1]中的最大值;然後將a[1...n-2]重新調整為最大值。 依次類推,直到整個數列都是有序的。

下面,通過圖文來解析堆排序的實現過程。注意實現中用到了"陣列實現的二叉堆的性質"。

在第一個元素的索引為 0 的情形中:

性質一:索引為i的左孩子的索引是(2*i+1);

性質二:索引為i的左孩子的索引是(2*i+2);

性質三:索引為i的父結點的索引是floor((i-1)/2);

例如,對於最大堆{110,100,90,40,80,20,60,10,30,50,70}而言:索引為0的左孩子的所有是1;索引為0的右孩子是2;索引為8的父節點是3。

3)  堆排序圖文說明

堆排序(升序)程式碼

/*

 * (最大)堆的向下調整演算法

 *

 * 注:陣列實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。

 *     其中,N為陣列下標索引值,如陣列中第1個數對應的N為0。

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *    start -- 被下調節點的起始位置(一般為0,表示從第1個開始)

 *    end   -- 截至範圍(一般為陣列中最後一個元素的索引)

 */

void maxheap_down(int a[], int start, intend)

{

   int c = start;            // 當前(current)節點的位置

   int l = 2*c + 1;        // 左(left)孩子的位置

   int tmp = a[c];            // 當前(current)節點的大小

   for (; l <= end; c=l,l=2*l+1)

    {

       // "l"是左孩子,"l+1"是右孩子

       if ( l < end && a[l] < a[l+1])

           l++;        // 左右兩孩子中選擇較大者,即m_heap[l+1]

       if (tmp >= a[l])

           break;        // 調整結束

       else            // 交換值

       {

           a[c] = a[l];

           a[l]= tmp;

       }

    }

}

/*

 * 堆排序(從小到大)

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *     n-- 陣列的長度

 */

void heap_sort_asc(int a[], int n)

{

   int i;

   // 從(n/2-1) --> 0逐次遍歷。遍歷之後,得到的陣列實際上是一個(最大)二叉堆。

   for (i = n / 2 - 1; i >= 0; i--)

       maxheap_down(a, i, n-1);

   // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素

   for (i = n - 1; i > 0; i--)

    {

       // 交換a[0]和a[i]。交換後,a[i]是a[0...i]中最大的。

       swap(a[0], a[i]);

        // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。

       // 即,保證a[i-1]是a[0...i-1]中的最大值。

       maxheap_down(a, 0, i-1);

    }

}

heap_sort_asc(a, n)的作用是:對陣列a進行升序排序;其中,a是陣列,n是陣列長度。

heap_sort_asc(a, n)的操作分為兩部分:初始化堆 和 交換資料。

maxheap_down(a, start, end)是最大堆的向下調整演算法。

下面演示heap_sort_asc(a, n)對a={20,30,90,40,70,110,60,10,100,50,80}, n=11進行堆排序過程。下面是陣列a對應的初始化結構:

1 初始化堆

在堆排序演算法中,首先要將待排序的陣列轉化成二叉堆。

下面演示將陣列{20,30,90,40,70,110,60,10,100,50,80}轉換為最大堆{110,100,90,40,80,20,60,10,30,50,70}的步驟。

1.1 i=11/2-1,即i=4

上面是maxheap_down(a, 4, 9)調整過程。maxheap_down(a, 4, 9)的作用是將a[4...9]進行下調;a[4]的左孩子是a[9],右孩子是a[10]。調整時,選擇左右孩子中較大的一個(即a[10])和a[4]交換。

1.2 i=3

上面是maxheap_down(a, 3, 9)調整過程。maxheap_down(a, 3, 9)的作用是將a[3...9]進行下調;a[3]的左孩子是a[7],右孩子是a[8]。調整時,選擇左右孩子中較大的一個(即a[8])和a[4]交換。

1.3 i=2

上面是maxheap_down(a, 2, 9)調整過程。maxheap_down(a, 2, 9)的作用是將a[2...9]進行下調;a[2]的左孩子是a[5],右孩子是a[6]。調整時,選擇左右孩子中較大的一個(即a[5])和a[2]交換。

1.4 i=1

上面是maxheap_down(a, 1, 9)調整過程。maxheap_down(a, 1, 9)的作用是將a[1...9]進行下調;a[1]的左孩子是a[3],右孩子是a[4]。調整時,選擇左右孩子中較大的一個(即a[3])和a[1]交換。交換之後,a[3]為30,它比它的右孩子a[8]要大,接著,再將它們交換。

1.5 i=0

上面是maxheap_down(a, 0, 9)調整過程。maxheap_down(a, 0, 9)的作用是將a[0...9]進行下調;a[0]的左孩子是a[1],右孩子是a[2]。調整時,選擇左右孩子中較大的一個(即a[2])和a[0]交換。交換之後,a[2]為20,它比它的左右孩子要大,選擇較大的孩子(即左孩子)和a[2]交換。

調整完畢,就得到了最大堆。此時,陣列{20,30,90,40,70,110,60,10,100,50,80}也就變成了{110,100,90,40,80,20,60,10,30,50,70}。

第2部分 交換資料

在將陣列轉換成最大堆之後,接著要進行交換資料,從而使陣列成為一個真正的有序陣列。

交換資料部分相對比較簡單,下面僅僅給出將最大值放在陣列末尾的示意圖。

上面是當n=10時,交換資料的示意圖。

當n=10時,首先交換a[0]和a[10],使得a[10]是a[0...10]之間的最大值;然後,調整a[0...9]使它稱為最大堆。交換之後:a[10]是有序的!

當n=9時, 首先交換a[0]和a[9],使得a[9]是a[0...9]之間的最大值;然後,調整a[0...8]使它稱為最大堆。交換之後:a[9...10]是有序的!

...

依此類推,直到a[0...10]是有序的。

4)  堆排序的時間複雜度和穩定性

堆排序時間複雜度

堆排序的時間複雜度是O(N*lgN)。

假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?

堆排序是採用的二叉堆進行排序的,二叉堆就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的定義,它的深度至少是lg(N+1)。最多是多少呢?由於二叉堆是完全二叉樹,因此,它的深度最多也不會超過lg(2N)。因此,遍歷一趟的時間複雜度是O(N),而遍歷次數介於lg(N+1)和lg(2N)之間;因此得出它的時間複雜度是O(N*lgN)。

堆排序穩定性

堆排序是不穩定的演算法,它不滿足穩定演算法的定義。它在交換資料的時候,是比較父結點和子節點之間的資料,所以,即便是存在兩個數值相等的兄弟節點,它們的相對順序在排序也可能發生變化。

演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!

堆排序實現

這實現的原理和輸出結果都是一樣的,每一種實現中都包括了"最大堆對應的升序排列"和"最小堆對應的降序排序"。

5)  實現程式碼(HeapSort.java)

/**

 * 堆排序:Java

 *

 *@author skywang

 *@date 2014/03/11

 */

public class HeapSort {

   /*

    * (最大)堆的向下調整演算法

    *

    * 注:陣列實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。

    *     其中,N為陣列下標索引值,如陣列中第1個數對應的N為0。

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     start -- 被下調節點的起始位置(一般為0,表示從第1個開始)

    *     end   -- 截至範圍(一般為陣列中最後一個元素的索引)

    */

   public static void maxHeapDown(int[] a, int start, int end) {

       int c = start;            // 當前(current)節點的位置

       int l = 2*c + 1;        // 左(left)孩子的位置

       int tmp = a[c];            // 當前(current)節點的大小

       for (; l <= end; c=l,l=2*l+1) {

           // "l"是左孩子,"l+1"是右孩子

           if ( l < end && a[l] < a[l+1])

                l++;        // 左右兩孩子中選擇較大者,即m_heap[l+1]

           if (tmp >= a[l])

                break;        // 調整結束

           else {            // 交換值

                a[c] = a[l];

                a[l]= tmp;

           }

       }

    }

   /*

    * 堆排序(從小到大)

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void heapSortAsc(int[] a, int n) {

       int i,tmp;

       // 從(n/2-1) --> 0逐次遍歷。遍歷之後,得到的陣列實際上是一個(最大)二叉堆。

       for (i = n / 2 - 1; i >= 0; i--)

           maxHeapDown(a, i, n-1);

       // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素

       for (i = n - 1; i > 0; i--) {

           // 交換a[0]和a[i]。交換後,a[i]是a[0...i]中最大的。

           tmp = a[0];

           a[0] = a[i];

           a[i] = tmp;

           // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。

           // 即,保證a[i-1]是a[0...i-1]中的最大值。

           maxHeapDown(a, 0, i-1);

       }

    }

   /*

    * (最小)堆的向下調整演算法

    *

    * 注:陣列實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。

    *     其中,N為陣列下標索引值,如陣列中第1個數對應的N為0。

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     start -- 被下調節點的起始位置(一般為0,表示從第1個開始)

    *     end   -- 截至範圍(一般為陣列中最後一個元素的索引)

    */

   public static void minHeapDown(int[] a, int start, int end) {

       int c = start;            // 當前(current)節點的位置

       int l = 2*c + 1;        // 左(left)孩子的位置

       int tmp = a[c];            // 當前(current)節點的大小

       for (; l <= end; c=l,l=2*l+1) {

           // "l"是左孩子,"l+1"是右孩子

           if ( l < end && a[l] > a[l+1])

                l++;        // 左右兩孩子中選擇較小者

           if (tmp <= a[l])

                break;        // 調整結束

           else {            // 交換值

                a[c] = a[l];

                a[l]= tmp;

           }

       }

    }

   /*

    * 堆排序(從大到小)

    *

    * 引數說明:

    *     a -- 待排序的陣列

    *     n -- 陣列的長度

    */

   public static void heapSortDesc(int[] a, int n) {

       int i,tmp;

       // 從(n/2-1) --> 0逐次遍歷每。遍歷之後,得到的陣列實際上是一個最小堆。

       for (i = n / 2 - 1; i >= 0; i--)

           minHeapDown(a, i, n-1);

       // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素

       for (i = n - 1; i > 0; i--) {

           // 交換a[0]和a[i]。交換後,a[i]是a[0...i]中最小的。

           tmp = a[0];

           a[0] = a[i];

           a[i] = tmp;

           // 調整a[0...i-1],使得a[0...i-1]仍然是一個最小堆。

           // 即,保證a[i-1]是a[0...i-1]中的最小值。

           minHeapDown(a, 0, i-1);

       }

    }

   public static void main(String[] args) {

       int i;

       int a[] = {20,30,90,40,70,110,60,10,100,50,80};

       System.out.printf("before sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

       heapSortAsc(a, a.length);           // 升序排列

       //heapSortDesc(a, a.length);       // 降序排列

       System.out.printf("after sort:");

       for (i=0; i<a.length; i++)

           System.out.printf("%d ", a[i]);

       System.out.printf("\n");

    }

}

它們的輸出結果:

before sort:20 30 90 40 70 110 60 10 100 5080

after sort:10 20 30 40 50 60 70 80 90 100 110

七、    歸併排序

1)  概要

本章介紹排序演算法中的歸併排序。內容包括:

1. 歸併排序介紹

2. 歸併排序圖文說明

3. 歸併排序的時間複雜度和穩定性

4. 歸併排序實現

歸併排序介紹

將兩個的有序數列合併成一個有序數列,我們稱之為"歸併"。

歸併排序(Merge Sort)就是利用歸併思想對數列進行排序。根據具體的實現,歸併排序包括"從上往下"和"從下往上"2種方式。

1. 從下往上的歸併排序:將待排序的數列分成若干個長度為1的子數列,然後將這些數列兩兩合併;得到若干個長度為2的有序數列,再將這些數列兩兩合併;得到若干個長度為4的有序數列,再將它們兩兩合併;直接合併成一個數列為止。這樣就得到了我們想要的排序結果。(參考下面的圖片)

2. 從上往下的歸併排序:它與"從下往上"在排序上是反方向的。它基本包括3步:

① 分解 -- 將當前區間一分為二,即求分裂點 mid = (low + high)/2;

② 求解 -- 遞迴地對兩個子區間a[low...mid]和 a[mid+1...high]進行歸併排序。遞迴的終結條件是子區間長度為1。

③ 合併 -- 將已排序的兩個子區間a[low...mid]和 a[mid+1...high]歸併為一個有序的區間a[low...high]。

下面的圖片很清晰的反映了"從下往上"和"從上往下"的歸併排序的區別。

2)  歸併排序圖文說明

歸併排序(從上往下)程式碼

/*

 * 將一個數組中的兩個相鄰有序區間合併成一個

 *

 * 引數說明:

 *     a-- 包含兩個有序區間的陣列

 *    start -- 第1個有序區間的起始地址。

 *    mid   -- 第1個有序區間的結束地址。也是第2個有序區間的起始地址。

 *    end   -- 第2個有序區間的結束地址。

 */

void merge(int a[], int start, int mid, intend)

{

   int *tmp = (int *)malloc((end-start+1)*sizeof(int));    // tmp是彙總2個有序區的臨時區域

   int i = start;            // 第1個有序區的索引

   int j = mid + 1;        // 第2個有序區的索引

   int k = 0;                // 臨時區域的索引

   while(i <= mid && j <= end)

    {

       if (a[i] <= a[j])

           tmp[k++] = a[i++];

       else

           tmp[k++] = a[j++];

    }

   while(i <= mid)

       tmp[k++] = a[i++];

   while(j <= end)

       tmp[k++] = a[j++];

   // 將排序後的元素,全部都整合到陣列a中。

   for (i = 0; i < k; i++)

       a[start + i] = tmp[i];

   free(tmp);

}

/*

 * 歸併排序(從上往下)

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *    start -- 陣列的起始地址

 *    endi -- 陣列的結束地址

 */

void merge_sort_up2down(int a[], int start,int end)

{

   if(a==NULL || start >= end)

       return ;

   int mid = (end + start)/2;

   merge_sort_up2down(a, start, mid); // 遞迴排序a[start...mid]

   merge_sort_up2down(a, mid+1, end); // 遞迴排序a[mid+1...end]

   // a[start...mid] 和 a[mid...end]是兩個有序空間,

   // 將它們排序成一個有序空間a[start...end]

   merge(a, start, mid, end);

}

從上往下的歸併排序採用了遞迴的方式實現。它的原理非常簡單,如下圖:

通過"從上往下的歸併排序"來對陣列{80,30,60,40,20,10,50,70}進行排序時:

1. 將陣列{80,30,60,40,20,10,50,70}看作由兩個有序的子陣列{80,30,60,40}和{20,10,50,70}組成。對兩個有序子樹組進行排序即可。

2. 將子陣列{80,30,60,40}看作由兩個有序的子陣列{80,30}和{60,40}組成。

    將子陣列{20,10,50,70}看作由兩個有序的子陣列{20,10}和{50,70}組成。

3. 將子陣列{80,30}看作由兩個有序的子陣列{80}和{30}組成。

    將子陣列{60,40}看作由兩個有序的子陣列{60}和{40}組成。

    將子陣列{20,10}看作由兩個有序的子陣列{20}和{10}組成。

    將子陣列{50,70}看作由兩個有序的子陣列{50}和{70}組成。

歸併排序(從下往上)程式碼

/*

 * 對陣列a做若干次合併:陣列a的總長度為len,將它分為若干個長度為gap的子陣列;

 *            將"每2個相鄰的子陣列" 進行合併排序。

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *    len -- 陣列的長度

 *    gap -- 子陣列的長度

 */

void merge_groups(int a[], int len, intgap)

{

   int i;

   int twolen = 2 * gap;    // 兩個相鄰的子陣列的長度

   // 將"每2個相鄰的子陣列" 進行合併排序。

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

    {

       merge(a, i, i+gap-1, i+2*gap-1);

    }

   // 若 i+gap-1 < len-1,則剩餘一個子陣列沒有配對。

   // 將該子數組合併到已排序的陣列中。

   if ( i+gap-1 < len-1)

    {

       merge(a, i, i + gap - 1, len - 1);

    }

}

/*

 * 歸併排序(從下往上)

 *

 * 引數說明:

 *     a-- 待排序的陣列

 *    len -- 陣列的長度

 */

void merge_sort_down2up(int a[], int len)

{

   int n;

   if (a==NULL || len<=0)

       return ;

   for(n = 1; n < len; n*=2)

       merge_groups(a, len, n);

}

從下往上的歸併排序的思想正好與"從下往上的歸併排序"相反。如下圖:

通過"從下往上的歸併排序"來對陣列{80,30,60,40,20,10,50,70}進行排序時:

1. 將陣列{80,30,60,40,20,10,50,70}看作由8個有序的子陣列{80},{30},{60},{40},{20},{10},{50}和{70}組成。

2. 將這8個有序的子數列兩兩合併。得到4個有序的子樹列{30,80},{40,60},{10,20}和{50,70}。

3. 將這4個有序的子數列兩兩合併。得到2個有序的子樹列{30,40,60,80}和{10,20,50,70}。

4. 將這2個有序的子數列兩兩合併。得到1個有序的子樹列{10,20,30,40,50,60,70,80}。

3)  歸併排序的時間複雜度和穩定性

歸併排序時間複雜度

歸併排序的時間複雜度是O(N*lgN)。

假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要