1. 程式人生 > 其它 >演算法和資料結構—— 查詢和排序

演算法和資料結構—— 查詢和排序

本文為簡書作者鄭永欣原創,CDA資料分析師已獲得授權

查詢和排序都是程式設計中經常用到的演算法。查詢相對而言較為簡單,不外乎順序查詢、二分查詢、雜湊表查詢和二叉排序樹查詢。排序常見的有插入排序、氣泡排序、歸併排序和快速排序。其中我們應該重點掌握二分查詢、歸併排序和快速排序,保證能隨時正確、完整地寫出它們的程式碼。同時對其他的查詢和排序必須能準確說出它們的特點、對其平均時間複雜度、最差時間複雜度、額外空間消耗和穩定性爛熟於胸。

1、內排序:

  • 插入排序:直接插入排序(InsertSort)、希爾排序(ShellSort)
  • 交換排序:氣泡排序(BubbleSort)、快速排序(QuickSort)
  • 選擇排序:直接選擇排序(SelectSort)、堆排序(HeapSort)
  • 歸併排序(MergeSort)
  • 基數排序(RadixSort)

2、外排序:

  • 磁碟排序
  • 磁帶排序

3、查詢:

  • 線性表的查詢:順序查詢、折半查詢(二分查詢)、索引儲存結構和分塊查詢
  • 樹表的查詢:二叉排序樹、平衡二叉樹、B-樹、B+樹
  • 雜湊表查詢

快速排序(QuickSort)

平均/最好時間複雜度:O(nlogn)

最差時間複雜度:O(n^2)

平均空間複雜度:O(logn)

最差空間複雜度:O(n)

穩定性:不穩定

時間複雜度分析

快速排序總體的平均效果是最好的,當如果陣列本身已經排好序或幾乎有序的情況下,每輪排序又都是以最後一個數字作為比較的標準,那麼排序的效率就只有O(n^2)。

空間複雜度分析

快速排序通過遞迴來實現,遞迴造成的棧空間的使用,最好情況,遞迴樹的深度為log2n,其空間複雜度為O(logn),最壞情況,需要進行n‐1遞迴呼叫,其空間複雜度為O(n),平均情況,空間複雜度也為O(logn)。

穩定性分析

由於關鍵字的比較和交換是跳躍進行的,因此,快速排序是一種不穩定的排序方法。

排序思想

快速排序是對氣泡排序的一種改進。通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

經典實現

歸併排序(MergeSort)

平均/最好/最差時間複雜度:O(nlogn)

平均空間複雜度:O(n)

穩定性:穩定

複雜度分析

歸併排序比較佔用記憶體,但卻是一種效率高且穩定的演算法。

程式碼實現

二分查詢

平均/最差時間複雜度:O(logn)

平均查詢長度ASL:log2(n+1) - 1

空間複雜度:O(1)

演算法分析:折半查詢要求線性表是有序表。另外,由於折半查詢需要確定查詢的區間,所以只適用於順序儲存結構,不適用於鏈式儲存結構。為保持順序表的有序,表的插入和刪除操作都需要移動大量元素,所以折半查詢特別適用於一旦建立就很少改動,又經常需要進行查詢的線性表。

實現程式碼

索引儲存結構和分塊查詢

索引儲存結構

索引儲存結構是在儲存資料的同時,還建立附加的索引表。索引表中的每一項稱為索引項,索引項的結構一般形式為(關鍵字,地址)。

關鍵字唯一標識一個節點,地址是指向該關鍵字對應節點的指標,也可以是相對地址。

索引儲存結構的優缺點

優點:線性結構採用索引儲存後,可以對節點進行隨機訪問。在進行插入、刪除運算時,由於只需要修改索引表中相關節點的儲存地址,而不必移動儲存在節點表中的節點,所以仍可儲存較高的運算效率。

缺點:為了建立索引表需要增加時間和空間的開銷。

分塊查詢

分塊查詢又稱索引順序查詢,它是一種效能介於順序查詢和二分查詢之間的查詢方法。

