【数据结构】搜索树、词典、优先级队列、字符串、排序算法

数据结构

本文参考邓俊辉老师的教材《数据结构(C++语言版)》及mooc课程,总结7-12章

邓老师上课的所有资源均已公开:https://dsa.cs.tsinghua.edu.cn/~deng/ds/dsacpp/index.htm

第七章 搜索树

image-20220704085308801

查找

  • 不同词条之间,依照各自的关键码(key)彼此区分,查找过程与数据对象的物理位置或逻辑次序均无关,称作循关键码访问(call-by-key);
  • 数据对象,表示为词条(entry),拥有成员变量key和value;

二叉搜索树(BST)

  • 顺序性:任一节点r 的左(右)子树中 ,所有节点( 若存在)均不大于(不小于)r;

    image-20220704085814443
  • 中序遍历序列:任何一棵二叉树是二叉搜索树,当且仅当其中序遍历序列单调非降;

  • BST模板类(派生自BinTree)

    image-20220704090049766

  • 查找算法:减而治之,逐步深入(与有序向量的二分查找过程等效);

    image-20220704090233984
    • 接口实现(递归)

      image-20220704090348982

      image-20220704090406001

    • 若查找失败,将此空节点转换为一个数值为e的哨兵节点;

    • 效率:所需时间线性正比于查找路径的长度(最好O(1),最差O(n));

  • 插入算法

    image-20220704090800125

    image-20220704090827281

    • 时间复杂度取决于新节点的深度;
  • 删除算法

    image-20220704091014599

    • 删除节点为单分支则与其孩子交换、双分支则先与其后继交换;

      image-20220704091118013

      image-20220704092217680

    • 效率:不超过全树的高度。

平衡二叉搜索树(BBST)

  • 二叉搜索树的性能主要取决于高度,故在节点数目固定的前提下,应尽可能地降低高度;

    • 理想平衡:树高恰好为log2n,称作理想平衡树;
    • 适度平衡:将树高限制为“渐进地不超过O(logn)”,如AVL树,伸展树、红黑树、kd-树等;
  • 等价二叉搜索树:中序遍历序列相同(连接关系不尽相同,但上下可变,左右不乱);

  • 旋转调整(等价变换)

    • zig

      image-20220704093010404

    • zag

      image-20220704093039248

AVL树

  • 属于BBST,在渐进意义下,AVL树可始终将其高度控制在O(logn)以内,从而保证每次查找、插入或删除操作,均可在**O(logn)**的时间内完成;

  • 定义:

    • 平衡因子:左、右子树的高度差;
    • 对AVL树,其中各节点平衡因子的绝对值均不超过1;
    • 失衡与重平衡:因节点x的插入或删除而暂时失衡的节点,构成失衡节点集UT(x),若x为被摘除的节点,则UT(x)仅含单个节点;但若x为被引入的节点,则UT(x)可能包含多个节点
  • 节点插入及重平衡

    • 失衡节点集:最深为g(x),p(x)为g(x)的孩子,v为p(x)的孩子;

    • 重平衡:

      • p,g,v同向:单旋(zig或zag)

        image-20220704094231100

      • p,g,v不同向:双旋(zigzag或zagzig)

        image-20220704094244773

    • 局部调整之后,g(x)能够重获平衡,而且局部子树的高度也必将复原,即在AVL树中插入新节点后,仅需不超过两次旋转,即可使整树恢复平衡。

    • 效率:O(logn);

  • 节点删除及重平衡

    • 失衡节点集UT(x)始终至多只含一个节点,g,p,v与上文同理;

    • 重平衡

      • 单旋

        image-20220704094721848

      • 双旋

        image-20220704094934310

      • 失衡传播:

        • 在删除节点之后,尽管也可通过单旋或双旋调整使局部子树恢复平衡,但就全局而言,依然可能再次失衡(因为重平衡后局部子树的高度可能再次降低);
        • 失衡传播的方向必然自底而上,可逐层遍历失衡祖先并进行处理;
    • 效率:O(logn)。

  • 统一重平衡算法(3 + 4重构)

    • 找到最低的失衡节点g(x),以g(x)、p和v为界,分解为四棵子树(T0-T3);

    • 按照中序遍历次序,重新排列g(x)、p和v,并将其命名为a、b和c,则这一局部的中序遍历序列应为:{ T0 , a, T1 , b, T2 , c, T3 }

      image-20220704095656230

      image-20220704095708767

    • 利用以上connect34()算法,即可视不同情况,按如下具体方法完成重平衡

      image-20220704100613904

