1. 程式人生 > 實用技巧 >讀書筆記:《資料結構與演算法分析Java語言描述》

讀書筆記:《資料結構與演算法分析Java語言描述》

目錄

第 3 章 表、棧和佇列

3.2 表 ADT

3.2.1 表的簡單陣列實現

  • 利於查詢,不利於增刪

3.2.2 簡單鏈表

  • 單鏈表
  • 雙鏈表

3.3 Java Collections API 中的表

在類庫中,Java 語言包含有一些普通資料結構的實現。該語言的這一部分通常叫作Collections API。

3.3.1 Collection 介面

pub1ic interface Collection AnyType> extends Iterable<AnyType>{
    int size( ); 
    boolean isEmpty( ); 
    void c1ear( ); 
    boolean contains( AnyType x ); 
    boolean add( AnyType x); 
    boolean remove( AnyType x ); 
    java.util.Iterator<AnyType> iterator();
}

3.3.2 Iterator 介面

Iterator 介面的思路是,通過 iterator 方法,每個集合均可建立並返回給客戶一個實現 Iterator 介面的物件,並將當前位置的概念在物件內部儲存下來。

public interface Iterator<AnyType>{
    boolean hasNext( ); 
    AnyType next( ); 
    void remove( );
} 
  • 增強的 for 迴圈:for( AnyType item : coll)
  • Iterator 自帶的 remove 方法,對迭代器已看到的最後一個元素髮揮作用
    • 這樣可以首先檢查某個元素是否滿足一些性質,然後再執行操作

3.3.3 List介面、ArrayList 類和 LinkedList 類

  • 本節跟我們關係最大的集合就是表(list), 它由 java. util 包中的 List 介面指定。List介面繼承了 Collection 介面,因此它包含 Collection 介面的所有方法,外加其他一些方法。

    • public interface List AnyType> extends Collection AnyType>{
      	AnyType get( int idx ); 
          AnyType set( int idx, AnyType newVal ); 
          void add( int idx, AnyType × ); 
          void remove( int idx ); 
          ListIterator<AnyType> listIterator( int pos );
      } 
      
      • add 在位置 idx 處新增一個新元素,並將其他元素向後推移 1 個位置
      • ListInterator
  • List ADT有兩種流行的實現方式:ArrayListLinkedList

    • ArrayList 為列表 ADT 提供了一種可增長陣列的實現
      • 優點:查詢快(setget
      • 缺點:增刪慢(addremove)、搜尋慢(containsremove
      • 其他的特點:容量(ensureCapacitytrimToSize
    • LinkedList 為列表 ADT 提供了一種雙鏈表的實現
      • 優點:增刪快(addremoveaddFirstremoveFirst等)
      • 缺點:不容易做索引、搜尋慢
        • 適時地利用 Iterator 提高順序索引速度

3.3.5 關於 ListIterator 介面

  • public interface ListIterator<Any Type> extends Iterator<AnyType>{
        boolean hasPrevious( ); 
        AnyType previous( ); 
        
        void add( AnyType x ); 
        void set( AnyType newval );
    } 
    
  • 當前項是一個不存在的索引,它存在於 nextprevious 之間

  • set 對迭代器已看到的最後一個元素髮揮作用

    • 這樣可以首先檢查某個元素是否滿足一些性質,然後再執行操作

3.4 ArrayList 類的實現

  • theItems (AnyType []) new Object[ newCapacity ];
    
    • 在建立更大陣列時使用了強制型別轉換

3.5 LinkedList 類的實現

  • 加入空頭節點和空尾節點避開了很多特殊情況
  • 加入了集合被修改情況的監測 modCount

3.6 棧 ADT

3.6.1 棧模型

3.6.2 棧的實現

  • 由於棧是一個表,因此任何實現表的方法都能實現棧。

  • 因為棧操作是常數時間操作,所以,除非在非常獨特的環境下,這是不可能產生任何明顯的改進的。

  • 棧很可能是在電腦科學中在陣列之後的最基本的資料結構

    • 在某些機器上,若在帶有自增和自減定址功能的暫存器上操作,則(整數的) pushpop 都可以寫成一條機器指令。最現代化的計算機將棧操作作為它的指令系統的一部分。

3.6.3 應用

  • 平衡符號
  • 字尾表示式
    • 字尾記法(與二叉樹的後序遍歷對應)
      • \(4.99*1.06 +5.99 +6.99 *1.06\) = -> \(4.99 1.06 *5.99 +6.99 1.06*+\)
      • \(a+b*c+(d*e+f)*g\) -> \(abc * +de*f+g*+\)
    • 中綴轉化為字尾
  • 方法呼叫
    • 尾遞迴(tail recursion),在方法的最後一行的遞迴呼叫。尾遞迴總是可以轉換成迴圈。
      • 避免在程式中出現尾遞迴。
    • 遞迴總能夠被徹底去除(編譯器是在轉變成組合語言時完成遞迴去除的),但是這麼做是相當冗長乏味的。
      • 這樣做雖然提高了速度,但犧牲了清晰度

3.6 佇列 ADT

  • 迴圈佇列
  • 排隊論

3.10 演算法題例項

3.10.1 Reverse Linked List

public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next;
        curr.next = prev;
        prev = curr;
        curr = nextTemp;
    }
    return prev;
}

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode p = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return p;
}
  • 取名儘量具有指代性
    • prev curr nextTemp
  • 第一種方法十分巧妙地在普遍解法中包含了連結串列的長度為0 或 1 的特殊情況
  • head.next.next = head; 實現了單鏈表的反向操作,十分優美;
  • 單鏈表中要避免迴圈連結串列出現,故 head.next =null

