讀書筆記:《數據結構與算法分析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).


免責聲明!

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



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