高级搜索树

伸展树

  • 属于BBST,无需时刻都严格地保持全树的平衡,不需要记录平衡因子或高度之类的额外信息;

  • 数据局部性

    image-20220704140914030

    • 因此需要将刚访问过的节点转移至树根附近
  • 逐层伸展(单次访问的分摊时间复杂度可能高达Ω(n)

    image-20220704141101914

  • 双层伸展(单次访问的分摊时间复杂度均在O(logn))

    • zig-zig/zag-zag

      image-20220704141430883

    • zig-zag/zag-zig

      image-20220704141448755

    • zig/zag

      image-20220704141510000
  • 伸展树(Splay)的实现

    image-20220704141619004

    • 查找算法:

      • 可调用二叉搜索树的标准插入算法BST::insert();

      • Splay::search() + Splay::insert():搜索–伸展 + 分裂–插入

        image-20220704141912962

    • 删除算法

      • 可调用二叉搜索树标准的节点删除算法BST::remove();

      • Splay::search() + Splay::remove():搜索–伸展 + 释放–伸展–合并

        image-20220704142838280

B树

  • 多路平衡查找

    • 分级存储:将内存作为外存的高速缓存,存放最常用数据项的
      复本,将内存的“高速度”与外存的“大容量”结合起来,可以忽略对内存的访问,转而更多地关注对外存的访问次数

    • 多路搜索树:将通常的二叉搜索树,改造为多路搜索树(在中序遍历的意义下,这也是一种等价变换);

      image-20220704162850807

    • 原理:

      • 可以两层为间隔,将各节点与其左、右孩子合并为“大节点”;
      • 搜索每下降一层,都以“大节点”为单位从外存读取一组(而不再是单个)关键码。这组关键码在逻辑上与物理上都彼此相邻,故可以批量方式从外存一次性读出,且所需时间与读取单个关键码几乎一样。
  • 多路平衡搜索树

    • m路平衡搜索树,即m阶B-树(B-tree)

      image-20220704163211830

      • 分支:[m/2取上整,m],故m阶B-树也称作(m/2取上整, m)-树;
      • 内部节点内关键码数:[(m/2取上整)-1, m-1];
      • 外部节点:叶节点的数值为空的孩子,所有外部节点深度均相等;
    • 表示

      image-20220704163758100

  • ADT接口及其实现

    • 节点

      image-20220704164252888

    • B-树模板类

      image-20220704164328173

  • 关键码查找

    • 原理:将大数据集组织为B-树并存放于外存,根节点会常驻于内存,任何时刻通常只有另一节点(称作当前节点)留驻于内存;

      image-20220704164441295

    • 实现

      image-20220704164754910

    • 性能

      • B-树的每一次查找过程中,在每一高度上至多访问一个节点即对于高度为h的B-树,外存访问不超过O(h - 1);

      • 树高h(m为最大分支树,N为关键码数)

        image-20220704164931036

        最大树高:O(log_m_N);

        最小树高:Θ(log_m_N);

        渐进意义:Ω(log_m_N);

      • 复杂度:O(log_m_N),没有渐进意义上的改进,但极其耗时的I/O操作的次数,已大致缩减为原先的1/log_2_m;

  • 关键码插入

    image-20220704165817518

    • 上溢与分裂

      • 原因:_hot所指的节点中增加了一个关键码后,总数超过m-1个
      • 分裂:取发生上溢的节点的居中节点,将该节点分前、后两个子节点,再另居中节点上升一层,归入其父节点中的适当位置,并以两个子节点作为其左右孩子;

      image-20220704170136256

      • 实例(3阶B-树,关键码数目不超过2)

        image-20220704191319801

        image-20220704191339723

      • 上溢的传递(最多到根节点);

      • 复杂度:O(log_m_N);

  • 关键码删除

    image-20220704190340666

    • 下溢与合并

      • 原因:关键码总数少于(m/2取上整)-1

      • 合并:左顾右盼,并进行旋转

        1. V的左兄弟L存在,且至少包含m/2取上整个关键码

          image-20220704190722403

        2. V的右兄弟R存在,且至少包含m/2取上整个关键码

          image-20220704190757760

        3. V 的左、右兄弟L和R 或者不存在,或者其包含的关键码均不足m/2取上整个

          image-20220704190840665

      • 实例(3阶B-树,关键码数目不少于1)

        image-20220704191049995

        image-20220704191202626

      • 下溢的传递,最多到根节点(全树高度也随之下降一层);

      • 复杂度:O(log_m_N);

红黑树

  • 概述

    • AVL树:插入后可能需要O(1)旋转后复原,删除后可能需要O(logn)旋转后才能复原,导致全树整体拓扑结构的大幅度变化;
    • 红黑树:每次插入或删除操作之后的重平衡过程中,全树拓扑结构的更新仅涉及常数个节点,最坏情况下需对Ω(logn)个节点重染色,但就分摊意义而言仅为O(1)个;
    • 适度平衡:任一节点左、右子树的高度,相差不得超过两倍;
  • 定义与性质

    • 由红、黑两色节点组成的二叉搜索树若满足以下条件,即为红黑树

      image-20220704192550924

      • 从根节点通往任一节点的沿途,黑节点都不少于红节点;
      • 黑深度:从根节点通往任一节点的沿途,除去根节点本身,沿途所经黑节点的总数;
      • 黑高度:从任一节点通往其任一后代外部节点的沿途,除去(黑色)外部节点,沿途所经黑节点的总数称作该节点的黑高度,黑节点的总数亦必相等
    • (2,4)-树

      • 经适当转换之后,红黑树与(2,4)-树相互等价

      • 转换方式:每遇到一个红节点,都将对应的子树整体提升一层,从而与其父节点(必黑)水平对齐,二者之间的联边则相应地调整为横向;

      • 将原红黑树的节点视作关键码,沿水平方向相邻的每一组(父子至多三个)节点即恰好构成4阶B-树的一个节点;

      • 四种情况的转化

        image-20220704193118069

        (2,4)-树中的每个节点应包含且仅包含一个黑关键码,同时红关键码不得超过两个。

    • 平衡性

      • 包含n个内部节点的红黑树T的高度h不超过O(logn),保证了适度平衡;
      image-20220704193437593
  • 红黑树接口定义

    image-20220704193531538

  • 节点插入算法

    • 创建节点x,染成红色(为满足红黑树条件1、2、4);

    • 若因新节点的引入导致父子同为红色,进行双红修正

      • RR-1:x的叔父u为黑,此时,x的兄弟、两个孩子的黑高度与u相等:此时令黑色关键码与紧邻的红色关键码互换颜色,等效于按中序遍历次序,对节点x、p和g及其四棵子树,做一次局部“3 + 4”重构,局部子树的黑高度将复原

        image-20220704194306342

      • RR-2:x的叔父u为红,此时需将红节点p和u转为黑色,黑节点g转为红色,x保持红色,等效于B树上溢的修复,但可能导致上溢的传播(将g视为新节点,继续按双红进行修复);

        image-20220704194731778

    • 双红修正复杂度:至多做O(logn)次节点染色(均只需常数时间),1次“3 + 4”重构(一但旋转,修复过程必然完成),就全树拓扑结构而言,每次插入后仅涉及常数次调整

  • 节点删除算法(将红黑树转化为B树进行理解)不太懂

    • 删除节点x,p = _hot为其父亲,r为其接替者(红黑树条件3、4可能不满足)

    • 若x与r一黑一红,则可通过重染色的方式恢复局部子树黑高度;

    • 若x与r均为黑色,则需进行双黑修正,其中原黑节点x的兄弟必然非空,将其记作s;

      • BB-1:s为黑,且至少有一个红孩子(对应于B树,即为兄弟节点足够富裕),此时将t和p染成黑色,s继承p此前的颜色,等效于对节点t、s和p实施“3 + 4”重构;

        image-20220704200423121

      • BB-2-R:s为黑,且两个孩子均为黑,p为红(对应于B树中,下溢节点与兄弟合并),此时将s和p颜色互换

        image-20220704200755786
      • BB-2-B:s为黑,且两个孩子为黑,p为黑,此时将节点s由黑转红

        image-20220704200948438

      • BB-3:s为红,其孩子均为黑,此时将s与p互换颜色,对应于以节点p为轴做一次旋转

        image-20220704201242183

    • 双黑修正复杂度:至多做O(logn)次节点染色(均只需常数时间),1次“3 + 4”重构(一但旋转,修复过程必然完成),1次单旋,就全树拓扑结构而言,每次删除后仅涉及常数次调整

第9章 词典

数据结构访问方式

  • 词典(dictionary)结构,是由一组数据构成的集合,其中各元素都是由关键码和数据项合成的词条(entry);
  • 以散列表为代表的符号表结构,将依据数据项的数值直接做逻辑查找和物理定位,关键码(key)与数值(value)的地位等同,即循值访问(call-by-value)

词典ADT

image-20220705085321735

image-20220705085418550

跳转表

  • 跳转表是一种高效的词典结构,定义与实现基于有序列表,其查询和维护操作在平均的意义下均仅需**O(logn)**时间;

  • Skiplist模板类

    image-20220705090152132

  • 总体逻辑结构

    • 内部由沿横向分层、沿纵向相互耦合的多个列表{ S_0 ,S_1 , S_2 , …, S_h }组成,h称作跳转表的高度;

      image-20220705090242120

  • 四联表:跳转表内各节点沿水平和垂直方向都可定义前驱和后继,支持这种联接方式的表称作四联表(quadlist),它是Skiplist模板类的底层实现方式

  • 查找、插入、删除复杂度均为O(logn),具体实现参考P252-P259。

散列表

  • 完美散列

    • 散列表(hashtable):散列方法的底层基础,逻辑上由一系列可存放词条的单元组成,称作桶(bucket)或桶单元,各桶单元也应按其逻辑次序在物理上连续排列,这种线性的底层结构用向量来实现
    • 散列函数(hash function):从关键码空间到桶数组地址空间的函数(然后再找到词条),即hash() : key -> hash(key)
  • 装填因子:非空桶的数目与桶单元总数的比值;

  • 散列函数:

    • 关键码均为[0, R)范围内的整数、词条数记作N、散列表长度记作M,则有:R >> M > N;

    • 散列函数hash()的作用可理解为,将关键码空间[0, R)压缩为散列地址空间[0, M)

      image-20220705091513971
    • 设计原则:确定性(同一关键码映射到同一散列地址)快速、最好满射、最好均匀;

    • 散列冲突(collision):关键码不同的词条被映射到同一散列地址;

  • 常见散列函数

    • 除余法:将散列表长度M取作为素数(降低聚集发生的概率),并将关键码key映射至key关于M整除的余数,即hash(key) = key mod M

      image-20220705092138893

    • MAD法(multiply-add-divide method):消除关键码空间到散列地址空间映射的连续性,将关键码key映射为(a × key + b ) mod M,其中M仍为素数,a > 0,b > 0,且a mod M ≠ 0;

      image-20220705092435705

    • 数字分析法:从关键码key特定进制的展开中抽取出特定的若干位,构成一个整型地址;

    • 折叠法:将关键码的十进制或二进制展开分割成等宽的若干段,取其总和作为散列地址;

    • 位异或法:将关键码的二进制展开分割成等宽的若干段,经异或运算得到散列地址;

    • (伪)随机数法。

  • 散列表

    • Hashtable 模板类

      image-20220705092804640

  • 冲突及其排解

    • 多槽位法:绝大多数的槽位通常都处于空闲状态,装填因子降低至原先的1/k,且冲突可能于某个特定桶单元,导致溢出;

      image-20220705105933590

    • 独立链法:需申请额外空间,且查找过程中一旦发生冲突,则需要遍历整个列表,导致查找成本的增加;

      image-20220705110102329

    • 公共溢出区法

      image-20220705110148001

  • 闭散列策略(开放定址):散列地址空间对所有词条开放,每个桶单元都有可能存放任一词条;

    • 线性试探法

      image-20220705110349060

      • 第i次试探的桶单元应为:ht[ (hash(key) + i) mod M ], i = 1, 2, 3, …
    • 查找链:采用开放地址策略时,散列表中每一组相互冲突的词条都将被视作一个有序序列,对其中任何一员的查找都需借助这一序列,查找链平均长度为n/2取上整;

    • 懒惰删除:

      • 查找链中任何一环的缺失,都会导致后续词条因无法抵达而丢失,若采用开放定址策略,则在执行删除操作时,需同时做特别的调整;
      • 为每个桶另设一个标志位,指示该桶尽管目前为空,但此前确曾存放过词条,如此,该桶虽不存放任何实质的词条,却依然是查找链上的一环;
    • 两类查找

      • probe4Hit():在删除等操作之前对某一目标词条的查找,对成功的判定条件基本不变,对失败的判定条件需兼顾懒惰删除标志(即带有懒惰删除标志时将沿着查找链继续试探);
      • probe4Free():在插入等操作之前沿查找链寻找空桶,无论当前桶为空,还是带有懒惰删除标记,均可报告“查找成功”;
  • 查找与删除

    • probe4Hit(k) + remove();
  • 插入

    image-20220705111338410

    • 装填因子:建议保持λ< 0.5;

    • 重散列(rehashing )

      image-20220705111503705

  • 更多闭散列策略

    • 聚集现象:线性试探法各查找链均由物理地址连续的桶单元组成,会加剧关键码的聚集趋势;

    • 平方试探法

      image-20220705111629121

      • 试探位置的间距将以线性(而不再是常数1的)速度增长,一旦发生冲突,即可“聪明地”尽快“跳离”关键码聚集的区段;
      • 只要散列表长度M为素数且装填因子λ ≤ 50%,则平方试探迟早必将终止于某个空桶;
    • (伪)随机试探法;

    • 再散列法:选取二级散列函数hash_2(),发生冲突时以hash_2()为偏移增量继续尝试,直到发现一个空桶;

      image-20220705112041768

  • 散列码转换

    • 关键码不仅限定为整数,首先利用散列码转换函数hashCode()将关键码key转换为一个整数(称作散列码(hash code));然后再利用散列函数将散列码映射为散列地址;

      image-20220705112242853
    • byte、short、int和char等强制转换为整数,作为散列码;

    • long long和double之类长度超过32位的基本类型,将高32位和低32位分别看作两个32位整数,将二者之和作为散列码;

    • 多项式散列码:将字符串中的各个字符对应到整数,将多项式的和作为散列码:

      image-20220705112427984

第10章 优先级队列

数据结构访问方式

  • 优先级队列:按照事先约定的优先级,可以始终高效查找并访问优先级最高数据项的数据结构;
  • 仿照词典结构,将优先级队列中的数据项称作词条(entry),与特定优先级相对应的数据属性,也称作关键码(key);
  • 关键码之间必须可以比较大小。

优先级队列ADT

  • 操作接口

    image-20220705142131302

  • 接口定义

    image-20220705142159944

  • 优先级队列的实现方法

    • 效率过高:BBST;
    • 效率低:list(O(1)但getmax()需要O(n))、vector(查找及顺次后移需要O(n));
    • 效率合理:堆(heap):仅维持偏序关系(即足以确定极值元素)、形(vector)+神(tree)
  • 完全二叉堆

    • 结构性:逻辑结构须等同于完全二叉树;

    • 堆序性:堆顶以外的每个节点优先级都不高(大)于其父节点;

    • 大顶堆与小顶堆;

    • 高度:n个词条组成的堆的高度h = log2n = O(logn),insert()和delMax()的时间复杂度线性正比于堆的高度h,故它们均可在O(logn)的时间内完成;

    • 基于向量的紧凑表示:按照层次遍历的次序,若将所有节点组织为一个向量,则堆中各节点(编号)与向量各单元(秩)将彼此一一对应;

      image-20220705143048188

    • image-20220705143107769

  • 完全二叉堆的实现

    image-20220705143315867

    image-20220705143431392

  • 元素插入O(logn)

    • 步骤:将新词条接至向量末尾,再对该词条实施上滤调整;

    • 上滤:当前节点优先级大与其父节点,交换这两个节点;

      • 最坏情况:上滤至堆顶(O(nlogn));
      • 平均上升为O(1),即总平均时间复杂度为O(logn);
    • 实例

      image-20220705144246483

    • 实现

      image-20220705144352016

  • 元素删除O(logn)

    • 步骤:摘除堆顶(首词条),代之以末词条,随后对新堆顶实施下滤调整;

    • 下滤:若当前节点优先级小于孩子节点,则交换该节点与其孩子节点中的大者;

    • 实例

      image-20220705144733619

    • 实现

      image-20220705144939777

  • 建堆

    • 蛮力算法O(nlogn):逐条插入;

    • 自上而下的上滤O(nlogn):将所有输入词条纳入长为n的向量之后,首单元处的词条本身可视作一个规模为1的堆,将下一单元中的词条插入当前堆并上滤,依次迭代;

    • 自下而上的下滤----Floyd算法O(n):将所有词条组成一棵完全二叉树,然后找到最后一个内部节点,自底而上地逐层合并;

      image-20220705150709182

  • 就地堆排序

    • 原理:将所有词条分成未排序(组织为一个堆)和已排序两类,不断从前一类中取出最大者(堆顶M),顺序加至后一类中;

    • 当与M交换的x无法胜任堆顶时,实施下滤;

      image-20220705151031588

    • 复杂度:O(nlogn),实际运行效率往往要高于其它O(nlogn)的算法;

    • 实例

      image-20220705151539471

    • 实现

      image-20220705151630326

左式堆

  • 堆合并:对于堆来说,为控制合并操作所涉及的节点数,反而需要保持某种意义上的“不平衡”;

  • 单侧倾斜:

    • 左式堆(leftist heap)是优先级队列的另一实现方式,可高效地支持堆合并操作,具体地,需参与调整的节点不超过O(logn)个;
    • 左式堆节点分布偏向左侧,合并操作只涉及右侧;
  • PQ_LeftHeap模板类

    image-20220705165701178

  • 空节点路径长度(null path length),记作npl(x),节点x的npl值取决于其左、右孩子npl值中的小者:

    • npl(x) = 1 + min( npl(lc(x)), npl(rc(x)) )

      image-20220705165857925

    • npl(x)既等于x到外部节点的最近距离(该指标由此得名),同时也等于以x为根的最大满子树(图中以矩形框出)的高度;

  • 左倾性与左式堆

    • 左式堆是处处满足“左倾性”的二叉堆,即任一内部节点x都满足:

      npl(lc(x)) ≥ npl(rc(x)),即任一内部节点的左孩子都不小于其右孩子;

      image-20220705170515495

    • 左式堆中任一内节点x都应满足:

      npl(x) = 1 + npl(rc(x)),即左式堆中每个节点的npl值,仅取决于其右孩子;

  • 最右侧通路

    • 从x出发沿右侧分支一直前行直至空节点,经过的通路称作其最右侧通路,记作rPath(x);

    • 每个节点的npl值,应恰好等于其最右侧通路的长度;

      image-20220705173106081

    • rPath®的终点必为全堆中深度最小的外部节点;

    • 最右侧通路必然不会长于O(logn);

  • 合并算法

    • 递归地将a的右子堆a_R与堆b合并,然后作为节点a的右孩子替换原先的a_R,比较a左、右孩子的npl值,如有必要还需将
      二者交换,以保证左孩子的npl值不低于右孩子;

      image-20220705173614315

    • 实例

      image-20220705174051058

  • 合并的实现

    image-20220705174158305

  • 复杂度

    • 递归只可能发生于两个待合并堆的最右侧通路上;
    • 若待合并堆的规模分别为n和m,则其两条最右侧通路的长度
      分别不会超过O(logn)和O(logm),合并算法总体运行时间应不超过O(log(max(n, m)))
  • 基于合并的插入和删除

    • delMax():总体不超过O(logn);
    • insert():总体不超过O(logn);

第11章 串

串及串匹配

  • 串:字符串、子串、判等;

  • 串匹配:如何在字符串数据中,检测和提取以字符串形式给出的某一局部特征

    image-20220706090806578

  • 约定:文本串T(|T| = n )和模式串P(|P| = m)

蛮力算法

image-20220706090930490
  • 时间复杂度:O(n∙m),其中,文本串长度为n、模式串长度为m;
  • 在最坏情况下所需时间,为文本串长度与模式串长度的乘积,无法应用于规模稍大的应用环境;

KMP算法

  • 构思

    image-20220706091617690

    • 避免文本串字符指针的回退,使模式串尽可能大跨度地右移(经验);
  • next表(P的自匹配—快速右移

    • 假设前一轮比对终止于T[i] ≠ P[j]。按以上构想,指针i不必回退,而是将T[i]与P[t]对齐并开始下一轮比对:

      image-20220706091934213

    • 若模式串P经适当右移之后,能够与T的某一(包含T[i]在内的)子串完全匹配,则:

      P[0, t) = T[i - t, i) = P[j - t, j)

      亦即,在P[0, j)中长度为t的真前缀,应与长度为t的真后缀完全匹配;

    • 具体由哪些t值构成,仅取决于模式串P以及前一轮比对的首个失配位置P[j],与文本串T无关;

    • 为保证P与T的对齐位置(指针i)不倒退,同时又不致遗漏任何可能的匹配,应挑选最大的t,即应该保守地选择P中移动距离最短者;

    • 总结:一旦发现P[j]与T[i]失配,即可转而将P[ next[j] ]与T[i]彼此对准,并从这一位置开始继续下一轮比对;

    • 通过预处理提前计算出所有位置j所对应的next[j]值,并整理为表格----next表

  • KMP算法

    image-20220706093329453

  • next[0] = -1

    • 假想地在P[0]的左侧“附加”一个P[-1],且该字符与任何字符都是匹配的,等同于“令next[0] = -1”;
  • next[j + 1]

    • 若next[j] = t,则意味着在P[0, j)中,自匹配的真前缀和真后缀的最大长度为t,故必有next[j + 1] ≤ next[j] + 1,当且仅当P[j] = P[t]时取等号;

      image-20220706093841513

    • 由next表的功能定义,next[j + 1]的下一候选者应该依次是:

      next[ next[j] ] + 1, next[ next[ next[j] ] ] + 1, …

      image-20220706093939545

    • 因此,令t = next[t],即可按优先次序遍历以上候选者;一旦发现P[j]与P[t]匹配(含与P[t = -1]的通配),即可令next[j + 1] = next[t] + 1;

    • 该算法必然会终止于通配的next[0] = -1;

  • 构造next表

    image-20220706094708600

  • KMP性能分析:总体运行时间为O(n + m)

  • 改进:

    • 除“对应于自匹配长度”以外,t只有还同时满足“当前字符对不匹配”的必要条件,方能归入集合N(P, j)并作为next表项的候选;

    image-20220706095334257

BM算法

  • 模式串P与文本串T的对准位置**“自左向右”推移,在每一对准位置“自右向左”**地逐一比对各字符;

    image-20220706104408525

  • 坏字符策略(bad character)

    • 失配处的字符称为坏字符,坏字符策略更多地关注教训,使之更早出现

      image-20220706103119199

    • 若P与T的某一(包括T[i + j]在内的)子串匹配,则必然在T[i + j] = X处匹配,只需找出P中的每一字符 X,分别与T[i + j] = 'X’对准,并执行一轮自右向左的扫描比对;

    • 对应于每个这样的字符X,P的位移量仅取决于原失配位置j,以及X在P中的秩,而与T和i无关;

    • bc表

      image-20220706103327005

      • 仅尝试P中最靠右的字符’X’(若存在)
    • 特殊情况

      • 若P根本就不含坏字符X,应将该串整体移过失配位置T[i + j],用P[0]对准T[i + j + 1],再启动下一轮比对,即将BC表中此类字符的对应项置为-1,效果也等同于在模式串的最左端,增添一个通配符;
      • 其中最靠右者的位置也可能太靠右,以至于k = bc[X] ≥ j,此时将P串右移一个字符,然后启动下一轮比对;
    • bc表构造算法

      image-20220706103901913

    • 复杂度:

      • 最好:O(n / m)
      • 最坏:O(n × m)

    image-20220706110107973

  • 好后缀策略(good suffix)

    • 每轮比对中的若干次(连续的)成功匹配,都对应于模式串P的一个后缀,称作“好后缀”,好后缀策略更多地关注经验;

      image-20220706104728498

    • 若值得将P[k]与T[i + j]对齐并做新的一轮比对,则P的子串V(k)首先必须与P自己的后缀U相互匹配;

    • 另一必要条件(与KMP的改进同理):P中这两个自匹配子串的前驱字符不得相等,即P[k] ≠ P[j];

    • k本身(包括向右位移量j - k)仅取决于模式串P以及j值,因此可以仿照KMP算法得到gs表,取gs[j] = m - |V(k)|;

    • 且若P中没有任何子串V(k)可与好后缀U完全匹配,则应找出可与U的某一(真)后缀相匹配的最长者作为V(k);

    image-20220706110052572

  • 时间效率

    image-20220706095811713

    image-20220706095823287

第12章 排序

快速排序

  • 分治策略的典型应用,将问题划分为彼此独立的两个子问题;

  • 轴点

    • 队任一向量区间S[lo, hi)。对于任何lo ≤ mi < hi,若S[lo, mi)中的元素均不大于S[mi],且S(mi, hi)中的元素均不小于S[mi],则元素S[mi]称作向量S的一个轴点(pivot);

      image-20220706110833640

    • 以轴点S[mi]为界,前、后子向量的排序可各自独立地进行,一旦前、后子向量各自完成排序,即可立即(在O(1)时间内)得到整个向量的排序结果;

  • 快速划分算法(找轴点及划分子向量)

    • 取出首元素m = S[lo]作为候选,腾出的空闲单元便于其它元素的位置调整,不断试图移动lo和hi,使之相互靠拢;

    • 当lo与hi彼此重合时,只需将原备份的m回填至这一位置,则S[lo = hi]= m便成为一个名副其实的轴点;

      image-20220706111122653

    • 实现

      image-20220706111500148

    • 实例

      image-20220706111711536

  • 复杂度:快速排序算法的平均效率可以达到O(nlogn),且具体地,平均运行时间记作 ^ T(n) = O(1.386∙log_2_n);

选取与中位数

  • 概述

    • 中位数可将原数据集(原问题)划分为大小明确、规模相仿且彼此独立的两个子集,故能否高效地确定中位数,将直接关系到采用分治策略的算法能否高效地实现;
    • 蛮力算法,复杂度O(nlog^2_n);
  • 众数

    • 若众数存在,则必然同时也是中位数

    • 减而治之(运行时间线性正比于向量规模)

      image-20220706112247135

      • 迭代剪除前缀P可以逐步缩小问题规模;

      image-20220706112318927

  • 归并向量的中位数

    image-20220706112501667

    • 减而治之(总体时间复杂度为O(logn))

      image-20220706112805444

      • 迭代剪除S的两翼可以逐步缩小问题规模;

    image-20220706112914928

  • 基于优先级队列的k选取

    image-20220706113352121

    • (a1):将全体元素组织为一个小顶堆;然后经过k次delMin()操作,则如图(a2)所示得到位序为k的元素;

    • (b):任取k个元素组织为大顶堆,然后将剩余的n - k个元素逐个插入堆中;每插入一个,随即删除堆顶,待所有元素处理完毕之后,堆顶即为目标元素;

    • ©:分别构建一个规模为n - k的小顶堆G和一个规模为k的大顶堆H,反复比较它们的堆顶g和h,只要g < h,则将二者交换;

    • 在目标元素的秩很小或很大时,上述算法性能较好,当k ≈ n/2时,以上算法的复杂度均退化至蛮力算法的O(nlogn);

  • 基于快速划分的k选取

    • 选取问题所查找元素的位序k,就是其在对应的有序序列中的秩;

    • 步骤:构造向量的一个轴点A[i] = x,若i = k,则该轴点恰好就是待选取的目标元素,即可直接将其返回,反之,若k < i,将子向量G剪除,然后递归地在剩余区间继续做k-选取,k>i同理;

      image-20220706142916776

    • 最坏情况下总体运行时间为O(n^2);

  • k- 选取算法

    image-20220706143220987

    • k-选取目标元素所处位置的三种可能情况

    image-20220706143409428

    • 复杂度

      • 算法的第5)步尽管会发生递归,但需进一步处理的序列的规模,绝不致超过原序列的3/4;

      • T(n) = cn + T(n/Q) + T(3n/4);若取Q = 5,则有

        T(n) = cn + T(n/5) + T(3n/4) = O(20cn) = O(n)

      • 线性复杂度中的常系数项过大,以致在通常规模的应用中难以体现出优势;

希尔排序

  • 递减增量策略

    • 希尔排序(Shellsort)算法将整个待排序向量A[ ]等效地视作一个二维矩阵B[ ] [][][][][][][ ]

      image-20220706143855496

    • 若原一维向量为A[0, n),则对于任一固定的矩阵宽度w,A与B中元素之间总有一一对应关系:

      • B[i] [j] = A[iw + j]
      • A[k] = B[k / w] [k % w]
    • 算法框架

      image-20220706144045957

    • 希尔排序是个迭代式重复的过程,各步迭代中矩阵的宽度呈缩减的趋势,直至最终使用w_1 = 1;

    • 支持希尔排序的底层排序算法,必须是输入敏感的,这样才会在每步迭代中减小逆序对数目;

    image-20220706144157734

  • 增量序列

    • 理论:

      • 已经g-有序的向量,再经h-排序之后,依然保持g-有序,对于g和h的任一线性组合mg + nh,该向量也应(mg + nh)-有序;
      • 必须尽可能减少不同增量值之间的公共因子,至少保证相邻项之间应彼此互素,只要g和h互素,逆序对的间距就绝不可能大于(g - 1)∙(h - 1);
      • 希尔排序过程中向量中每个元素所能参与构成的逆序对持续减少,整个向量所含逆序对的总数也持续减少,底层所采用的插入排序算法的实际执行时间,也将不断减少,从而提高希尔排序的整体效率。
    • Shell序列

      image-20220706144427082

      • 由于除首项外均可被2整除,故最后一次迭代前,两个子序列之间有序性并无改善,导致最后一轮插入排序所做比较操作次数共计O(n^2);
    • Papernov-Stasevic序列

      image-20220706145818968

      • 在最坏情况下的运行时间不超过O(n^(3/2));
    • Pratt序列

      image-20220706145919117

      • 在最坏情况下的运行时间不超过O(nlog^2_n),但其中各项的间距太小,会导致迭代趟数过多;
    • Sedgewick序列

      image-20220706150029146

      • 在最坏情况下的时间复杂度为O(n^(4/3)),平均复杂度为O(n ^(7/6)),在通常的应用环境中,这一增量序列的综合效率最佳。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值