第 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).