分塊查詢需要按照如下的索引方式建立儲存線性表:將表R[0.. n-1]均分為b塊,前b-1塊中元素個數為s=?n/b?,最後一塊即第b塊的元素個數等於或小於s;每一塊的關鍵字不一定有序,但是前一塊中的最大關鍵字必須小於後一塊中的最小關鍵字,即要求表是“分塊有序”的。

分塊查詢的基本思路是:首先查詢索引表,因為索引表是有序表,故可以用折半查詢或順序查詢,以確定待查的元素在哪一塊;然後在已確定的塊中進行順序查詢(因塊內元素無序,只能用順序查詢)。

採用折半查詢來確定塊元素所在塊的平均查詢長度ASL:log2(b+1) + s/2,s越小,即每塊長度越小越好

採用順序查詢來確定塊元素所在塊的平均查詢長度ASL:(b+s)/2 + 1,s=√ ̄n時,ASL取極小值√ ̄n+1,即採用順序查詢確定塊時,各塊元素個數取√ ̄n最佳。

分塊查詢的缺點:增加一個索引表的儲存空間和建立索引表的時間。

氣泡排序(BubbleSort)

平均/最差時間複雜度:O(n^2)

最好時間複雜度:O(n)

平均空間複雜度:O(1)

穩定性:穩定

演算法思想

從最下面的元素開始,對每兩個相鄰的元素的關鍵字進行比較,且使關鍵字小的元素換至關鍵字大的元素之上,使得一趟排序後,關鍵字最小的元素到達最上端。

程式碼實現

選擇排序(SelectSort)

平均/最好/最差時間複雜度:O(n^2)

空間複雜度:O(1)

穩定性:不穩定

效能分析:適用於從大量元素中選擇一部分排序元素,例如從1w個元素中找出前10個元素

排序思路:

第i趟排序開始時,R[0 .. i-1]是有序區,而R[i .. n-1]是無序區。該趟排序是從當前無序區中選出關鍵字最小的元素R[k],將它和無序區的第一個元素R[i]交換,使得R[0.. i]和R[i+1 .. n-1]變成新的有序區和新的無序區。

程式碼實現

堆排序(heapSort)

平均/最好/最差時間複雜度:O(nlogn)

空間複雜度:O(1)

穩定性:不穩定

效能分析:由於建初始堆所需的比較次數較多,所以堆排序不適宜於記錄數較少的檔案。

排序思路

堆排序是一種樹形選擇排序方法,在排序過程中,將data[1 .. n]看成一顆完全二叉樹的順序儲存結構,利用完全二叉樹中雙親節點和孩子節點之間的內在關係,在當前無序區中選擇關鍵字最大(或最小)的元素。

程式碼實現

