第 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有兩種流行的實現方式:
ArrayList
和LinkedList
ArrayList
為列表 ADT 提供了一種可增長數組的實現- 優點:查找快(
set
、get
) - 缺點:增刪慢(
add
、remove
)、搜索慢(contains
、remove
) - 其他的特點:容量(
ensureCapacity
、trimToSize
)
- 優點:查找快(
LinkedList
為列表 ADT 提供了一種雙鏈表的實現- 優點:增刪快(
add
、remove
、addFirst
、removeFirst
等) - 缺點:不容易做索引、搜索慢
- 適時地利用
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 ); }
-
當前項是一個不存在的索引,它存在於
next
和previous
之間 -
set
對迭代器已看到的最后一個元素發揮作用- 這樣可以首先檢查某個元素是否滿足一些性質,然后再執行操作
3.4 ArrayList 類的實現
-
theItems (AnyType []) new Object[ newCapacity ];
- 在創建更大數組時使用了強制類型轉換
3.5 LinkedList 類的實現
- 加入空頭節點和空尾節點避開了很多特殊情況
- 加入了集合被修改情況的監測
modCount
3.6 棧 ADT
3.6.1 棧模型
3.6.2 棧的實現
-
由於棧是一個表,因此任何實現表的方法都能實現棧。
-
因為棧操作是常數時間操作,所以,除非在非常獨特的環境下,這是不可能產生任何明顯的改進的。
-
棧很可能是在計算機科學中在數組之后的最基本的數據結構
- 在某些機器上,若在帶有自增和自減尋址功能的寄存器上操作,則(整數的)
push
和pop
都可以寫成一條機器指令。最現代化的計算機將棧操作作為它的指令系統的一部分。
- 在某些機器上,若在帶有自增和自減尋址功能的寄存器上操作,則(整數的)
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),在方法的最后一行的遞歸調用。尾遞歸總是可以轉換成循環。
- 避免在程序中出現尾遞歸。
- 遞歸總能夠被徹底去除(編譯器是在轉變成匯編語言時完成遞歸去除的),但是這么做是相當冗長乏味的。
- 這樣做雖然提高了速度,但犧牲了清晰度
- 尾遞歸(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)。二叉查找樹是兩種庫集合類
Treeset
和TreeMap
實現的基礎,它們用於許多應用之中。
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
) -
若兩個子樹,則需要考慮誰應當替代原節點的位置(“替代”意味着新節點被覆蓋,用來覆蓋的節點被刪除)。通過分析,能確定新節點應是整個子樹(以新節點為根節點)的中間值。可以通過兩種方法來尋找一個這樣的值:
- 左子樹的最大值
- 右子樹的最小值
在這個過程中,可能出現遞歸,因為用來覆蓋的節點可能也有兩個子節點。
-
4.3.5 平均情況分析
- 二叉樹中,內部路徑長(internal path length)是滿足 \(O(log\ N)\) 的。
- 但是,刪除操作產生的影響使得不是所有的二叉樹操作都是 \(O(log \ N)\)
- 書中的刪除操作總是從右子樹選擇節點替代原節點,使得左子樹不斷增大,右子樹不斷變小。整個二叉樹會失去平衡。
- 直接從已排序的數組中建立二叉樹也會出現不平衡的情況
- 在使用懶惰刪除的情況下,二叉樹操作符合 \(O(log\ N)\)
- 為了解決不平衡的問題,需要引入一些規則來維持平衡,有兩種基本的思路:
- 每次刪除時都隨機地從左子樹或右子樹刪除
- 每次操作后,都進行一次調整,使得后續的操作效率更高。這屬於自調整的數據結構。
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
容器即 ArrayList
和 LinkedList
用於查找效率很低。因此, Collections API
提供了兩個附加容器 Set
和 Map
,它們對諸如插入、刪除和查找等基本操作提供有效的實現。
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 要求 Treeset
和 TreeMap
支持基本的 add
、 remove
和 contains
操作以對數最壞情形時間完成。因此,基本的實現方法就是平衡二叉查找樹。一般說來,我們並不使用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 章 排序
在這一章,我們討論元素數組的排序問題。能夠在主存中完成的排序被稱為內部排序,必須在硬盤上完成的排序被稱為外部排序.
我們對內部排序的考查將指出:
- 存在幾種容易的算法以 \(O(N^2)\)完成排序,如插入排序。
- 有一種算法叫作希爾排序(Sellsort),它編程非常簡單,以$ O(N^2)$運行,並在實踐中很有效。
- 存在一些稍微復雜的$ O(N log N)$的排序算法。
- 任何通用的排序算法均需要 \(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)\).
結論
在希爾排序中,一個經典的序列是
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++ 使用了另一種移動較少,而比較更多的算法,即快速排序.
- 在 Java 中,當執行一次泛型排序(使用 Comparator)的開銷較大,但得益於引用傳遞,其元素移動的效率較高. 恰好歸並排序是所有流行的排序算法中比較次數最少的,所以它是Java的通用排序算法中的上好選擇.
7.7 快速排序
顧名思義,快速排序(quicksort)是實踐中的一種快速的排序算法,在C++或對 Java 基本類型 ** 的排序中特別有用。它的平均運行時間是 \(O(N log N)\)。該算法之所以特別快,主要是由於非常精練和高度優化的內部循環**。它的最壞情形性能為 \(O(N)\),但經過稍許努力可使這種情形極難出現。
- 通過將快速排序和堆排序結合,由於堆排序的 \(O(N log N)\)最壞情形運行時間,我們可以對幾乎所有的輸入都能達到快速排序的快速運行時間.
7.7.1 選取樞紐元
- 以第一個、或最后一個元素為樞紐元會導致不平衡
- 使用隨機數生成器挑選樞紐元,開銷過大
- 三數中值分割法,平衡了前兩種策略
7.7.2 分割策略
- 樞紐元與最末尾的元素交換
- 使用雙指針分別從剩下元素的頭尾處向中間移動
- 頭指針只可跨過小於樞紐元的元素,否則停下;尾指針只可跨過大於樞紐元的指針,否則停下
- 當兩個指針都停下時,交換彼此的元素
除此之外,還需要考慮指針對應元素等於樞紐元的情況:
- 在數組全是重復元的特殊例子中,若都不停止,則
i
和j
會一直運行到數組的頭尾,是低效的,且不平衡的; - 若只一個停止,那么同樣會得到兩個不平衡的數組
- 若都停止,會發生多次無謂的交換,但能得到平衡的兩個子數組,從時間上考慮這種方案的時間花費最少.
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 快速排序的分析
基本的快速排序關系
其中,\(i=|S_i|\) 是 \(S_i\) 中的元素個數.
最壞的情況分析
- 樞紐元總是總是最小元素或最大元素
最好的情況分析
-
樞紐元總是中位數,使得數組被分為兩個同樣大小的數組
\[T(N)=c N \log N+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).