第 4 章 樹

  • 對於大量的輸入資料,連結串列的線性訪問時間太慢,不宜使用。本章討論一種簡單的資料結構,其大部分操作的執行時間平均為\(O(log N)\)
  • 這種資料結構叫作二叉查詢樹(binary search tree)。二叉查詢樹是兩種庫集合類 TreesetTreeMap 實現的基礎,它們用於許多應用之中。

4.1 預備知識

  • 樹(tree)可以用幾種方式定義。定義樹的一種自然的方式是遞迴的方式。
    • 遞迴即自己呼叫自己(或者說自己重複自己、自己實現自己,如分形)
    • 遞迴的反義詞是分而治之,前者從下往上,後者從上往下

4.1.1 樹的實現

  • 對於非二叉樹,將所有的兒子都放在樹節點的連結串列中

4.1.2 樹的遍歷和應用

  • 樹應用在檔案系統中
    • 先序遍歷可以得到常見的檔案目錄
    • 後序遍歷可以得到帶有檔案大小的目錄
  • 中序遍歷可以用於在查詢二叉樹中按順序列印所有節點
  • 先序遍歷可以用於在二叉樹中用深度標記每個節點
  • 層序遍歷使用佇列,而不是棧

4.2 二叉樹

  • 二叉樹的平均深度 \(O(\sqrt{n})\)
  • 搜尋二叉樹的平均深度 \(O(log N)\)

4.2.1 實現

  • 二叉樹節點類:元素資訊與兩個子節點的引用

4.2.2 例子:表示式樹

  • 順序計算表示式——中序遍歷
  • 從字尾表示式構造表示式樹

4.3 查詢樹 ADT——二叉查詢樹

使二叉樹成為二叉查詢樹的性質是:對於樹中的每個節點X,它的左子樹中所有項的值小於X中的項,而它的右子樹中所有項的值大於X中的值。

查詢樹 ADT 的核心是比較,一個非常經典的演算法結構是:

private boolean contains(  AnyType x, BinaryNode<AnyType>){
     if(t == nu11 ) 
         return false; 
    int compareResult = x.compareTo( t.element ); 
    
    if( compareResult < 0) 
        return contains( x, t.left ); 
    else if( compareResult >0) 
        return contains( x, t.right ); 
    else 
        return true; 	//Match
}

