幾種常見的排序算法


幾種常見的排序算法

冒泡排序(Bubble Sort):

  冒泡排序是一種計算機科學領域的較簡單的排序算法。以數字排序為例,冒泡排序讓相連的兩個數字進行比較,將比較大的數字放在右邊。假設最大的數字N在最左邊。第一趟排序的時候,N每次和右邊的數字做對比,都將比右邊的數字大,然后將N一直往右移動只到最右。情形猶如冒泡,因此稱為冒泡排序。

  以下為冒泡排序的一次范例:

    

冒泡排序示例
原始數據 10 1 66 20 19 3
第1次排序 1 10 20 19 3 66
第2次排序 1 10 19 3 20 66
第3次排序 1 10 3 19 20 66
第4次排序 1 3 10 19 20 66
第5次排序 1 3 10 19 20 66

  第一趟排序對數組的0、1位置進行排序,10>1,將1放在10之后;10<66,不動;66>20,對換20跟66的位置;66>19;對換66跟19的位置;66>3,對換66跟3的位置。從而獲取到了最大值66。

  第二趟排序進行跟上面一樣的對比,只是66可以不參與比較。獲取第二大的數字20

  第三趟排序獲取第三大的數字19。

  。。。。。。

  重復步驟,完成數字排序。

  以java代碼為例,進行編碼:

  

    public static void bubbleSorted(Integer[] list) {
        Integer temp;
        for (int i = 0; i < list.length; i++) {
            //如果沒有進行數據調換,則說明排序完成
            boolean didSwap = false;
            for (int j = 0; j < list.length - i - 1; j++) {
                if (list[j] > list[j + 1]) {
                    temp = list[j];
                    list[j] = list[j + 1];
                    list[j + 1] = temp;
                    didSwap = true;
                }
            }
            if (didSwap == false) {
                return;
            }
        }
    }

 

  分析:冒泡排序每一次都進行了N次迭代,因此排序的時間復雜度為O(n²)。它過於簡單了,以至於可以毫不費力地寫出來。然而當數據量很小的時候它會有些應用的價值,數據量比較小的時候其實也有其他更優的選擇,所以一般不會使用冒泡排序。

 

 

 

 

插入排序(insertion Sort):

 

  插入排序是一種最簡單的排序算法,假設待排序的數組長度為N,插入排序必須進行N-1趟排序。插入排序的第M趟排序,保證數組的0位置開始到M位置處於已排序狀態。

      以下為插入排序的一次范例:

插入排序示例
原始數據 10 1 66 20 19 3
第1次排序 1 10 66 20 19 3
第2次排序 1 10 66 20 19 3
第3次排序 1 10 20 66 19 3
第4次排序 1 10 19 20 66 3
第5次排序 1 3 10 19 20 66

  第一趟排序對數組的0、1位置進行排序,10>1,將1放在10之后。

  第二趟排序對數組的0、1、2位置進行排序,1<10<66,不進行數據移動。

  第三趟排序對數組的0、1、2、3位置進行排序,1<10<19<20。移動數據。

  。。。。。。

  重復上述步驟,直到數組中的所有元素排序完成。

 

  以java代碼為例,進行編碼:

  

 1     public static void insertionSorted(Integer[] list) {
 2         for (int i = 1; i < list.length; i++) {
 3             Integer temp = list[i];
 4             int j;
 5             for (j = i; j > 0 && list[j - 1] > temp; j--) {
 6                 list[j] = list[j - 1];
 7             }
 8             list[j] = temp;
 9         }
10     }

  分析:由於插入排序每一次都進行了N次迭代,因此插入排序的時間復雜度為O(n²)。而且在數組的數量比較小的情況下,插入排序的速度是很可觀的。插入排序跟冒泡排序經常會拿起來做比較,看下方數據比較:

冒泡排序和插入排序對比
算法名稱 最差時間復雜度 平均時間復雜度 最優時間復雜度 空間復雜度 穩定性
冒泡排序 O(N²) O(N²) O(N) O(1) 穩定
插入排序 O(N²) O(N²) O(N) O(1) 穩定

   

  兩者在數據上的表現非常一致。但是插入排序可能因為循環不成立而退出,從而減少了比較的次數。因此一般情況下,插入排序是相比冒泡排序更優秀的排序算法,小數據量的情況下一般優先選擇插入排序。

 

 

 

