读书笔记:《数据结构与算法分析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