4.3.2 findMin 方法和 findMax 方法

  • 在二叉查詢樹中,這兩個方法是簡潔且快速的
  • Java 的物件服從引用的拷貝傳遞,而不是物件內容的拷貝傳遞。

4.3.4 remove 方法

  • 若空,則返回空樹;

  • 比較,若小於,則遞迴檢視左樹,若大於,則遞迴檢視右樹;若等於,則:

    • 考察節點的子樹,若沒有子樹,則直接等於 null

    • 若有一個子樹,則等於該子樹;(1和2兩種情況可合併,因為沒有子樹 = 子樹 == null

    • 若兩個子樹,則需要考慮誰應當替代原節點的位置(“替代”意味著新節點被覆蓋,用來覆蓋的節點被刪除)。通過分析,能確定新節點應是整個子樹(以新節點為根節點)的中間值。可以通過兩種方法來尋找一個這樣的值:

      1. 左子樹的最大值
      2. 右子樹的最小值

      在這個過程中,可能出現遞迴,因為用來覆蓋的節點可能也有兩個子節點。

4.3.5 平均情況分析

  • 二叉樹中,內部路徑長(internal path length)是滿足 \(O(log\ N)\) 的。
  • 但是,刪除操作產生的影響使得不是所有的二叉樹操作都是 \(O(log \ N)\)
    • 書中的刪除操作總是從右子樹選擇節點替代原節點,使得左子樹不斷增大,右子樹不斷變小。整個二叉樹會失去平衡。
    • 直接從已排序的陣列中建立二叉樹也會出現不平衡的情況
    • 在使用懶惰刪除的情況下,二叉樹操作符合 \(O(log\ N)\)
  • 為了解決不平衡的問題,需要引入一些規則來維持平衡,有兩種基本的思路:
    1. 每次刪除時都隨機地從左子樹或右子樹刪除
    2. 每次操作後,都進行一次調整,使得後續的操作效率更高。這屬於自調整的資料結構。

4.4 AVL 樹

AVL( Adelson-Velskii 和 Landis)樹是帶有平衡條件(balance condition)的二叉查詢樹。

  • 一棵AVL樹是其每個節點的左子樹和右子樹的高度最多差1的二叉查詢樹。

4.4.1 單旋轉

4.4.2 雙旋轉

4.5 伸展樹

4.5.2 展開

  • 之字形
  • 一字形

4.5.3 總結

  • 有些操作快,但可能導致樹的形態變壞;有的操作慢,但留下一個更適合後續操作的樹。二者平衡的結構可以被證明是高效的。

  • 對伸展樹的分析很困難,因為必須要考慮樹的經常變化的結構。另一方面,伸展樹的程式設計要比AVL樹簡單得多,這是因為要考慮的情形少並且不需要保留平衡資訊。

4.8 標準庫中的集合與對映

List 容器即 ArrayListLinkedList 用於查詢效率很低。因此, Collections API提供了兩個附加容器 SetMap,它們對諸如插入刪除查詢等基本操作提供有效的實現。

4.8.1 關於 Set 介面

  • Set 介面代表不允許重複元的 Collection
  • SortedSet 介面中元素是有序的
    • 保持以有序狀態的 Set 的實現是 TreeSet
    • TreeSet 使用的比較器可以自定義

4.8.2 關於 Map 介面

  • Map 是一個介面,代表由關鍵字以及它們的值組成的一些項的集合
  • SortedMap 介面中,關鍵字保持邏輯上的有序狀態,TreeMap 是它的一種實現
  • Map 的重要基本操作包括:ContainsKey get put
  • Map 不提供迭代器,而是提供三種方法 KeySet values entrySet

4.8.3 TreeSet 類和 TreeMap 類的實現

Java 要求 TreesetTreeMap 支援基本的 addremovecontains 操作以對數最壞情形時間完成。因此,基本的實現方法就是平衡二叉查詢樹。一般說來,我們並不使用AVL樹,而是經常使用一些自頂向下的紅黑樹。

  • 實現對迭代器的支援——搜尋樹(thread tree)

4.8.4 使用多個對映的例項

  • 編寫一個程式以找出通過單個字母的替換可以變成至少15 個其他單詞的單詞

  • 方案一:暴力搜尋

  • 方案二:按長度分成多個集合再搜尋

  • 方案三:將每個單詞去掉某一位置上的字母后的結果作為關鍵字,單詞本身作為值的一個元素(值為列表)。這樣,不需要比較,直接通過新構建的 Map 就可以得到相互之間可以變換的單詞。

    這裡體現了一種利用 Map 進行內部搜尋的思路:將每個元素經過特定變化的結果作為關鍵字存入 Map,這樣,該變換隻需要在每個元素上執行 \(O(N)\) 次,而不是 \(O(N^2)\)

第 5 章 雜湊

5.1 一般想法

  • 選找一個合適的雜湊函式,在“表格”單元中均勻地分配關鍵字。除此之外,雜湊函式必須適當地處理“衝突”情況。

5.2 雜湊函式

  • 若輸入的關鍵字是整數,則一般合理的方法是直接返回 Key mod Tablesize
    • 通常,使表格大小為素數來減少衝突
  • 關鍵字更多時候是字串,這時候有多種雜湊函式可以選擇
    • 可以將字串中所有字元的 ASCII 碼值加起來,這樣得到的值較小且集中,不夠均勻與分散
    • 考察所有的字元(a-z,0-9,_)共37 個字元,計算 37 的多項式函式。由於這個結果更容易增長,所以允許溢位。

5.3 分離連結法

5.10 演算法題例項

5.10.1 Two Sum

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<> ();
        for (int i=0; i < nums.length; i++){
            int completence = target - nums[i];
            if (map.containsKey(completence)){
                return new int[] {map.get(completence), i};
            }else{
                map.put(nums[i], i);
            }
        }

        throw new IllegalArgumentException("No two sum solution!");
    }
}
  • 泛型(<>)是為了解決在資料在裝入集合時的型別都被當做Object對待,從而失去本身特有的型別,從集合裡讀取時,還要強制轉換
    • java是所謂的靜態型別語言,意思是在執行前,或者叫編譯期間,就能夠確定一個物件的型別,這樣做的好處是減少了執行時由於型別不對引發的錯誤。但是強制型別轉換是鑽了一個空子,在編譯期間不會有問題,而在執行期間,就有可能由於錯誤的強制型別轉換,導致錯誤,這個編譯器無法檢查到。有了泛型,就可以用不著強制型別轉換,在編譯期間,編譯器就能對型別進行檢查,杜絕了執行時由於強制型別轉換導致的錯誤。