希爾排序(Shellsort):

  希爾排序的名稱來自他的發明者DonaldShell,希爾排序是第一批沖破二次時間屏障的算法之一。它的算法思路是通過比較一定距離的元素,逐漸縮減,直到比較距離為1的元素。所以希爾排序也叫縮減增量排序。

以下為希爾排序的一次范例:

希爾排序示例
原始數據 10 1 66 20 19 3
第1次排序(間距為3) 10 1 3 20 19 66
第2次排序(間距為2) 3 1 10 20 19 66
第3次排序(間距為1) 1 3 10 19 20 66

  

  第一趟排序間距為3,將10與20比較;1跟19比較;66跟3比較,交換66跟3的位置。

  第二趟排序間距為2,將10與3比較,交換10個3的位置;20跟1比較;10跟19比較;66跟20比較;

  第三趟排序間距為1,將1跟3比較,交換1跟3的位置;3跟10比較;10跟20比較;20跟19比較,交換19跟20的位置。19再跟10比較;20跟66比較;

 

  以java代碼為例,進行編程:

  

 1     public static void shellSorted(Integer[] list) {
 2         for (int gap = 3; gap > 0; gap--) {
 3             for (int i = gap; i < list.length; i++) {
 4                 Integer temp = list[i];
 5                 int j;
 6                 for (j = i; j - gap >= 0 && list[j - gap] > temp; j -= gap) {
 7                     list[j] = list[j - gap];
 8                 }
 9                 list[j] = temp;
10             }
11         }
12     }

  分析:上述代碼中,gap為3、2、1,稱為增量序列。希爾排序的效率依賴於增量序列的選擇。希爾排序的問題在於,增量未必是互素的。

  Hibbard提出一個增量序列,稱為Hibbard增量序列。Hibbard增量序列形如1、3、7、.....、2^k-1。因此增量之間沒有公因子,所以Hibbard增量的最壞運行情形時間為O(N^3/2),平均運行時間為O(N^5/4)。

  Sedgewick提出的幾種增量序列,最壞運行時間為O(N^4/3),平均運行時間為O(N^7/6),其中最好的序列為{1,5,19,41,109....},該序列9*4^i-9*2^i+1,或者4^i-3*2^i+1的形式。

 

堆排序(Heapsort):

  堆排序是一種使用堆為結構來進行排序的算法。利用二叉堆作為優先隊列,進行排序,每次獲取堆中最大值或者最小值,放入新序列中。實現了隊列的排序。

 

堆排序例子:

 

  第一步獲取最大值66;

  第二步獲取最大值20;

  第三部獲取最大值19;

     。。。。。

    以上述方式獲取新的排序隊列。

 

    以java代碼為例,進行編程:

    

 1     private static int leftChild(int i) {
 2         return 2 * i + 1;
 3     }
 4 
 5     private static void perDown(Integer[] list, int i, int n) {
 6         int child;
 7         Integer temp;
 8         for (temp = list[i]; leftChild(i) < n; i = child) {
 9             child = leftChild(i);
10             if (child != n - 1 && list[child] < list[child + 1]) {
11                 child++;
12             }
13             if (temp < list[child]) {
14                 list[i] = list[child];
15             } else {
16                 break;
17             }
18         }
19         list[i] = temp;
20     }
21 
22     public static void heapSort(Integer[] list) {
23         Integer temp;
24 
25         for (int i = list.length / 2 - 1; i >= 0; i--) {
26             perDown(list, i, list.length);
27         }
28         for (int i = list.length - 1; i > 0; i--) {
29             temp = list[0];
30             list[0] = list[i];
31             list[i] = temp;
32             perDown(list, 0, i);
33         }
34     }

    分析:堆排序種,第一階段構建堆最多用到2N次比較,第二階段,第i次deleteMax最多用到2Logi次比較,總共最多2N*LogN-O(N)次比較。堆排序也是一個非常穩定的算法。他的比較平均只比最壞情況指出的情況略少。

    