void heapSort(int[] data, int length) { // 為了與二叉樹的順序儲存結構一致,堆排序的資料序列的下標從1開始 if (data == null || length <= 0) throw new RuntimeException("Invalid Paramemers"); for (int i = length/2; i >= 1; i--) //初始化堆 sift(data, i, length); for (int i = length; i >=2; i--) { //進行n-1趟堆排序,每一趟堆排序的元素個數減1 swap(data, i, 1); //將最後一個元素同當前區間內data[1]對換 sift(data, 1, i-1); //篩選data[1]節點,得到i-1個節點的堆 } } void sift(int[] data, int low, int high) { int i = low, j = 2 * i; //data[j]是data[i]的左孩子 int tmp = data[i]; while (j <= high) { if (j < high && data[j] < data[j + 1]) //若右孩子較大,把j指向右孩子 j++; if (tmp < data[j]) { data[i] = data[j]; //將data[j]調整到雙親節點位置上 i = j; //修改i和j值,以便繼續向下篩選 j = 2 * i; } else break; //篩選結束 } data[i] = tmp; //被篩選節點的值放入最終位置 }

插入排序(InsertSort)

平均/最差時間複雜度:O(n^2)

最好時間複雜度:O(n)

空間複雜度:O(1)

穩定性:穩定

演算法思想

每次將一個待排序元素,按其關鍵字大小插入到已經排好序的子表中的恰當位置,知道全部元素插入完成為止。

實現程式碼

void insertSort(int[] data, int length) {    if (data == null || length <= 0) 
        throw new RuntimeException("Invalid Paramemers");    for (int i = 1; i < length; i++) {        int tmp = data[i];        int j = i-1;                      //從右向左在有序區data[0 .. i-1]中找data[i]的插入位置
        while (j >= 0 && data[j] > tmp) {
            data[j+1] = data[j];          //將大於data[i]的元素後移
            j--;
        }
        data[j+1] = tmp;
    }
}

希爾排序(ShellSort)

平均時間複雜度:O(n^1.3)

空間複雜度:O(1)

穩定性:不穩定

演算法分析:希爾排序和插入排序基本一致,為什麼希爾排序的時間效能會比插入排序優呢?直接插入排序在表初態為正序時所需時間最少,實際上,當表初態基本有序時直接插入排序所需的比較和移動次數都比較少。另一方面,當n值較小時,n和n^2的差別也比較小,即直接插入排序的最好時間複雜度O(n)和最差時間複雜度O(n^2)差別也不大。在希爾排序開始時,增量d1較大,分組較多,每組的元素數目少,故各組內直接插入排序較快,後來增量di逐漸縮小,分組數逐漸減少,而各組內的元素數目逐漸增多,但由於已經按di-1作為增量排過序,使表教接近有序狀態,所以新的一趟排序過程也較快。因此,希爾排序在效率上較直接插入排序有較大的改進。

程式碼實現

基數排序(RadixSort)

平均/最好/最差時間複雜度:O(d(n+r))

空間複雜度:O(r)

穩定性:穩定

實現程式碼:

各種內排序方法的比較和總結

按平均時間複雜度將排序分為3類:

  • 平方階O(n^2)排序,一般稱為簡單排序,例如直接插入排序,直接選擇排序和氣泡排序
  • 線性對數階O(nlogn)排序,如快速排序、堆排序和歸併排序
  • 線性階O(n)排序,如基數排序(假定資料的位數d和進位制r為常量時)

各種排序方法的效能

因為不同排序方法適應於不同的應用環境和要求,所以選擇適合的排序方法應綜合考慮下列因素:

  • 待排序的元素數目n(問題規模);
  • 元素的大小(每個元素的規模);
  • 關鍵字的結構及其初始狀態;
  • 對穩定性的要求;
  • 語言工具的條件;
  • 儲存結構;
  • 時間和空間複雜度等。

沒有一種排序方法是絕對好的。每一種排序方法都各有其優缺點,適用於不同的環境。因此,在實際應用中,應根據具體情況做選擇。首先考慮排序對穩定性的要求,若要求穩定,則只能在穩定方法中選取,否則可以在所有方法中選取;其次要考慮待排序節點數n的大小,若n較大,則可在改進方法中選取,否則在簡單方法中選取;然後在考慮其他因素。綜合考慮以上幾點可以得出的大致結論:

  • 若n較小(如n<=50),可採用直接插入排序或直接選擇排序。當元素規模較小時,直接插入排序較好;否則因為直接選擇排序移動的元素少於直接插入排序,應選直接選擇排序。
  • 若檔案初始狀態基本有序(指正序),則應選用直接插入、冒泡或隨機的快速排序。
  • 若n較大,則應採用時間複雜度為O(nlogn)的排序方法:快速排序、堆排序或歸併排序。快速排序被認為是目前基於比較的內部排序中較好的方法,當待排序的關鍵字隨機分佈時,快速排序的平均時間最短;但堆排序所需的輔助空間比快速排序少,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的,若要求穩定,則可選用歸併排序。
  • 若要將兩個有序表合併成一個新的有序表,最好的方法是歸併排序。
  • 在基於比較的排序方法中,至少是需要O(nlogn)的時間。而基數排序只需要一步就會引起r種可能的轉移,即把一個元素裝入r個佇列之一,因此一般情況下,基數排序可能在O(n)時間內完成對n個元素的排序。但遺憾的是,基數排序只適用於像字串和整數這類有明顯結構特徵的關鍵字,而當關鍵字的取值範圍屬於某個無窮集合(例如實數型關鍵字)時,無法使用基數排序,這時只有藉助於“比較”的方法來排序。由此可知,若n很大,元素的關鍵字位數較少且可以分解時,採用基數排序較好。

原文連結:http://www.jianshu.com/p/3471b2dfa2b4