第 7 章 排序

在這一章,我們討論元素陣列的排序問題。能夠在主存中完成的排序被稱為內部排序,必須在硬碟上完成的排序被稱為外部排序.

我們對內部排序的考查將指出:

  1. 存在幾種容易的演算法以 \(O(N^2)\)完成排序,如插入排序。
  2. 有一種演算法叫作希爾排序(Sellsort),它程式設計非常簡單,以$ O(N^2)$執行,並在實踐中很有效。
  3. 存在一些稍微複雜的$ O(N log N)$的排序演算法。
  4. 任何通用的排序演算法均需要 \(O(N log N)\)次比較。

7.1 預備知識

  • 基於比較的排序

7.2 插入排序

7.2.1 演算法

  • 每次都使插入的元素在一個合適的位置,使得前 $N-1 $ 個元素依然是有序的.
  • 在演算法設計時,每兩個元素在比較時不必交換. 這一點是通過使用 temp 來儲存 a[p] 的值實現的.

7.2.2 插入排序的分析

  • 由於輸入序列的有序程度深刻地影響了不同排序演算法的速度,所以研究這些演算法的平均時間花費是很有必要的.

7.3 一些簡單排序演算法的下界

  • 輸入陣列的無序程度用逆序數來衡量.

定理 7.1

  • \(N\) 個互異數的陣列的平均逆序數是 \(N(N-1)/4\).
    • 證明:列表與反序列表的逆序數和等於兩個列表的序數之和. 故,一個互異陣列的平均逆序數是其總逆序數的一半.