歸並排序(mergeSort):

   歸並排序的用法於上面幾種排序不同,歸並排序使用場景是存在已經排序完成的隊列A和隊列B,現要將已經排序完成A、B兩個隊列合並成隊列C;歸並排序以O(N*logN)的最壞情形時間運行,而使用的比較時間幾乎是最優的。它是遞歸算法的一個好的事例應用。

  歸並排序例子:

  

    A、B為已經排序的數組,C為新建數組。A、B數組一開始有個指針指向第一個元素。

    第一步,比較A、B第一個元素1<2。將1寫入C中,A的指針往右移動。

    第二部,比較2<4,將2寫入C中,B的指針往右移動。

    .....

    同上,A或者B全部寫入后,將另外的數組剩余的數字也寫入C中。

 

    在單個數組進行排序的時候使用歸並排序,則是將單個數組拆分成兩個數組,將這兩個數組分別排序完成后,再進行歸並。

    以java代碼為例,進行編程:

    

 1     public static void mergeSort(Integer[] list) {
 2         Integer[] tempArray = new Integer[list.length];
 3         mergeSort(list, tempArray, 0, list.length - 1);
 4     }
 5 
 6     private static void mergeSort(Integer[] list, Integer[] tempArray, int left, int right) {
 7         if (left >= right) {
 8             return;
 9         }
10         int center = (left + right) / 2;
11         mergeSort(list, tempArray, left, center);
12         mergeSort(list, tempArray, center + 1, right);
13         merge(list, tempArray, left, center + 1, right);
14     }
15 
16     private static void merge(Integer[] list, Integer[] tempArray, int leftPos, int rightPos, int rightEnd) {
17         int leftEnd = rightPos - 1;
18         int tempPos = leftPos;
19         int numberElements = rightEnd - leftPos + 1;
20 
21         while (leftPos <= leftEnd && rightPos <= rightEnd) {
22             if (list[leftPos] <= list[rightPos]) {
23                 tempArray[tempPos++] = list[leftPos++];
24             } else {
25                 tempArray[tempPos++] = list[rightPos++];
26             }
27         }
28 
29         while (leftPos <= leftEnd) {
30             tempArray[tempPos++] = list[leftPos++];
31         }
32 
33         while (rightPos <= rightEnd) {
34             tempArray[tempPos++] = list[rightPos++];
35         }
36 
37         for (int i = 0; i < numberElements; i++, rightEnd--) {
38             list[rightEnd] = tempArray[rightEnd];
39         }
40     }

    

    分析:歸並排序的運行時間為O(N*logN),但是歸並排序需要線性附加內存。整個算法在花費了數據拷貝再拷回來這些附加工作,減慢了排序速度。這種拷貝可以通過list和tempArray進行角色交換來避免。在java中歸並排序的數據移動是很省時的,因為歸並排序只是移動了對象的引用。在java類庫中泛型排序使用的就是歸並算法。

    

快速排序(mergeSort):

  快速排序是實踐中的一種快速排序算法,在Java中對基本類型的排序特別有用,也是面試時候最容易被問道的排序算法。快排的平均運行時間為O(N*logN),最壞情形為O(N^2),但是經過稍許努力就能讓最壞情況很難出現。通過快排和堆排序進行結合,由於堆排序的最壞為O(N*logN),我們可以對幾乎所有的輸入都能達到快速排序的快速運行時間。

  

  快排例子:

  第一步,隨機獲取樞紐元,上例中為19。

  第二步,將其他數字跟19做比較,一組比19小,一組等於19,一組大於19。

  第三步,將大於19和小於19的兩組進行排序。

  第四步,按照順序將他們寫入數組中。

 

  以java代碼為例,進行編程:

  

 1     public static void quickSort(Integer[] list) {
 2         quickSort(list, 0, list.length - 1);
 3     }
 4 
 5     private static void quickSort(Integer[] list, int left, int right) {
 6         if (left + CUTOFF <= right) {
 7             Integer pivot = media3(list, left, right);
 8             int i = left + 1, j = right - 2;
 9             while (true) {
10                 //=pivot,防止list[i]=list[j]=pivot,進入死循環
11                 while (i<list.length-1&&list[i] <=pivot) {
12                     i++;
13                 }
14                 while (j>0&&list[j] >= pivot) {
15                     j--;
16                 }
17                 if (i < j) {
18                     swapReferences(list, i, j);
19                 }else {
20                     break;
21                 }
22             }
23             //分組完成再把中值拎回來放在中間
24             swapReferences(list, i, right - 1);
25 
26             //防止數組中數字一樣進入死循環
27             if(left<i-1&&i-1<right){
28                 quickSort(list, left, i - 1);
29             }
30             if(left<i+1&&i+1<right){
31                 quickSort(list, i + 1, right);
32             }
33         } else {
34             insertionSorted(list, left, right);
35         }
36     }
37 
38     private static Integer media3(Integer[] list, int left, int right) {
39         int center = (left + right) / 2;
40         if (list[center] < list[left]) {
41             swapReferences(list, left, center);
42         }
43         if (list[right] < list[left]) {
44             swapReferences(list, left, right);
45         }
46         if (list[right] < list[center]) {
47             swapReferences(list, center, right);
48         }
49         //先把中值拎出去,方便交換操作
50         swapReferences(list, center, right - 1);
51         return list[right - 1];
52     }
53 
54     private static void swapReferences(Integer[] list, int i, int j) {
55         Integer temp = list[i];
56         list[i] = list[j];
57         list[j] = temp;
58     }

 

 

 

 

  分析:樞紐元的選擇對快排的效率有很大影響,隨機選取樞紐元是很比較靠譜的做法。比較不好的選取方式選取第一個或者最后一個,上述代碼的樞紐元用的三數中值分割法,取數組第一位,中間位,和最后一位,從三個數中去中間值作為樞紐元。因為快排是遞歸的分隔數組進行排序,當數組被切割得比較小的時候(N<=20),可以使用插入排序來完成,效率會更高。快速排序的最壞情況為O(N^2),最好情況為O(NlogN),平均情況也是O(NlogN)。

 

桶排序(bucketSorted):

  當輸入的數據由僅小於M的正整數來組成,可以使用桶排序來完成。假設有一個存在一個整數數組,數組中的元素不超過10。對這個數組進行排序,創建一個長度為11的數組count,count中的每個元素都為0,當讀入數據為i的時候,count(i)加上1,等全部讀取完畢之后根據count數將數組排列出來。

  桶排序例子:

    

    原數組進行迭代,讀到的數據在在count數組中對應下標的地方進行+1操作。等原數組迭代完成后,將count數組讀出就是排序完成的數組。

    

    java代碼如下:

    

 1     public static void bucketSorted(Integer[] list) {
 2         Integer[] count = new Integer[10];
 3         for (int i = 0; i < count.length; i++) {
 4             count[i] = 0;
 5         }
 6 
 7         for (Integer value : list) {
 8             count[value] += 1;
 9         }
10         int i = 0;
11         for (int k = 0; k < count.length - 1; k++) {
12             for (int j = 1; j <= count[k]; j++) {
13                 list[i++] = k;
14             }
15         }
16     }

 

    

  分析:桶排序比較簡單,然而在業務中很少有機會能使用到。當你的數字M上限比較高的時候,可以使用基數排序來實現。算法的用時為O(M+N),M為桶的個數。如果M為O(N),則總時間為O(N)。

 

    

 

基數排序(radix sort):

 

基數排序又叫做卡片排序,假設我們有大小在0~999之間數據進行排序,如果使用桶排序就不大合適。此時我們就可以使用基數排序,按照個位、十位、百位數進行三次排序。

  

基數排序例子:

  

    第一步,根據個位數排序,排序完成依次讀取。

    第二部,根據十位數排序,排序完成依次讀取。

    第三部,根據百位數排序,排序完成依次讀取。

    

    由於每一次排序的過程都保留了上一次排序的順序,所以最后數字能按照大小進行排序。

    

    以java代碼為例,進行編程:

  

 1     public static void radixSort(Integer[] list, int numberLen) {
 2 
 3         //初始化
 4         final int BUCKETS = 10;
 5         ArrayList<Integer>[] buckets = new ArrayList[BUCKETS];
 6         for (int i = 0; i < BUCKETS; i++) {
 7             buckets[i] = new ArrayList<Integer>();
 8         }
 9 
10         //排序
11         for (int i = 0; i < numberLen; i++) {
12 
13             for (Integer num : list) {
14                 buckets[(int) ((num / Math.pow(10, i)) % 10)].add(num);
15             }
16 
17             int j = 0;
18             for (ArrayList<Integer> bucket : buckets) {
19                 for (Integer number : bucket) {
20                     list[j++] = number;
21                 }
22                 bucket.clear();
23             }
24         }
25     }

    

  分析:基數排序的運行時間為O(p(N+b)),p是排序的趟數,對應上述代碼的numberLen。b為桶的個數,對應上述代碼中的BUCKETS。桶排序和基數排序實現了線性時間完成排序。但是桶排序和基數排序的條件相比其他排序苛刻。

 