定理 7.2

  • 通過交換相鄰元素進行排序的任何演算法平均都需要 \(\Omega(N^2)\) 時間。

結論

  • 這個下界告訴我們,為了使一個排序演算法以亞二次(subquadratic)時間執行,必須執行一些比較,特別是要對相距較遠的元素進行交換。一個排序演算法通過刪除逆序得以向前進行,而為了有效地進行,它必須使每次交換刪除不止一個逆序

7.4 希爾排序

希爾排序 (Sellsort) 通過比較相距一定間隔的元素來工作;各趟比較所用的距離隨著演算法的進行而減小,直到只比較相鄰元素的最後一趟排序為止. 由於這個原因,希爾排序有時也叫作縮減增量排序(diminishing increment sort).

  • 希爾排序使用一個序列 \(h_1,h_2, ...,h_t,\)叫作增量序列(increment sequence)

  • 通過仔細觀察可以發現,一趟 \(h_k\) 排序的作用就是對 \(h_k\) 個獨立的子陣列執行一次插入排序。當我們分析希爾排序的執行時間,這個觀察結果是很重要的.

  • 使用希爾增量的希爾排序例程(可能有更好的增量)

    /** 
     * Shellsort, using Shell's (poor) increments.
     * @param a an array of Comparable items.
     */
    public static <AnyType extends Comparable<? super AnyType>>{
        void shell sort( AnyType []a){
            int j; 
    
            for( int gap = a.length/ 2; gap> 0; gap /= 2){
                for( int i = gap; i < a.length; i++){
                    AnyType tmp = a[ i ];
                    for( j =i; j >= gap && tmp.compareTo( a[ j - gap ] )<0; j-- gap ) 
                        a[j] =a[ j - gap ]; 
                    a[ j ]= tmp;
                }  
            } 
        }
    }
    
7.4.1 希爾排序的最壞情形分析

希爾增量相對的自由選擇使得希爾排序的平均情形難以分析.

定理7.3

使用希爾增量時希爾排序的最壞情形執行時間為 \(O(N)\).

定理7.4

使用 Hibbard 增量的希爾排序的最壞情形執行時間為 \(O(N^2)\).

結論

在希爾排序中,一個經典的序列是

\[{1,5,19,41,109} \]

7.5 堆排序

優先佇列可以用於以 \(O(N log N)\) 時間的排序。基於該思想的演算法叫作堆排序(heapsort).

TODO:學習“堆”

7.6 歸併排序

歸併排序(mergesort)以 \(O(NlogN)\) 最壞情形時間執行,而所使用的比較次數幾乎是最優的。它是遞迴演算法一個好的例項。

  • 這個演算法中基本的操作是合併兩個已排序的表。
    • 例如,欲將8元素陣列24, 13, 26,1,2, 27, 38, 15排序,遞迴地將前4個數據和後4個數據分別排序,得到1,13, 24, 26,2, 15,27, 38。然後,像上面那樣將這兩部分合並,得到最後的表1,2, 13,15, 24, 26, 27, 38。
  • 該演算法是經典的分治(divide-and- conquer)策略,它將問題分(divide)成一些小的問題然後遞迴求解,而治(conquer)的階段則將分的階段解得的各答案修補在一起. 分而治之是遞迴非常有效的用法.

7.6.1 歸併排序的演算法

  • 使用一個建立在遞迴演算法之外的陣列來儲存臨時元素,這樣節省了記憶體空間.

7.6.2 歸併演算法的分析

  • 執行時間的遞迴關係

    \[T(1) =1 \]

    \[T(N) = 2T(N/2)+N \]

    通過將遞推方程全部相加,得到

    \[\frac{T(N)}{N}=\frac{T(1)}{1}+log \ N \]

    得出結論

    \[T(N)=N\ log\ N+N=O(N\ log\ N) \]

  • 合併排序有一個明顯的問題,即合併兩個已排序的表用到線性附加記憶體.

  • 與其他的O(N log N)排序演算法比較,歸併排序的執行時間嚴重依賴於比較元素和在陣列(以及臨時陣列)中移動元素的相對開銷。這些開銷是與語言相關的。

    • 在 Java 中,當執行一次泛型排序(使用 Comparator)的開銷較大,但得益於引用傳遞,其元素移動的效率較高. 恰好歸併排序是所有流行的排序演算法中比較次數最少的,所以它是Java的通用排序演算法中的上好選擇.
      • 實際上,歸併排序正是 Java 泛型排序所使用的演算法.
    • 在 C++ 中,情況正好相反. 所以 C++ 使用了另一種移動較少,而比較更多的演算法,即快速排序.

7.7 快速排序

顧名思義,快速排序(quicksort)是實踐中的一種快速的排序演算法,在C++或對 Java 基本型別 ** 的排序中特別有用。它的平均執行時間是 \(O(N log N)\)。該演算法之所以特別快,主要是由於非常精練和高度優化的內部迴圈**。它的最壞情形效能為 \(O(N)\),但經過稍許努力可使這種情形極難出現。

  • 通過將快速排序和堆排序結合,由於堆排序的 \(O(N log N)\)最壞情形執行時間,我們可以對幾乎所有的輸入都能達到快速排序的快速執行時間.

7.7.1 選取樞紐元

  • 以第一個、或最後一個元素為樞紐元會導致不平衡
  • 使用隨機數生成器挑選樞紐元,開銷過大
  • 三數中值分割法,平衡了前兩種策略

7.7.2 分割策略

  1. 樞紐元與最末尾的元素交換
  2. 使用雙指標分別從剩下元素的頭尾處向中間移動
  3. 頭指標只可跨過小於樞紐元的元素,否則停下;尾指標只可跨過大於樞紐元的指標,否則停下
  4. 當兩個指標都停下時,交換彼此的元素

除此之外,還需要考慮指標對應元素等於樞紐元的情況:

  • 在陣列全是重複元的特殊例子中,若都不停止,則 ij 會一直執行到陣列的頭尾,是低效的,且不平衡的;
  • 若只一個停止,那麼同樣會得到兩個不平衡的陣列
  • 若都停止,會發生多次無謂的交換,但能得到平衡的兩個子陣列,從時間上考慮這種方案的時間花費最少.

7.7.3 小陣列

  • 對於很小的陣列(\(N \leq20\)),快速排序不如插入排序.

7.7.4 實際的快速排序

/** 
 * Internal quicksort method that makes recursive calls.
 * Uses median-of-three partitioning and a cutoff of 10.
 * @param a an array of Comparable items.
 * @param left the 1eft-most index of the subarray.
 * @param right the right-most index of the subarray.
 */
private static <AnyType extends Comparable<? super AnyType>>{
	void quicksort( AnyType [ ] a, int left, int right ){
        if( left + CUT0FF <= right ){
            AnyType pivot = median3( a, left, right );
            // Begin partitioning 
            int i = left,j = right - 1;
            for(;;){
                while( a[ ++i ].compareTo( pivot )<0) { }
                while( a[ --j ].compareTo( pivot )>0) { }
                if( i < j ) 
                    swapReferences( a, i, j );
                else 
                    break;
            } 
            swapReferences( a, i, right -1 ); // Restore pivot quicksort( a, left,i-1);
            
            quciksort( a, left, i-1 );	//sort small elements
            quicksort( a, i, right);	//sort large elements
        }
        else // Do an insertion sort on the subarray insertionSort( a, 1eft, right );
            insertionSort( a, left, right );
    }	
}

7.7.5 快速排序的分析

基本的快速排序關係

\[T(N)=T(i)+T(N-i-1)+cN \]

其中,\(i=|S_i|\)\(S_i\) 中的元素個數.

最壞的情況分析
  • 樞紐元總是總是最小元素或最大元素

\[T(N)=T(1)+c\sum^N_{i=2}i=O(N^2) \]

最好的情況分析
  • 樞紐元總是中位數,使得陣列被分為兩個同樣大小的陣列

    \[T(N)=c N \log N+N=O(N \log N) \]

  • 這和歸併排序的分析結果是類似的.

平均情況的分析

\[T(N)=O(N \log\ N) \]

7.7.6 快速選擇

  • 受到快速排序演算法的啟發,可以設計類似的快速選擇演算法.

7.8 排序演算法的一般下界

7.9 選擇問題的決策樹下界

7.10 對手下界

7.11 線性時間的排序: 桶排序和基數排序

桶排序

  • 輸入資料 \(A_1, A_2,\cdots,A_N\) 必須僅由小於 \(M\) 的正整陣列成
  • 使用一個大小為 \(M\) 的稱為 count 的陣列,初始化為全 \(0\)
  • 於是,count\(M\) 個單元(或稱為桶),初始為空。當讀入 \(A_i\) 時, count [A_i] 增1。在所有的輸入資料被讀入後,掃描陣列 count,打印出排序後的表。該演算法用時 \(O(M+N)\).
  • 演算法在單位時間內實質上執行了一個 M-路比較

基數排序

  • 計數基數排序

7.12 外部排序

7.12.1 為什麼需要一些新的演算法

  • 當資料儲存在外部時,無法如主存一樣進行直接定址.
    • 以磁帶驅動器為例,如果只有一個磁碟驅動器可用,那麼任何演算法都需要 \(O(N^2)\) 次磁帶訪問.

7.12.3 簡單演算法

  • 基本的外部排序演算法使用歸併排序中的合併演算法。
  • 設資料最初在 \(T_{a1}\) 上,並設記憶體可以一次容納(和排序) \(M\) 個記錄。一種自然的第一步做法是從輸入磁帶一次讀入 \(M\) 個記錄,在內部將這些記錄排序,然後再把這些排過序的記錄交替地寫到 \(T_{b1}\)\(T_{b2}\) 上。我們將把每組排過序的記錄叫作一個順串(run)。做完這些之後,倒回所有的磁帶。
    • 該演算法將需要 \(\lceil log_2(N/M) \rceil\) 趟工作,外加一趟初始的順串構造。

7.12.4 多路合併

如果我們有額外的磁帶,可以減少將輸入資料排序所需要的趟數,通過將基本的“2-路合併”擴充為“k-路合併”就能做到這一點。

  • 使用k-路合併所需要的趟數為\(\lceil log_k(N/M) \rceil\)

7.12.5 多相合並

使用更少的外部儲存裝置來完成 k-路合併.

7.12.6 替換選擇

在記憶體中構造優先數列,形成類似流水線的操作,而不是批操作.

我們已經看到,替換選擇可能做得並不比標準演算法更好。然而,輸入資料常常從已排序或幾乎已排序開始,此時替換選擇僅僅產生少數非常長的順串,而這種型別的輸入通常要進行外部排序,這就使得替換選擇具有特別的價值。

7.13 小結

  • 插入排序適合小陣列
  • 希爾排序適合中等規模,實際中常用的增量序列是 \({1,5,19,41,109}\)
  • 歸併排序的最壞表現為 \(O(N log N)\) ,但需要額外的空間.
    • 歸併排序的比較次數最少
  • 選擇排序並不保證最壞表現為 \(O(N log N)\),且程式設計較麻煩. 但和堆排序組合在一起可以保證.
  • 基數排序區別於一般的基於比較的演算法,它實際進行了在一個常數時間內進行了一次 M-路比較. 基數排序可以將字串線上性時間內排序.

第 10 章 演算法設計技巧

在這一章,我們將集中討論用於求解問題的五種通常型別的演算法。對於許多問題,很可能這些方法中至少有一種方法是可以解決問題的。

10.5 回溯演算法

在許多情況下,回溯(backtracking)演算法相當於窮舉搜尋的巧妙實現,但效能一般不理想(不過相比窮舉,有顯著的效能提升)。

  • 在一步內刪除一大組可能性的做法叫作剪枝(pruning).