計數基數排序(counting radix sort):

  計數基數排序是基數排序的另外一個實現,他避免使用ArrayList,取而代之的是用一個計數器。當數組掃描的對應的數字的時候,計數器在對應的位置上+1。等所有排序完成之后,根據計算器的數據,將對應的數據翻出來。完成排序。

 

  

 

  

  第一步,遍歷原數組,讀取到值之后,在對應的計數器上+1,比如讀取到5,則在計數器的5上加1。

  第二部,將計數器上數據進行疊加,獲取對應數字的位置,計數器上1的位置上為0的數量+1的數量,2位置上的數量為0的數量+1的數量+2的數量。

  第三部,根據計數器,重排排序數組。

  

  以java代碼為例,進行編程:

  

 1     public static Integer[] countingSort(Integer[] list) {
 2         Integer max = getMax(list);
 3 
 4         Integer min = getMin(list);
 5 
 6         Integer[] result = new Integer[list.length];
 7 
 8         int k = max - min + 1;
 9         int[] count = new int[k];
10         for (int i = 0; i < list.length; ++i) {
11             count[list[i] - min] += 1;
12         }
13         for (int i = 1; i < count.length; ++i) {
14             count[i] = count[i] + count[i - 1];
15         }
16         for (int i = list.length - 1; i >= 0; --i) {
17             result[--count[list[i] - min]] = list[i];//按存取的方式取出c的元素
18         }
19         return result;
20     }
21 
22     private static Integer getMax(Integer[] list) {
23         int b[] = new int[list.length];
24         int max = list[0];
25         for (int i : list) {
26             if (i > max) {
27                 max = i;
28             }
29         }
30         return max;
31     }
32 
33     private static Integer getMin(Integer[] list) {
34         int b[] = new int[list.length];
35         int min = list[0];
36         for (int i : list) {
37             if (i < min) {
38                 min = i;
39             }
40         }
41         return min;
42     }

   

    分析:計數排序也可以用來做字符串的排序。但是和桶排序有一個共同的問題是,當要進行排序的數字之間跨度非常大,或者字符串排序中字符串的長度特別長的時候,該算法的優勢就沒有那么明顯,甚至完全消失。

 

 

外部排序(external sorting):

  以上的各種算法,都是將要排序的數組放到內存之中進行排序,但是在一些特殊場景下,需要被排序的內容數量太大,而沒辦法裝載入內存的時候,我們就需要用到外部排序算法。下面簡單介紹一種在磁盤上進行外部排序的算法,因為在磁帶上訪問一個元素需要把磁帶轉到正確的位置上,因此磁帶需要被連續訪問才能提高效率。我們通過部分排序的方式,在磁帶上順序得讀入數據和寫出數據來完成排序。假設現在我們有4個磁帶,a1,a2,b1,b2,內存一次可以容納3個數據,見下方模型:

 

 

  

   1、一開始數據並無排序,因為內存每次可以讀入3個數據,每三個數據進行一次排序,保存12個數據中,有四隊有序數據,b1兩組,b2有兩組。

   2、使用歸並排序的套路,磁盤的磁頭分別指向兩組數據的開頭部分,讀入后進行比較,然后按順序寫入a1中。第三組數據和第四組數據寫入a2中。

   3、跟上述方式一樣,磁頭指向a1、a2的開始部分,然后將比較后的的數據寫入b1中。

   排序完成。

   

     分析:上方的模型比較簡單,當我們有更多磁帶,就可以進行多路合並,即同時比較多組數據來減少數據寫入寫出的次數。但是通過增加磁帶來減少讀寫次數並不方便,還可以通過多相合並,即通過分配不同磁帶的不同順串的數量來完成上述工作。

  

 

   總結排序是最古老,被研究得最完備的問題之一。對於大部分內部排序的應用,不是插入、希爾、歸並、就是快排。就主要取決於輸入數量的大小和底層環境決定的。插入排序適用於非常少量的輸入。中等規模用希爾是個不錯的選擇,只要增量序列合適,就能表現優異。歸並排序最壞情況為O(NLogN),但是卻需要額外的空間,但是比較的次數幾乎是最優的。快排並不保證提供最壞的時間復雜度,並且編程麻煩。但是幾乎可以肯定做到O(N logN),配合堆排序,就可以保證最壞情況為O(N logN)。基數排序可以在線性時間內排序,在特定環境下是相對比較的排序法更加實用。而外部排序則處理數據量大無法完全放入內存的情況。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM