数据结构

Author: 经12-计18 张诗颖 2021011056

 

1 绪论

1. 工具:尺规计算机
2. 算法

要求:正确性、确定性、可行性、有穷性 好算法:正确、健壮、可读、效率 Algorithms+DataStructures=Programs (Algorithms+DataStructures)×Efficiency=Computation 算法分析:正确、成本 TA(P)(算法A求解问题实例P的计算成本)

正确性证明:Loop Invariant, Convergence, Correctness

[EG/有穷性]:Hailstone序列(尚未证明或证否)

3. 计算模型
  1. 图灵机 (Turing Machine)

    image-20230131093824233

    • Tape:依次均匀地划分为单元格,各存有某一字符(初始为#),理想假设无限长
    • Head:总是对准某一单元格,可读写;经过一个节拍可转向左右邻格
    • Alphabet:字符的种类有限
    • State:图灵机总是处于有限种状态中的某一种,约定'h' = halt

    Transition Function(q, c; d, L/R, p)(当前状态q,当前字符c,改写d,状态p) [EG](<, 1; 0, L, <) 计算成本:从启动至停机所经历的节拍数目/Head累计的移动次数

    image-20230131095101421

  2. RAM (Random Access Machine)

    • 寄存器顺序编号,总数没有限制

    • 可通过编号直接访问任意寄存器 call-by-rank

    • 每一基本操作(10个)仅需常数时间

    计算成本:算法需要执行的基本操作次数

    [EG]:Ceiling Division(除法上整)

    image-20230131095426004

     

1.1 渐进复杂度:大O记号

Paul Bachmann, 1894: T(n) = O(f(n)) iff c > 0 s.t. T(n) < c · f(n) n >> 2 T(n) = Ω(f(n)) iff c > 0 s.t. T(n) > c · f(n) n >> 2 T(n) = Θ(f(n)) iff c1 > c2 > 0 s.t. c1· f(n) > T(n) > c2· f(n) n >> 2

O(1):constant O(logcn):poly-log,复杂度无限接近于常数(c>0,logn=O(nc)),[EP]:逆Ackermann函数 O(logn):对数,复杂度无限接近于常数,[EP]:二分查找、堆、词典查询、插入与删除 O(nc):polynomial,其中所有O(n)类函数称为线性 (Linear function)的([EP]:数、图的遍历),复杂度已可令人满意 [EP/线性]O(nlogcn):某些MST算法; O(nloglogn):某些三角剖分算法; O(logn):排序、EU\Huffman编码 [EP/平方]O(n2):Dijkstra算法 [EP/立方]O(n3):矩阵乘法 以上均为P问题(存在多项式算法的问题) O(2n):exponential,通常属于有效算法到无效算法的分水岭,计算成本不可忍受 [EG]:2-Subset问题——NP-complete(不存在可在多项式时间内解答的算法)

NP = Non-deterministic Polynomial

image-20230131100351260

1. 级数复杂度
2. 复杂度分析
  1. 常见的二层循环
  1. 封底估算:Back-Of-The-Envelope Calculation

    1 day ≈ 105 sec 1生 ≈ 1 世纪 ≈ 3 × 109 sec 三生三世 ≈ 300yr ≈ 1010 sec 宇宙大爆炸至今 ≈ 4 × 1017 sec

    普通PC:1GHz,109 flops 天河1A:1P,1015 flops

    image-20230131102836891

任何算法其时间复杂度一般是空间复杂度的上界(所以一般不首先考虑空间复杂度)

 

1.2 迭代与递归

递归跟踪:绘出计算过程中出现过的所有递归实例及其调用关系,他们各自所需时间之和为整体运行时间 递推方程[EG/Sum] T(n) = T(n - 1) + O(1), base: T(0) = O(1) => 求解

1. 减而治之

Decrease-and-conquer,将原问题划分为两个子问题,一个平凡,一个规模缩减 递归的潜在问题:空间复杂度爆炸

[EG/Reverse]:

递归版

迭代版

2. 分而治之

Divide-and-conquer,将原问题划分为若干个子问题,且子问题规模大体相当

[EG/BinaryRecursion]

3. Master Theorem

分治策略对应的递推式通常形如:T(n) = a·T(n/b) + O(f(n)) [解释]:原问题被划分为a个规模均为n/b的子任务;任务的划分,解的合并耗时f(n)

4. EP:Multiplication

image-20230131104131789

image-20230131104154846

5. EP:总和最大区段

问题描述:从整数序列中,找出总和最大的区段(有多个时,短者优先)

  1. 蛮力算法:O(n3)

  2. 递增策略:O(n2)

  3. 分治策略:O(nlogn)

    image-20230131112357065

  4. 减治策略:O(n) 构思:考察最短的总和非正的后缀A[k, hi),以及总和最大的区段GS(lo, hi) = A[i, j),可以发现后者要么是前者的(真)后缀,要么与前者无交

    image-20230131112909788

 

1.3 动态规划

动态规划:Dynamic Programming,一种颠倒计算方法,自底而上迭代的方式。

[EG/fibonacci]:T(n) = O(n)

1. EP:最长公共子序列
  1. 减而治之——递归:复杂度T(n)=O(2n)(当n=m时)

    image-20230131121134228

    image-20230131121243467

    递归基:n = 0m = 0取作空序列""(长度为零) A[n - 1] = 'X' = B[m - 1],则取作:LCS(n - 1, m - 2) + 'X' A[n - 1] ≠ B[m - 1],则在LCS(n, m - 1)LCS(n - 1, m)中取更长者

  2. 动态规划——递推:复杂度T(n)=O(n·m)

    image-20230131121709824

2. EP:就地循环移位

问题描述:仅用O(1)辅助空间,将数组A[0, n)中的元素向左循环移动k个单位

  1. 蛮力算法:while (k--) shift(A, n, 0, 1); 反复以1为间距循环左移,T(n)=O(nk)

  2. 迭代版:按照同余类依次左移,时间复杂度T(n)=O(2n)

    image-20230131125232076

  3. 倒置版:reverse(A, k), reverse(A + k, n - k), reverse(A, n), 时间复杂度T(n)=O(3n),但是由于计算机Cache的存在,在实际实现时会更快

     

 

2 向量

循秩访问 (Call-By-Rank)

抽象数据类型 (Abstract Data Type):数据模型 + 定义在该模型上的一组操作 数据结构 (Data Structure):基于某种特定语言,实现ADT的一整套算法 Application = Interface × Implementation 具体实现 (Implementation) + 实际应用 (Interface/Application)

2.1 基本语法

数组:元素由编号唯一指代并可以直接访问,称作线性数组 (Linear Array),访问方式称为寻秩访问 (call-by-rank),其中元素下标称为元素的using Rank = int;

向量:数组的抽象与泛化,由一组元素按线性次序封装而成;元素类型可以灵活选取

1. Vector ADT

vec.begin():0秩迭代器 vec.end():末秩迭代器

  1. size()返回元素总数
  2. get(r)返回秩为r的元素
  3. put(r, e):用e替换秩为r元素的数值
  4. insert(r, e):e作为秩为r的元素插入,原先后继依次后移
  5. remove(r):删除秩为r的元素,返回该元素原值
  6. disordered():判断所有元素是否已经按非降序排列
  7. sort():调整各元素的位置,使之按非降序排列
  8. find(a)返回目标元素a的秩(如果不存在返回-1)
  9. search(e)返回不大于e且秩最大的元素(有序向量)
  10. deduplicate(), uniquify():剔除重复元素(无序/有序向量)
  11. traverse():遍历向量并统一处理所有元素,参数为函数对象或函数指针
2. Vector的构造与析构

(typename=T)

vec._capacity:容量 define DEFAULT_CAPACITY X:默认初始容量X vec._size:元素总数 vec._elem:指向元素数组的指针

  1. 构造函数 默认构造函数:Vector(int c = DEFAULT_CAPACITY) {_elem = new T[_capacity = c]; _size = 0;}

    数组区间复制:Vector(T const * A, Rank lo, Rank hi) {copyFrom(A, lo, hi);} 向量区间复制:Vector(Vector<T> const & V, Rank lo, Rank hi) {copyFrom(V._elem, lo, hi);} 向量整体复制:Vector(Vector<T> const & V) {copyFrom(V._elem, 0, V._size);} copyFrom()_elem = new T[_capacity = max(DEFAULT_CAPACITY, 2*(hi - lo))];

  2. 析构函数 ~Vector{delete [] _elem;}

3. 动态空间管理

上溢 (overflow):_elem[]不足以存放所有元素 下溢 (underflow):_elem[]中的元素寥寥无几 装填因子 (load factor):λ=_size_capacity<<50%

--扩容算法:expand()
--算法分析

平均分析 (average complexity):根据各种操作出现概率的分布,计算成本加权平均 各种可能的操作独立考察,割裂了操作之间的相关性和连贯性,往往不能准确评判dsa 分摊分析 (amortized complexity):连续实施足够多次操作,所需总体成本摊还至单次操作 忠实刻画可能出现的操作序列,更为精准地评判dsa

  1. 容量递增策略:时间成本O(n2),每次操作的分摊成本O(n),装填因子λ100%
  2. 容量加倍策略:时间成本O(n),每次操作的分摊成本O(1),装填因子λ>50%
4. 无序向量:基本操作的实现
  1. 元素访问:重载[]运算符

  2. 插入元素:insert(r, e)

  3. 区间删除:remove(lo, hi)

    单元素删除:remove(Rank r)

  4. 判等器

    有序向量:比较器(较判等器增加<, >

  5. 顺序查找:find(e, lo, hi)

    输入敏感 (input-sensitive):最好O(1),最差O(n)

  6. 去重:deduplicate()

  7. 遍历:traverse()

    [EP/Increase]

5. 有序向量:基本操作的实现

相邻逆序对的数目可以度量向量的紊乱程度

判断是否有序

  1. 去重:uniquify(),复杂度O(n)

    image-20230131145014453

 

2.2 查找(有序向量)

更精细地评估查找算法的性能查找长度(search length,关键码的比较次数)

1. 二分查找(版本A)
  1. binary_search 平均查找长度 = O(1.50·logn):左右分治复杂度≈1:2

  2. Fibonacci_search 平均查找长度 = O(1.44·logn)

    image-20230131145901200

    通用策略:在任何区间[0, n)内,总是选取λ·n作为轴点,0λ1。二分查找对应于λ=0.5,Fibonacci查找对应于λ=ϕ=0.6180339... 这类算法的渐近复杂度为α(λ)·log2n=O(logn)

    递推式:α(λ)·log2n=λ·[1+α(λ)·log2(λn)]+(1λ)·[2+α(λ)·log2((1λ)n)] 整理后:ln2α(λ)=λ·lnλ+(1λ)·ln(1λ)2λ

    λ=ϕ=512时,有α(λ)=1.440420...最小

2. 二分查找(版本B)

相对于版本A,整体性能更趋均衡

3. 二分查找(版本C)

较版本A和版本B优化了返回信息

返回信息search(e)应该返回不大于e的最后一个元素,使得语句V.insert(1 + V.search(e), e)成立

证明:① 不变性:A[0, lo) ≤ e < A[hi, n) 在算法执行过程中的任意时刻,A[lo - 1]总是(截至目前已确认的)≤ e的最大者,A[hi]总是(截至目前已确认的)> e的最小者;算法结束时A[lo - 1] = A[hi - 1]是全局 ≤ e的最大者。 ② 规模缩减

4. 插值查找

假设:已知有序向量中各元素随机分布的规律,如独立且均匀的随机分布 [lo, hi]内各元素应大致呈线性趋势增长:milohiloeA[lo]A[hi]A[lo] 因此,通过猜测轴点mi,可以极大地提高收敛速度:milo+(hilo)·eA[lo]A[hi]A[lo]

算法分析 最坏情况:O(n) 平均情况:O(loglogn)(每经过一次比较,待查区间宽度由n缩减为n,有效字长logn减半)

插值查找:在字长意义上的折半查找 二分查找:在字长意义上的顺序查找

image-20230131185919972

综合评价 O(logn)O(loglogn),优势并不明显 须引入乘法、除法运算 易受小扰动的干扰和畸形分布的“蒙骗”

实际可行的方法:首先通过插值查找将查找范围缩小,然后再进行二分查找进一步缩小范围,最后在102数据范围采用顺序查找

 

2.3 排序

排序器:统一入口

1. 起泡排序bubbleSort

复杂度:O(n2)

基本版

提前终止版

跳跃版

image-20230131194309378

算法评价
2. 归并排序mergeSort

分而治之的思想,by J. von Neumann (1945) 复杂度:O(n·logn)

image-20220930100236163

算法评价

 

2.4 位图:数据结构

有限整数集:实现三个主要功能bool test(int k);void set(int k);void clear(int k)

1. 位图dsa实现

image-20220930124556768

应用:小集合+大数据 int A[n]的元素均取自[0, m),已知数据规模不大但重复度极高,如何去重? Idea1. 先排序,在扫描——T(n)=O(nlogn+n)S(n)=O(4n) 考虑224=m<<n=1010,S(n)=40GB(24位无符号整数),内存消耗不可忍受 Idea2. 运用Bitmap存储所有数据是否存在的状态 总体运行时间 = O(n+m)=O(n),S(n)=O(m) 就上例而言,空间复杂度降至m/8=221=2MB<<40GB

2. 筛法

内循环每趟迭代O(n/i)步,由素数定理外循环至多n/lnn趟,累计耗时O(n·logn)

image-20220930131154594

3. 快速初始化:校验环

结构校验环(J. Hopcroft, 1974),将B[]拆分成一对等长的Rank型向量,有效位均满足:T[F[k]] = k, F[T[k]] = k,操作复杂度为O(1)

 

 

3 列表

循位置访问(Call-By-Position)

3.1 ListNode ADT

  1. pred():当前节点前驱节点的位置

  2. succ():当前节点后继节点的位置

  3. data():当前节点所存数据对象

  4. insertAsPred(e):插入前驱节点,存入被引用对象e,返回新节点位置

  5. insertAsSucc(e):插入后继节点的,存入被引用对象e,返回新节点位置

 

3.2 List ADT

约定:头、首、末、尾节点的秩,分别理解为-1, 0, n-1, n

image-20220930160142504


  1. size():报告列表当前的规模(节点总数)

  2. first(), last():返回首、末节点的位置

  3. insertAsFirst(e), insertAsLast(e):将e当作首、末节点插入

  4. insert(p, e), insert(e, p):将e当作节点p的直接后继前驱插入

  5. remove(p):删除位置p处的节点,返回其中数据项

  6. disordered():判断所有节点是否已按非降序排列

  7. sort():调整各节点的位置,使之按非降序排列

  8. find(e):查找目标元素e,失败时返回NULL

  9. search(e)【有序列表】:查找e,返回不大于e且秩最大的节点

  10. deduplicate(), uniquify():针对列表/有序列表,踢除重复节点

  11. traverse():遍历列表

     

3.3 排序

1. 选择排序selectionSort

性能分析

2. 循环节/Cycle

Definition

任何一个序列A[0,n),都可以分解为若干个循环节 任何一个序列A[0,n),都对应于一个有序序列S[0,n) 元素A[k]S中对应的秩,记作r(A[k])=r(k)[0,n) 元素A[k]所属的循环节是:A[k],A[r(k)],A[r(r(k))],......A[r(...(r(k)))]=A[k] 每个循环节,长度均不超过n 循环节之间,互不相交

image-20230202144834154

Analysis

  1. 采用交换法,每迭代(swap())一步,M都会脱离原属的循环节,自成一个循环节 M原属的循环节,长度恰好减少一个单位,其余循环节保持不变
  2. M已经就位,无需交换”——这样的情况会出现c次,其中c表示最初有的循环节个数 c:最大值为n期望为Θ(logn)
3. 插入排序insertionSort

image-20220930174003511

性能分析

若各元素的取值系独立均匀分布,平均要做多少次元素比较? 考察e=[r]刚插入完成的那一时刻,此时的有序前缀[0,r]中,谁是e 观察:其中的r+1个元素都有可能,且概率均为1r+1 因此,在刚完成的这次迭代中,为引入S[r]所花费时间的数学期望为:1+k=0rkr+1=1+r2 于是,总体时间的数学期望为 r=0n1(1+r2)=O(n2)

Case Sensitive best case = O(n) worst case = O(n2)

4. 归并排序mergeSort

时间复杂度:O(n·logn)

5. 逆序对/Inversion

Definition: I(j)={0i<j|A[i]>A[j]} (将逆序对统一记到后者的账上)

逆序对总数:I=jCn2=O(n2)

3.4 游标实现

利用线性数组,以游标方式模拟列表

elem[]:对外可见的数据项 link[]:数据项之间的引用

维护逻辑上互补的列表datafree insert()操作的时间复杂度为O(1)remove()操作的时间复杂度为O(n) 游标的数组大小是固定的、不可更改的

image-20221005091226352

 

3.5 Java和Python的列表实现

 

 

4 栈与队列

栈(stack):是受限的序列 只能在栈顶(top)插入和删除 栈底(bottom)为盲端 后进先出(LIFO)

image-20221005091957826

 

4.1 栈及其基本运用

基本接口

size() / empty():判断大小 / 判断是否为空 push():入栈 pop():出栈 top():查顶

扩展接口:getMax()......

  1. 属于序列的特例,可以直接基于向量或列表派生
  1. 也可以通过继承List来实现
  2. 如此实现的栈的各接口,均只需O(1)时间
递归

image-20230204210026151

空间复杂度:递归算法所需的空间,主要取决于递归的深度,而非递归实例总数

1. 消除递归:尾递归

定义:在递归实例中,作为最后一步的递归调用

是最简单的递归模式 一旦抵达递归基,便会引发一连串的return(且返回地址相同),调用栈相应地连续pop 编译器一般可以自动识别并代为改写为迭代形式 改用迭代形式:时间复杂度有常系数改进,空间复杂度或有渐近改进

以阶乘为例

image-20221007101327420

2. 进制转换

实现难点:位数m不确定,如何使得空间不浪费,且自低而高得到数位、自高而低输出

3. 括号匹配

算法思路:顺序扫描表达式,用栈记录已扫描的部分——反复迭代(遇到(则进栈,遇到)则出栈)

实际上,若仅考虑一种括号,只需一个计数器足矣:S.size()

一旦转负,则为适配(右括号多余); 最后不为零,则不匹配(左括号多余); 最后归零,即为匹配

扩展:多类括号 只需约定“括号”的通用格式,而不必实现固定括号的类型与数量 [EX/html]:<body> | </body>, <h1> | </h1>, <font> | </font>, <p> | </p>...

4. 栈混洗 Stack Permutation

栈混洗:将栈A中的元素全部转入栈B中(S.push(A.pop()); B.push(S.pop())

Notation: <a1,a2,a3,...,an]

括号匹配 观察:每一栈混洗,都对应于栈S的n次pushn次pop操作构成的某一序列;反之亦然 n个元素的栈混洗,等价于n对括号的匹配;二者的组合数,也自然相等

 

4.2 表达式求值

1. 中缀表达式求值

思路:自左向右扫描表达式,用栈记录已扫描的部分(含已执行运算的结果) 栈的顶部存在可优先计算的子表达式 ? 该子表达式推展;计算其数值;计算结果进栈 : 当前字符进栈,转入下一字符

image-20221007132132530

2. 逆波兰表达式 Reverse Polish Notation / RPN

逆波兰表达式:J. Lukasiewicz (1878 ~ 1956) 由运算符 (operator) 和操作数 (operand) 组成的表达式中,不需要括号 (parenthesis-free),即可表示带优先级的运算关系 作为补偿,须额外引入一个起分割作用的元字符(比如空格)

亦称作后缀式 (postfix)

应用:PostScript (1985),支持设备独立的图形描述 (1 × 解释器 + 5 × 栈)× RPN语法

image-20221007154742329

 

4.3 队列

队列(queue):受限的序列

只能在队尾插入 / 查询:enqueue(), rear() 只能在队头删除 / 查询:dequeue(), front()

先进先出 (FIFO): First in First out 后进后出 (LILO): Last in Last out

扩展接口:getMax()...

基本接口

bool empty():判空 void enqueue(T const& e):队尾插入 T dequeue():队头删除 T front():查询队头元素 int size():查询队列长度

  1. 属于序列的特例,可以直接基于向量或列表派生

  2. 如此实现的队列接口,均只需O(1)时间

1. 资源循环分配
2. 银行服务模型

提供n个服务窗口: 任一时刻,每个窗口至多接待一位顾客,其他顾客排队等候 顾客到达后,自动地选择和加入最短队列(的末尾)

3. 双栈当队

Queue = Stack × 2

Best / Worse case: O(1)/O(n)

Amortized Cost (of any operation sequence involving n items) = 4n=O(n) Aggregate Cost = 3e+d (e denotes enqueue() times, d denotes dequeue() times) the amortized cost for each OPERATION is 3e+de+d<3 Amortization by Potential: Consider the kth operation Define ϕk=|Rk||Fk| Then Ak=Tk+ϕkϕk12 Hence 2n=T(n)+ϕnϕ0>T(n)n T(n)<3n=O(n)

image-20221012083303494

4. Steap + Queap
Steap = Stack + Heap

P中每个元素,都是S中对应后缀里的最大者

image-20230205142252297

改进:构造p'队列,记录相同最大值的数量大小,从后往前扫描时只需累加并合并即可,分摊复杂度可以下降。

Queap = Queue + Heap

P中每个元素,都是Q中对应后缀的最大者

image-20230205142454871

5. 直方图内最大矩形

Maximum: 全局最大值 Maximal: 局部极大值

image-20221014110648241

 

 

5 二叉树

一种半线性的数据结构(在确定某种次序之后,具有线性特征)

image-20221014132316935

二叉树

二叉树:有根有序树

二叉树/多叉树的描述
  1. 二叉树的描述

    root():根节点 parent():父节点 firstChild():长子 nextSibling():兄弟 insert(i, e):将e作为第i个孩子插入 remove(i):删除第i个孩子(及其后代) traverse():遍历

    image-20221014153002235

  2. 多叉树的描述:长子-兄弟表示法

    有根有序的多叉树,均可转化并表示为二叉树

    • 长子 ~ 左孩子:firstChild() ~ lc()
    • 兄弟 ~ 右孩子:nextSibling() ~ rc()

image-20221014153503980

 

5.1 二叉树的实现

基本接口
  1. BinNode模板类

  2. BinTree模板类

image-20221019132621745

以下遍历其实都是深度优先搜索

1. 先序遍历

Problem: 使用默认的Call Stack,允许的递归深度有限 改进:藤缠树

沿着左侧藤,自上而下访问藤上节点,自下而上遍历各右子树 各右子树的遍历彼此独立,自成一个子任务

image-20221019133334063

2. 中序遍历
藤缠树

沿着左侧藤,遍历可自底而上分解为d+1步迭代:访问藤上节点,再遍历其右子树 各右子树的遍历彼此独立,自成一个子任务

image-20221019134611723

后继与前驱

image-20221019140041954

3. 后序遍历
藤缠树

从根出发下行:尽可能沿分支,实不得已才沿分支 最后一个节点必定是叶子,而且是按中序遍历次序最靠左者,也是递归版中visit()首次执行处

image-20221019140806910

image-20230207002649275

表达式树(RPN)

image-20221019143017444

RPN实际上是Expression Tree的后序遍历

4. 层次遍历(广度优先)

概念区分:满二叉树、完全二叉树、真二叉树

完全二叉树 叶节点仅限于最低两层 底层叶子,均居于次底层叶子左侧(相当于LCA) 除末节点的父亲,内部节点均有双子

叶节点:不致少于内部节点,但至多多出一个

image-20221021100404680

考察遍历过程中的n步迭代... [n/2]1步迭代中,均有孩子入队 [n/2]步迭代中,都有孩子入队 累计至少n1次入队

辅助队列的规模: 先增后减,单峰对称 最大规模 = [n/2],最大规模可能出现2次

 

5.2 二叉树的重构

1. 先序 | 后序 => 中旭

image-20221021123103947

2. 先序 <=> 后序

image-20221021123151818

3. 增强序列

假想地认为,每个NULL也是“真实”节点,并在遍历时一并输出 每次递归返回,同时输出一个事先约定的元字符“^”

若将遍历序列表示为一个Iterator,则可将其定义为Vector<BinNode<T>*>,于是在增强的遍历序列中,这类“节点”可统一记作NULL

可归纳证明:在增强的先序、中序、后序遍历序列中 1)任一子树依然对应于一个子序列,而且 2)其中的NULL节点恰比非NULL节点多一个

如此,通过对增强序列分而治之,即可重构原树

image-20221021123652861

实例

image-20221021123729541

 

5.3 Huffman编码树

编码

image-20221021102819389

1. PFC编码 (prefix-free code)

中的字符组织成一棵二叉树,以0/1表示左/右孩子,各字符x分别存放于对应的叶子v(x) 字符x的编码串rps(v(x))=rps(x)由根到v(x)的通路(root path)确定 不同字符的编码互不为前缀,故不致歧义

平均编码长度ald(T)=xdepth(v(x))|| 对于特定的ald()最小者即为最优编码树Topt

完全树是最优编码树

2. Huffman编码:最优带权编码树

已知各字符的期望频率,构造最优编码树 频率最/的(超)字符,应尽可能放在/ 故此,通过适当的交换,同样可以缩短wald(T)

文件长度正比于平均带圈深度wald(T)=xrps(x)×w(x)

伪代码

贪婪策略:频率的字符优先引入,位置亦更 为每个字符创建一棵单节点的数,组成森林F,按照出现频率,对数排序 while (F中的树不止一棵) 取出频率最小的两棵树:T1T2 将它们合并成一棵新树T,并令: lc(T)=T1rc(T)=T2 w(root(T))=w(root(T1))+w(root(T2))

尽管贪心策略未必总能得到最优解,但如上算法的确能够得到最优编码树之一

正确性
  1. 双子性

    最优编码树的特征

    首先,每一内部节点都有两个孩子——节点度数均为偶数(0或2),即二叉树 否则,将1度节点替换为其唯一的孩子,则新书的wald更小

    image-20221021104929101

  2. 不唯一性

    对任一内部节点而言,左右子树互换之后wald不变 为消除这种歧义,可以(比如)明确要求子树的频率更

    image-20221021105041740

  3. 层次性

    出现频率最低的字符x和y,必在某棵最优编码树中处于最底层,且互为兄弟
    否则,任取一棵最优编码树,并在其最底层任取一对兄弟a和b,于是,a和x、b和y 交换之后,wald绝不会增加

image-20221021105304256

  1. 数学归纳和定差

    ||做归纳可证:Huffman算法所成的必然是一棵最优编码树

    ||=2时显然 设算法在||<n时均正确。现在设||=n,取中频率最低的xy(不放设二者互为兄弟),令=( \ {x,y}){z},w(z)=w(x)+w(y)

    定差:对于任一编码树T,只要为z添加孩子xy,即可得到的一棵编码树T,且满足wd(T)wd(T)=w(x)+w(y)=w(z)

    因此,只要T的最优编码树,则T也是的最优编码树(之一)

    image-20221021125006912

3. Huffman编码树:算法实现

Huffman(超)字符

Huffman(子)树

Huffman森林

更高效的数据结构实现方式: (得益于已定义的统一接口,支撑Huffman算法的这些底层数据结构可直接彼此替换

构造编码树

构造编码表

优化:向量+列表+优先级队列

方案1O(n2) 初始化时,通过排序得到一个非升序向量O(nlogn) 每次(从端)取出频率最低的两个节点:O(1) 将合并得到的新树插入向量,并保持有序:O(n)

方案2O(n2) 初始化时,通过排序得到一个非降序列表O(nlogn) 每次(从端)取出频率最低的两个节点:O(1) 将合并得到的新树插入列表,并保持有序:O(n)

方案3O(nlogn) 初始化时,将所有数组织为一个优先级队列O(n) 取出频率最低的两个节点,合并得到的新树插入队列:O(logn)+P(logn)

方案4O(nlogn) 所有字符按频率排序,构成一个O(nlogn) 维护另一个有序队列O(n)

image-20221021133354548

 

5.4 下界:代数判定树

问题P的难度:若问题P存在算法,则所有算法中最低的复杂度成为P的难度

多种角度估算时间、空间复杂度

最好 / best-case 最坏 / worst-case 平均 / average-case 分摊 / amortized

其中,对最坏情况的估计最保守、最稳妥。因此,首先应考虑最坏情况最优的算法(worst-case optimal)

基于比较的算法(Comparison-Based Algorithm):任何CBD在最坏情况下,都需要Ω(nlogn)时间才能完成排序

1. 判定树

每个CBA算法都对应于一棵Algebraic Decision Tree 每一可能的输出,都对应于至少一个叶节点 每一次运行过程,都对应于起始于根的某条路径

代数判定树(Algebraic Decision Tree) 针对“比较-判定”式算法的计算模型 给定输入的规模,将所有可能的输入所对应的一系列判断表示出来

image-20221021153622224

代数判定 使用某一常数次数代数多项式将任意一组关键码作为变量,对多项式求值 根据结果的符号,确定算法推进方向

叶节点深度 ~ 比较次数 ~ 计算成本 树高 ~ 最坏情况时的计算成本 树高的下界 ~ 所有CBA的时间复杂度下界

2. 下界:归约

线性归约(Linear-Time Reduction):除了(代数)判定树,归约(reduction)也是确定下界的有利工具

image-20221021154155242

实例

[Element Uniqueness] 任意n个实数中,是否包含雷同?——O(nlogn) EUNClosest Pair [Integer Element Uniqueness] 任意n个整数中,是否包含雷同?——O(nlogn) IEDNSegment Intersection Detection [Set Disjointness] 任意一对集合A和B,是否存在公共元素?——O(nlogn) SDNDiameter [Red-Blue Matching] 平面上任给n个红色点和n个蓝色点,如何用互不相交的线段配对联结——O(nlogn) SortingNRedBlue Matching

SortingNHuffman TreeNOptimal Encoding Tree

 

5.5 其它应用

  1. Graph/Tree: Diameter / Eccentricity / Radius / Center

  2. Knights of the Round Table / Travelling Knight

     

 

6 二叉搜索树 BST

call-by-Key:循关键码访问 数据集中的数据项,统一地表示和实现为词条(entry)形式

假设:没有重复关键码

顺序性:任一节点均不小/大于/后代 <==> 任一节点均不小/大于其左/右孩子

顺序性只是对局部特征的刻画,却可导出BST的整体特征

单调性:对树高做数学归纳,可以证明BST的中序遍历序列,必然单调非降

基本接口

6.1 查找

减而治之:对照中序遍历序列,整个过程可视作有序向量的二分查找

image-20221027231951397

6.2 插入

先借助search(e)确定插入位置及方向 e不存在,则再将新节点作为叶子插入 _hot为新节点的父亲 v = search(e)_hot对新孩子的引用 于是,只需令_hot通过v指向新节点

image-20221027232533863

6.3 删除

主要难点:双分支情况下的删除

调用BinNode::succ()找到x的直接后继(必无左孩子),交换x进而问题转换为删除w,可按前一情况处理 尽管顺序性在中途曾一度不合,但最终必将重新恢复

image-20221027233729592

 

6.4 平衡 BBST

1. 期望树高

BST的主要接口search()insert()remove()的运行时间在最坏情况下,均线性正比于其高度O(h) 在最坏情况下,二叉搜索树可能彻底退化为列表 下面给出此类较坏情况下的概率分析

2. 理想与渐进平衡

理想平衡:由n个节点组成的二叉树,高度不低于log2n,达到该下界时称作理想平衡

大致相当于完全树甚至满树:叶节点只能出现与最底部的两层——条件过于苛刻

渐近平衡:高度渐近地不超过O(logn),即可接受

渐进平衡的BST,简称平衡二叉搜索树

image-20221028000252471

3. 等价变换

各种BBST都可视作BST的某一子集,相应地满足精心设计的限制条件

单次动态修改操作后,至多O(logn)处局部不再满足限制条件(可能相继违反,未必同时) 可在O(logn)时间内,使局部重新满足

 

6.5 AVL树

平衡因子Balance Factor: balFac(v)=height(lc(v))height(rc(v)) vAVL,|balFac(v)|1

AVL树未必理想平衡,但必然渐近平衡

渐近平衡

高度为h的AVL树,至少包含S(h)=fib(h+3)1个节点

image-20221028095256425

image-20221028095410997

重平衡

失衡

插入:从祖父开始,每个祖先都有可能失衡,且可能同时失衡,但只需调整一次 删除:从父亲开始,每个祖先都有可能失衡,且至多一个,但可能需要调整多次

image-20221028100116302

重平衡

局部性:所有旋转都在局部进行 //每次只需O(1)时间 快速性:在每一深度只需检查并旋转至多一次 //共O(logn)

1. 插入

同时可有多个失衡节点,最低者g不低于x的祖先 g经单旋调整后复衡,子树高度复原 更高祖先也必平衡,全树复衡

2. 删除

同时至多一个失衡节点g,首个可能就是x的父亲_hot 复衡后子树高度未必复原,更高祖先仍可能随之失衡 失衡可能持续向上传播,最多需做O(logn)次调整

3. (3+4)-重构

设g为最低的失衡节点,沿最长分支考察祖孙三代:g~p~v,按中序遍历次序,重命名为:a < b < c 它们总共拥有四个子树(或为空),按中序遍历次序,重命名为:T0 < T1 < T2 < T3 将原先以g为根的子树S,替换为一棵新子树S` 等价变换,保持中序遍历次序:T0 < a < T1 < b < T2 < c < T3

image-20221028104706798

统一调整

image-20221104004757249

AVL:综合评价

优点 无论查找、插入或删除,最坏情况下的复杂度均为O(logn) O(n)的存储空间 缺点 借助高度或平衡因子,为此需改造元素结构,或额外封装 实测复杂度与理论值尚有差距 插入/删除后的旋转,成本不菲 删除操作后,最多需旋转Ω(logn)次(Knuth:平均仅0.21次) 若需频繁进行插入/删除操作,未免得不偿失 单次动态调整后,全树拓扑结构的变化量可能高达Ω(logn)

 

7 BST Application

Range Query

Counting and Reporting

 

disjoint subtrees

1. kd-Tree: 1D

结构:a complete (balanced) BST

v,v.key=min{u.key|uv.rTree}=v.succ.key uv.lTree, u.key<v.key, uv.rTree, u.keyv.key

search(x): return the MAXIMUM key not greater than x

image-20221104010738145

disjoint subtrees

Starting from the LCA (lowest common ancestor), traverse path(16) and path(78) once more resp. All R/L-turns along path(16)/path(78) are ignored and the R/L subtree at each L/R-turn is reported

image-20221104011241053

Complexity: Query: O(logn) Preprocessing: O(nlogn) Storage: O(n)

2. kd-Tree: 2D

To extend the BBST method to planar GRS, we - divide the plane recursively and - arrange the regions into a kd-tree

image-20221104012104036

Definition: each region is defined to be open/closed on the left-lower/right-upper sides

image-20221104012221139

Example:

image-20221104012345971

Quadtree

image-20221104012530961

each node => each rectangular sub-region of the plane each of these subsets is called a canonical subset for each internal node X with children L and R, region(X)=region(L)  region(R) sub-regions of nodes at a same depth never intersect with each other and their union covers the entire plane

3. Complexity

 

Multi-Level Search Tree

2D Range Query = x-Query * y-Query

image-20221104014924718

Complexity: Query Time = O(r+log2n)O(r+logn)

2-level search tree: build-tree in O(nlogn) time each input point is stored in O(logn) y-trees needs O(nlogn) space

x-range query needs O(logn) time to locate the O(logn) nodes then for each of these nodes, a y-range search is invoked, which needs O(logn) time

image-20221104015055700

Beyond 2D: a d-level tree for S uses O(n·logd1n) storage can be constructed in O(n·logd1n) time each orthogonal range query of S can be answered in O(r+logdn) time

 

Range Tree

 

Interval Tree

Stabbing Query:

Given a set of intervals in general position on the x-axis: S={si=[xi,xi|1in]} and a query point qx Find all intervals that contain qx: {si=[xi,xi]|xiqxxi}

image-20221104095533214

1. Partitioning

Median: Let P=S be the set of all endpoints Let xmid=median(P) be the median of P

image-20221104095732756

max{|Sleft|,|Sright|}n/2 Best case: |Smid|=mid Worst case: |Smid|=1

2. Construction

Complexity: O(nlogn) Storage: O(n)

Hint: avoid repeatedly sorting

each segment appears twice (one in each list)image-20221104100910184

3. queryIntervalTree

image-20221104100425240

Query Time: O(r+logn)

 

Segment Tree

Stabbing Query

Discretization 离散化

Let I={[xi,xi]|i=1,2,3,...,n} be n intervals on the x-axis sort all the endpoints into p1,p2,p3,...,pm,m2n m+1 elementary intervals are hence defined as: (,p1],(p1,p2],...,(pm1,pm],(pm,+)

image-20221104102400766

Worst Case: every interval spans Ω(n) EI's and a total space of Ω(n2) is required

image-20221104102614271

Sorted Vector --> BBST

For each leaf v, denote the corresponding elementary interval as R(v) //range of domination denote the subset of intervals spanning R(v) as Int(v) and store Int(v) at v

To find all intervals containing qx, we can find the leaf v whose R(v) contains qx //O(logn) time for a BBST and then report Int(v) //O(1+r) time

image-20221104103019333

Store each interval at O(logn) common ancestors by greedy merging

Canonical Subsets with O(nlogn) Space

image-20221104103852475

1. BuildSegementTree

Sort all endpoints in I before determining all the EI's:O(nlogn) Create T a BBST on all the EI's:O(n) Determine R(v) for each node vO(n) if done in a bottom-up manner For each s of I, InsertSegment( T.root, s )

2. Query

Complexity:O(r+logn) Only one node is visited per level, altogether O(logn) nodes At each node v the CS Int(v) is reported in time: 1+|Int(v)|=O(1+rv) Reporting all the intervals: costs O(r) time

3. Conlusion

a segment tree of size: O(nlogn) can be built in O(nlogn) time which reports all intervals containing a query point in O(r+logn) time

 

8 高级搜索树

存储器现状

CPU Register: [cycles] = 0, [sec] = ns SRAM/cache: [cycles] = 4~75, [sec] = ns DRAM/main memory: [cycles] = 102, [sec] = ns DISK: [cycles] = 107, [sec] = ms

不同类型的存储器,容量、访问速度差异悬殊 实际运行时间主要取决于:相邻存储级别之间数据传输(I/O)的速度次数

Definition(内存 / 外存):所有相对于当前存储器级别更低的都叫做“外存”,反之称为“内存”

分级存储:批量访问 (page)或(block)为单位,借助缓冲区,可大大缩短字节的平均访问时间

image-20221116083501460

8.1 B树

基本结构

d代合并为超级节点m=2d路,m1个关键码——逻辑上与BBST完全等价

image-20221116084419994

I/O优化:多级存储系统中使用B树,可针对外部查找,大大减少I/O次数 普通AVL:若有n=1G个记录,每次查找需要log210930次I/O操作 B树:充分利用外存的批量访问,每下降一层,都以超级节点为单位,读入一组关键码 目前多数数据库系统采用m=200300。取m=256,则每次查找只需要log2561094次I/O

([m/2],m)-树:以(2,4)-树为例

各节点的分支数,可能是2,3或4 各节点所含key的数目,可能是1,2或3

image-20221118223928178

1. 查找

image-20221118224313210

最大树高:含N个关键码的m阶B树,高度hO(logmN)

推导:logm(N+1)h1+[logm/2N+12] m=256,树高约降低至17 ~ 18

2. 插入

分裂:关键码上升一层,并分裂以所得的两个节点为左右孩子

image-20221118225012155

再分裂:若上溢节点的父亲本就饱和,则在接纳被提升的关键码之后也将上溢,逐层向上传播,总体执行时间正比于分裂次数O(h)

分裂至根节点:B树高度增加,且新生的树根仅有两个分支

3. 删除

(2,3)-树:底层节点

image-20221118230224363

(2,3)-树:非底层节点

image-20221118230328455

代码实现

image-20221118230518296

思考:B树的插入删除优先策略非对称(插入:split,删除:rotate 而不是 merge)

 

8.2 红黑树 Red-Black Tree

并发性 Concurrent Access To A Database: 修改之前先加锁(lock);完成后解锁(unlock) 访问延迟主要取决于“lock/unlock”周期 对于BST而言,每次修改过程中,唯结构有变(reconstruction)处才需加锁 访问延迟主要取决于这类局部之数量 Red-Black Tree保证无论insert / remove,结构变化均不超过O(1)

image-20221123233042281

持久性 Persistent Structures:支持对历史版本的访问 Partial Persistence:仅支持对历史版本的读取 每个版本的新增复杂度,仅为O(logn),甚至O(1)...!

1. 结构

由红、黑两类节点组成的BST,统一增设外部节点NULL,使之成为真二叉树

规则

  1. 树根:必为黑色
  2. 外部节点:均为黑色
  3. 红节点:只能有黑孩子(及黑父亲)
  4. 外部节点:黑深度(黑的真祖先数目)相等 亦即根(全树)的黑高度 子树的黑高度,即后代NULL的相对黑深度

(一种直观理解方式:)

image-20221124131642656

于是,每棵红黑树都对应于一棵(2,4)-树:将黑节点与其红孩子视作关键码,再合并为B-树的超级节点(无非四种组合)

image-20221124141022135

红黑树 BBST

2. 插入

首先按照BST规则插入关键码e,首先将x染红,可能违反规则3:双红 (double-red) 考察:祖父g = p->parent和叔父u = uncle(x) = sibling(p)

image-20221124143644879

3. 删除

首先按照BST常规算法,执行r = removeAt(x, _hot) x由孩子r接替,此时另一孩子k必为NULL。假想将另一孩子k理解为一棵黑高度与r相等的子树,且随x一并摘除。 可能违反规则3、4: x为红,则自然满足 r为红,则令其与x交换颜色即可 xr双黑 (double black),摘除x并代之以r后,全树黑深度不再统一(相当于B-树中x所属节点发生下溢,考察r的父亲p = r->parent、兄弟s = sibling(r)

image-20221124152249409

image-20221124152537623

 

8.3 伸展树 Splay Tree

局部性 / Locality:刚被访问的数据,极有可能很快再次被访问

伸展树:

自适应链表 self-adjusting list:节点一旦被访问,随即移到最前端 伸展树 self-adjusting binary tree:逐层伸展,使得BST的节点一旦被访问,随即调整到树根(或其附近),(实现方法:zig + zag)

Worst-Case: 倒序访问退化为链表的顺序树,Ω(n2),分摊Ω(n)

1. 双层伸展

反复考察祖孙三代,根据它们的相对位置,经两次旋转,使v上升两层,称为(子)树根 (下图中第一行为“俗”办法,第二行为“好”办法)

image-20221123083804999

这种结构可以使得最坏的情况并不持续性发生(如下图所示,节点访问之后,对应路径的长度随机折半)。在这种情况下,伸展操作分摊仅需O(logn)

image-20221123084349873

要是v只有父亲,没有祖父,此时必有v.parent()==T.root(),只需要做单次旋转。但是这种情况最多在最后出现一次

2. 数学证明

定义伸展树S的势能:

ϕ(S)=log(ΠvSsize(v))=vSlog(size(v))=vSrank(v)=vSlogV

直觉:越平衡/倾斜的树,势能越/

单链:ϕ(S)=logn!=O(nlogn) 满树:ϕ(S)=O(n)

image-20221123085951320

考察对伸展树Sm>>n次连续访问,记A(k)=T(k)+ϕ(k),k=0,1,2...m 则有:AO(nlogn)T=AϕA+O(nlogn) 故若能证明A=O(mlogn),则必有T=O(mlogn)

尽管T(k)的变化幅度可能很大,我们可以证明:A(k)都不致超过节点v的势能变化量,即:O(rank(k)(v)rank(k1)(v)=O(logn))

事实上,A(k)不过是v的若干次连续伸展操作的时间成本的累计,这些操作无非三种情况...

image-20221123091224862

image-20221123091252465

image-20221123091324936

综合评价 局部性强、缓存命中率极高时(即k<<n<<m

效率甚至可以更高——自适应的O(logk) 任何连续的m次查找,仅需O(mlogk+nlogn)时间

反复顺序访问任一子集,分摊成本仅为常数 不能杜绝单次最坏情况,不适用于对效率敏感的场合

3. 算法实现

伸展算法(举例:zig-zig):

image-20221123230347284

  1. 查找算法

    伸展树的查找, 与常规BST::search()不同:很可能会改变树的拓扑结构,不再属于静态操作

  2. 插入算法

    直观方法:先调用标准的BST::search(),再将新节点伸展至根 Splay::search()已集成splay(),查找失败之后, _hot即是根 既如此,何不随即就在树根附近接入新节点?

    image-20221123230739893

  3. 删除算法

    直观方法:调用BST标准的删除算法,再将_hot伸展至根 注意到,Splay::search()成功之后,目标节点即是树根 既如此,何不随即就在树根附近完成目标节点的摘除...

    image-20221123231609539

     

9 词典

9.1 散列 / Hash

循对象访问

entry = (key, value)

Map / Dictionary: 词条的集合 关键码禁止/允许雷同 get(key), put(key, value), remove(key)

关键码未必可定义大小,元素类型较BST更多样 查找对象不限于最大/最小词条,接口功能较PQ更强大

Notation

R: real-world scenario (可能的所有rank,e.g. 可能的所有合法电话) N: 实际使用的rank,e.g.实有的固定电话

散列表/散列函数

桶(bucket):直接存放或间接指向一个词条

Bucket array ~ Hashtable 容量:M 满足:N<M<<R 空间:O(N+M)=O(N)

定址/杂凑/散列 根据词条的key(未必可比较)“直接”确定散列表入口(无论表有多长)

散列函数hash(): key |-> &entry “直接”expectedO(1)O(1)

image-20221109081309828

1. 冲突

装填因子(load factor):λ=N/M

λ越大,空间利用率越高,冲突的情况越严重 通过降低λ,冲突程度将会有所改善,但只要数据集在动态变化,就无法彻底杜绝

完美散列(perfect hashing):实现单射的散列 采用两级散列模式 仅需O(n)空间 关键码之间互不冲突 最坏情况下的查找时间也不过O(1)

装填因子确定之后, 散列策略的选取将至关重要, 散列函数的设计也很有讲究

2. 设计Hash算法

image-20221109084225664

image-20221109085105084

3. 处理冲突:开放散列

Open Hashing (necessarily closed addressing)

多槽位
公共溢出区
独立链
4. 处理冲突:封闭散列

Closed Hashing (necessarily open addressing): 只要有必要,任何散列桶都可以接纳任何词条

开放定址
懒惰删除
重散列
平方试探
双向平方试探
双散列 Double Hashing

预先约定第二散列函数:hash2(key,i) 冲突时,由其偏移增量,确定下一试探位置:[hash(key)+i=1khash2(key,i)]%M

  • 线性试探:hash2(key,i) 1
  • 平方试探:hash2(key,i)=2i1

 

9.2 桶排序

1. 基本算法实现
  1. [0,m)n(<m)互异的整数借助散列表H[]进行排序

空间:O(m) 时间:O(n)

初始化:for i = 0 to m - 1, let H[i] = 0 映射:for each key in the input, let H[key] = 1 枚举:for i = 0 to m - 1, output i if H[i] = 1

image-20221115211156357

  1. 允许重复(可能m<<n),依然可以使用Hash——没一簇同义词各成一个链表

空间:O(m+n) 时间:O(n)

image-20230213235848273

2. 最大缝隙 MaxGap

任意n个互异点均将实轴氛围n1段有界区间,其中的哪一段最长

线性算法

image-20221115211716762

 

9.3 基数排序 Radixsort

有时,关键码由多个组成: kd , kt-1 , ... , k1 // (suit, point) in bridge 若将各域视作字母,则关键码即单词——按词典的方式排序(lexicographic order)

  1. 自k1到kt低位优先),依次以各域为序做一趟桶排序

image-20221115212327114

image-20221115212720713

 

9.4 跳转表 Skip List

image-20221223102400492

空间性能 逐层随机减半Sk中的每个关键码,在Sk+1中依然出现的概率,均为p=1/2 各塔的高度符合几何分布Pr(h=k)=pk1·(1p) 期望的塔高:E(h)=11p=2,即期望空间总和为expectedO(n)

1. 查找

时间复杂度:O(logn)

时间性能 查找时间取决于横向、纵向的累计跳转次数 整张表的期望高度为O(logn),查找中纵向跳转的次数,累计expectedO(logn) (每一个塔的期望高度为O(2)=O(1) 事实上,查找中横向跳转的次数也为O(logn)

image-20221223105355328

image-20221223105625490

image-20221223105729582

2. 插入与删除

时间复杂度:O(logn)

image-20230214115852744

 

 

 

10 图 Graph

G=(V;E)

10.1 基本概念

节点vertex: n=|V| edge|arc: e=|E|

同一条边的两个顶点——彼此邻接(adjacency) 同一顶点自我邻接——构成自环(self-loop) 不含自环及重边——即为简单图(simple graph) 非简单图(non-simple)暂不讨论

顶点与其所属的边——彼此关联(incidence) (degree/valency)——与同一顶点关联的边数

若邻接顶点u和v的次序无所谓,则(u, v)为无向边(undirected edge) 所有边均无方向的图,即无向图(undigraph) 反之,有向图(digraph)中均为有向边(directed edge)。u,v分别称作边(u, v)的(tail)和(head) 无向边、有向边并存的图,称作混合图(mixed graph)

image-20221125113827635

路径 π=<v0,v1,...,vk>长度 |π|=k 简单路径 vivj 除非 i=j 环/环路vo=vk 有向无环图(DAG) 欧拉环路|π|=|E|,各边恰好出现一次 哈密尔顿环路|π|=|V|,各顶点恰好出现一次

image-20221125114151505

G=(V;E)的子图T=(V;F)若是树,则为其支撑树(spanning tree) 同一图的支撑树,通常并不唯一 各边e均有对应的权值wt(e),则为带权网络(weighted network) 同一网络的支撑树中,总权重最小者为最小支撑树(MST)

1. 邻接矩阵 adjacency matrix

记录顶点之间的邻接关系——对应:矩阵元素(图中可能存在的边)

既然只考察简单图,对角线统一设置为0 空间复杂度为Θ(n2),与图中实际的边数无关

image-20221125114844340

image-20221125115439133

静态操作
  1. 顶点的读写

  2. 边的读写

  3. 邻点的枚举

    image-20221125115651464

动态操作
  1. 边的插入

  2. 边的删除

  3. 顶点插入

    image-20221125115819587

  4. 顶点删除

性能分析
2. 关联矩阵 incidence matrix

记录顶点之间的关联关系——对应:矩阵元素(每条边的两个节点)

空间复杂度为Θ(n×e)=O(n3) 空间利用率=2ene=2n 解决某些问题时十分有效

image-20221125115056279

3. 邻接表
空间复杂度
时间复杂度

image-20221125120331931

 

10.2 广度优先搜索 BFS

Breadth-First Search

相当于树的层次遍历 事实上,BFS也的确会构造出原图的一棵支撑树(BFS tree)

image-20221125120954603

1. 连通分量 + 可达分量

问题:给定无向图,找出其中任一顶点s所在的连通图 给定有向图,找出源自其中任一顶点s的可达分量

算法:从s出发做BFS,输出所有被发现的顶点,队列为空后立即终止

2. 边分类
3. BFS树 / 森林

image-20221125122231921

4. 最短路径

无向图中,顶点v到u的举例记作dist(v,u) BFS树中从s到v的路径,即是二者在原图中的最短通路

image-20221125122442861

5. Erdös Number: collaborative distance

image-20221125122649948

6. Bipartite Graph (Bigraph)

image-20221125122722713

7. Eccentricity / Radius / Diameter / Center

image-20221125122748905

8. Knights of the Round Table

image-20221125122820618

 

10.3 深度优先搜索 DFS

Depth-First Search

相当于树的先序遍历 事实上,DFS也的确会构造出原图的一棵支撑树(DFS tree)

image-20221126112612052

image-20221126112735585

1. 连通分量 + 可达分量
2. DFS树 / 森林

从顶点s出发的DFS 无向图中将访问与s连通的所有顶点(connectivity) 有向图中将访问由s可达的所有顶点(reachability)

所有DFS树构成DFS森林

活跃期active[u]=(dTime[u],fTime[u]) Lemma:给定有向图G=(V,E)及其任一DFS森林,则 u是v的后代 iff active[u]active[v] u与v无关 iff active  active[v]= 仅凭status[], dtime[]ftime[]即可对各边分类...

边分类

image-20221126113051874

image-20221126113024451

image-20230214231614127

image-20230214231706247

3. 遍历算法应用举例

image-20221126113211449

 

拓扑排序

有向无环图:Directed Acyclic Graph

任给有向图G(不一定是DAG),尝试将所有顶点排成一个线性序列,使其次序与原图相容(每一顶点都不会通过边指向前驱顶点)

若原图存在回路(即并非DAG),检查并报告。否则,给出一个相容的线性序列

策略顺序输出零度顶点

image-20221130082144640

零出度算法逆序输出零度顶点

(基于DFS,借助栈S) 对图G做DFS,其间 每当有顶点被标记为VISITED,则将其压入S 一旦发现有后向边,则报告“NOT_A_DAG”并退出 DFS结束后,顺序弹出S中的各个顶点 各节点按ftime逆序排列,即是拓扑排序 复杂度与DFS相当,也是O(n+e)

image-20221130083038821

 

10.4 优先级搜索 PFS

不同顶点选取策略,取决于存放和提供顶点的数据结构——Bag 此类结构,为每个顶点v维护一个优先级数——priority(v) 每个顶点都有初始优先级数,并可能随算法的推进而调整 通常的习惯是,优先级数越/,优先级越/

复杂度

image-20221130085102408

 

10.5 Dijkstra算法:最短路径

Dijkstra:SSSP / Single-Source Shortest Path 给定顶点s,计算s到其余各个顶点的最短路径及长度 Floyd-Warshall:APSP / All-Pairs Shortest Path 找出每对顶点u和v之间的最短路径及长度

1. 最短路径树

Lemma:任一最短路径的前缀,也是一条最短路径

uπ(v) only if π(u)π(v)

image-20221130085709972

消除歧义

各边权重均为正,否则 有可能出现总权重非正的环路 以致最短路径无从定义 负权重的边时,即便多有环路总权重皆为正,Dijkstra算法依然可能失效 任意两点之间,最短路径唯一 在不影响计算结果的前提下,总可通过适当扰动予以保证(*)

Shortest Path Tree

image-20221130090239062

2. 算法实现

vVk, let priority(v)=||s,v|| 于是套用PFS框架,为将Tk扩充至Tk+1,只需 选出优先级最高的跨边ek及其对应顶点uk,并将其加入Tk 随后,更新V or Vk+1中所有顶点的优先级(数) 注意:优先级数随后可能改变(降低)的顶点,必与uk邻接 因此,只需枚举uk的每一邻接顶点v,并取 priority(v)=min(priority(v),priority(uk)+||uk,v||) 以上完全符合PFS的框架,唯一要做的工作无非是按照prioUpdater()规范,编写一个优先级(数)更新器...

image-20221130090955356

 

10.6 Floyd-Warshall算法:最短路径

给定带权网络G,计算其中所有点对之间的最短距离 应用:确定G的中心点(center) radius(G, s) = s的半径 = 所有顶点到s的最大距离 中心点 = 半径最小的顶点s 直觉:依次将个顶点作为源点,调用Dijkstra算法 时间 = n×O(n2)=O(n3) 思路:图矩阵 ~ 最短路径矩阵 效率O(n3),与执行n次Dijkstra相同 优点:形式简单、算法紧凑、便于实现;允许负权边(但不允许负权环路

1. 思路

image-20221201180432793

动态规划

image-20221201180642402

 

10.7 Prim算法:最小支撑树 MST

连通网络N=(V;E)的子图T=(V;F) 支撑 / spanning = 覆盖N中所有顶点 树 / tree = 连通且无环,|F|=|V|1 同一网络的支撑树,未必唯一 minimum = optimal: w(T)=eFw(e)达到最小

image-20221201131959571

1. Prim算法:极短跨边

任何环路C上的最长边f,都不会被MST采用 否则,移除f之后,MST将分裂为两棵树,将其视作一个,则C上必有该割的另一跨边e,既然|e|<|f|,那么只要用e替换f,就可以的到一棵总权重更小的支撑树

若uv是某一割的一条极短跨边,则必存在一棵包含uv的MST 任一MST都必然通过极短跨边连接每一

image-20221201132510056

递增式构造Tk+1=(Vk+1;Ek+1)=(Vk){vk+1};Ek{vk+1u}),其中uVk

注意:当MST不唯一时,由极短跨边构成的支撑树,未必就是一棵MST

反例:

image-20221201133018685

可行的证明 在不增加总权重的前提下,可以将任一MST转换为T,每一Tk都是某棵MST的子树

image-20221201133213109

2. Prim算法:实现

于是套用PFS框架,为将Tk扩充至Tk+1,只需 选出优先级最高的跨边ek及其对应顶点uk,并将其加入Tk 随后,更新V|Vk+1中所有顶点的优先级(数) 注意:优先级数随后可能改变(降低)的顶点,必与uk邻接 因此,只需枚举uk的每一邻接顶点v,并取 priority(v)=min(priority(v),||uk,v||) 以上完全符合PFS的框架,唯一要做的工作无非是按照prioUpdater()规范,编写一个优先级(数)更新器...

image-20221201142702393

 

10.8 Kruskal算法

贪心原则 根据代价,从小到大依次尝试各边 只要“安全”就加入该边 (每步局部最优 =全局最优)

1. 正确性

定理:Kruskal引入的每条边都属于某棵MST 设:边e=(u,v)的引入导致树T和S的合并 若:将(T;V|T)视作原网络N的割,则e当属于该割的一条跨边 在确定引入e之前,该割的所有跨边都经Kruskal考察,且只可能因不短于e而被淘汰 故:e应当是该割的一条极短跨边

Kruskal算法过程中不断生长的森林,总是某棵MST的子图

image-20221201172909376

2. 并查集

并查集 - OI Wiki (oi-wiki.org)

Union-Find问题: 给定一组互不相交的等价类 各由其中一个成员作为代表

Find(x):找到元素x所属等价类 Union(x, y):合并x和y所属等价类 Singleton:初始时各包含一个元素

Kruskal = Union-Find

image-20221201173357016

 

11 优先级队列

stack和queue都是PQ的特例——优先级完全取决于元素的插入次序 steap和queap也都是PQ的特例——插入和删除的位置受限

引入
1. Vector

image-20221202095959184

Sorted Vector:

image-20221202100104584

2. List

image-20221202100131975

Sorted List:

image-20221202100222921

3. BBST

AVL、Splay、Red-black:三个接口均只需O(logn)时间

但是BBST的功能远远超出了PQ的需求: 若只需查找极值元,则不必维护所有元素之间的全序关系,偏序足矣 因此有理由相信,存在某种更为简单、维护成本更低的实现方式

 

11.1 完全二叉堆

结构性:逻辑元素、物理节点依层次遍历次序彼此对应 逻辑上等同于完全二叉树 物理上直接借助向量实现 内部节点的最大秩 = [n12]=[n32]

image-20221202101024411

PQ_ComplHeap = PQ + Vector

堆序性:只要0<i,必然满足H[i]H[Parent(i)],故H[i]即是全局最大

1. 插入

算法:逐层上滤

image-20221202101740156

效率

e在上滤过程中,只可能与祖先们交换 完全树必平衡,e的祖先不超过O(logn)个,因此时间复杂度在O(logn) 然而,就数学期望而言,实际效率往往远远更高

2. 删除

算法:割肉补疮 + 逐层下滤

image-20221202102418905

效率

e在每一高度至多交换一次,累计交换不超过O(logn) 通过下滤,可在O(logn)时间内 删除堆顶节点,并整体重新调整为堆 然而,就数学期望而言,实际效率往往远远更高

3. 批量建堆

蛮力算法

image-20221202104420821

Worst Case = O(nlogn)

自下而上的下滤

任意给定堆H0H1,以及节点P 未得到堆H0{p}H1,只需将r0r1当作p的孩子,再对p下滤

image-20221202104809626

Example

image-20221202105240846

效率

每个内部节点所需的调整时间,正比于其高度而非深度 不失一般性,考察满树:n=2h+11 所有节点的高度总和:

image-20221202105418041

算法优化的本质:堆结构“深度”和“高度”的不确定性。采用高度累计减少复杂度。

课后思考

image-20221202105541838

 

11.2 堆排序

对比选择排序SelectionSort() 初始化:heapify(), O(n) 迭代:delMax(), O(logn) 不变性:HS

O(n)+n×O(logn)=O(nlogn) ——J. Williams, 1964

image-20221202110105413

image-20221202110211586

1. 锦标赛树:胜者树

完全二叉树 叶节点:待排序元素(选手) 内部节点:孩子中的胜者 树根总是全局冠军:类似于堆顶 内部结点各对应于一场比赛的胜者——重复存储

create(): O(n) remove(): O(logn) insert(): O(logn)

Tournamentsort() CREATE a tounament tree for the input list while there are active leaves RMOVE the root RETRACE the root down to its leaf DEACTIVATE the leaf REPLAY along the path back to the root

image-20221207081243645

效率 空间:O()=O()=O(n) 构造:仅需O(n)时间 更新:每次只需要重排上一优先者的祖先 为此,只需从其所在叶节点除法,逐层上溯直到树根 如此,为确定各轮优胜者,总共所需时间仅O(logn) 时间:n×O(logn)=O(nlogn)

选取:从n个元素中找出最小的k个 初始化:O(n) 迭代k步:O(klogn),与小顶堆旗鼓相当?

渐进意义上确实如此,但就常系数而言,区别不小 Floyd算法,delMax()中的percolateDown()在每一层需做2次比较,累计大致2logn

2. 锦标赛树:败者树

内部节点记录对应比赛的败者 增设根的“父节点”,记录冠军 ——这样的算法设计是因为胜者树的REPLAY涉及“找兄弟比赛”的过程(时间上就有消耗) 而采用败者树REPLAY时每次的比赛只需要找父亲即可

image-20221207082039411

image-20221207082440453

image-20221207082501743

image-20230216230240368

 

 

11.3 多叉堆

背景:优先级搜索(例如Prim和Dijkstra算法)中,若采用邻接表,更新优先级和选出优先级最高者的累计时间分别为O(n+e)O(n2)。能否更快?

dsa

优先级队列对pfs的改进

PFS中的各顶点可以组织为优先级队列 为此需要使用PQ接口: heapify():由n个顶点创建初始PQ,总计O(n) delMax():取优先级最高 / 极短跨边(u, w),总计O(nlogn) increase():更新所有关联顶点到u的距离,提高优先级,总计O(elogn) 总体运行时间 = O((n+e)logn)

对于稀疏图,处理效率很高 对于稠密图,反而不如常规实现的版本

多叉堆多优先级队列的改进

给予Vector实现,父子节点的可以简明地互相换算 parent(k)=[(k1)/d] child(k,i)=k·d+i,0<id heapify()O(n),不可能再快了 delMax()O(logn),实质就是perculateDown(),已是极限 increase()O(logn),实质就是perculateUp(),还有改进空间...

二叉堆改成多叉堆(d-heap) 堆高度降至O(logdn) 上滤 / perculateUp()成本降至O(logdn) 下滤 / perculateDown()成本增至(只要d>4d·logdn=lnnlndd

image-20221202160932473

则此时PFS时间复杂度为n·dlogdn+e·logdn=(n·d+e)·logdn den+2时,总体性能达到最优O(e·log(en+2)n)

对于稀疏图保持高效:e·logen+2nn·lognn+2n=O(nlogn) 对于稠密图改进较大:e·logen+2nn2·logn2n+2nn2=O(e) 对于一般的图,会自适应地实现最优

 

11.4 左式堆

1. 堆合并

H=merge(A,B):将堆A和B合二为一

方法一A.insert(B.delMax()) O(m×(logm+logm(n+m)))=O(m(log(n+m))) 方法二union(A, B).heapify(n + m) O(m+n)

二堆合并 = 二路归并

放弃“完全性”(completeness),保留“堆序性”

image-20221207083204281

image-20221207083257543

image-20221207083538380

递归实现:所需时间正比于右侧藤总长O(logn) 可以保证藤长不超过logn...但是一次合并之后藤长变为2logn...

image-20221207084019933

2. NPL与控制藤长

C. A. Crane, 1972: 保持堆序性,附加新条件,使得在堆合并过程中,只涉及少量节点:O(logn)

新条件 = 单侧倾斜 节点分布偏向 合并操作只涉及

image-20221207084337631

空节点路径长度

引入所有的外部节点 消除一度节点,转为二叉树

Null Path Length npl(NULL)=0 npl(x)=1+min{npl(lc(x)),npl(rc(x))}

验证:npl(x)=x=x

image-20221207084626082

左式堆

对任何内节点x,都有:npl(lc(x))npl(rc(x)) 推论:npl(x)=1+npl(rc(x))

左倾性与堆序性,相容而不矛盾 左式堆的子堆必然也是左式堆 左式堆倾向于更多节点分布于左侧分支

右侧链

rChain(x):从节点x出发,一直沿右分支前进 特别地,rChain(r)的终点,即全堆中最浅的外部节点 npl(r)=|rChain(r)|=d 存在一棵以r为根,高度为d的满子树

右侧链长为d的左式堆,至少包含 2d1个内部节点 2d+11个节点 反之,包含n个节点的左式堆,右侧链长度d[log2(n+1)]1=O(logn)

image-20221207085934491

3. 合并算法

image-20221207090046631

image-20230217144841482

4. 插入与删除

 

 

12 串

image-20221207092231634

image-20221207092252205

 

12.1 模式匹配

Text: 串,长度记为n Pattern: 模式子串,长度记为m 记字符集为,则字符集的个数s=||

蛮力算法 Best Case: Ω(n) (绝大概率情况下都是Best Case) Worst Case: O(n·m)(概率比较低)

image-20221207092842673

Version 1:

image-20221207093036456

Version 2:

1. KMP算法

在任意时刻,都有T[ij,i)==P[0,j) 此时,我们已经掌握了T[ij,i)所有信息 因此,一旦失败,就应该已知哪些位置值得/不必对齐,则在下一轮比对中,T[ij,i)直接接收,而不必再做比对

image-20221209095954975

由此,i将永远不必回退 比对成功,则与j同步前进一个字符 否则,j更新为某一更小的t,比继续比对

优化 = P可快速右移 + 避免重复比对

提问:为确定t,需花费多少时间和空间?更重要的,可否在事先就确定?

查询表

t:不仅可以事先确定,而且仅根据P[0,j)=T[ij,i)即可确定 视失败的位置j,无非m种情况 构造查询表next[0,m),做好预案 一旦在P[j]处失配,只需将j替换为next[j],继续与T[i]比对

image-20221209100557014

image-20221209101054261

串首补齐一个通配符*

快速右移,绝不回退

image-20221209101451242

next表

j1,N(P,j)={0t<j|P[0,t)=P[jt,j)} P在t处自匹配 next[j]=max{N(P,j)} 长度最大,位移最小,不致日后回溯

image-20221209103540302

传递链:动态规划

image-20221209104215047

分摊分析

image-20221209110103762

尽管如此,“黄色块”不会一直出现,其总的分摊复杂度并不高

while循环前,令计步器k = 2 * i - j(初始有k = 0),代表迭代步数的上界 则算法结束时:k=2ij2(n1)(1)=2n1

因此,总体时间复杂度为O(n+m)

再分析

反例

image-20221209111201297

image-20221209111932365

渐近复杂度仍然是线性的。

image-20230517110427553

小结 充分利用以往的比对所提供的信息:模式串快速右移,文本串无需回退 经验 ~ 以往成功的比对:T[ij,i)什么 教训 ~ 以往失败的比对:T[i]不是什么 特别适用于顺序存储介质 单词匹配概率越(字符集越),优势越明显(比如二进制串) 否则,可能与蛮力算法的性能相差无几

2. BM算法
BC策略:以终为始

image-20221214081053750

自左向右移动P,自右向左比对P和T

Boyer + Moore, 1977: A Fast String Searching Algorithm 预处理:根据模式串P,预先构造gs[]表和bc[] 迭代:自右向左依次比对字符,找到极大的匹配后缀 若完全匹配,则返回位置; 否则,查表确定P右移的适当位置,并重新自右向左比对

image-20221214081434169

坏字符

image-20221214081757587

构造bc[]:画家算法

image-20221214082156880

image-20221214082139988

性能分析

GS策略:好后缀

image-20221214085133869

[EG]:

image-20221214085515963

构造gs[]

image-20221214090654270

image-20221214090800833

image-20221214090732265

BC+GS综合性能

image-20221214085758404

3. Karp-Rabin算法:串即是数

凡物皆数:Godel Numbering 逻辑系统的符号、表达式、公式、命题、定理、公理等,均可表示为自然数 每个有限维的自然数向量(包括字符串),都唯一对应于某个自然数

素数序列:p(k)=k=2,3,5,7,11,13,17,19...

image-20221214091232272

godel=21+7×31+15×51+4×71+5×111+12=139869560310664817087943919200000 若果真如RAM模型所假设的字长无限,则只需一个寄存器即可...

凡物皆数:Cantor Numbering

image-20221214092214903

image-20221214092254220

十进制串,可直接视作自然数:指纹(fingerprint),等效于多项式法 P=82818       T=27182818284590452353602874713527 一般地,随意对字符编号0,1,2,...,d1 //设d = |∑| 于是,每个字符串都对应于一个 d 进制自然数 //尽管不是单射 CAT=2019(26)=1371(10) //∑ = { A, B, C, ..., Z } ABBA=0110(26)=702(10) P在T中出现仅当T中某一子串与P相等

散列

基本构思:通过对比经压缩之后的指纹,确定匹配位置 关键技巧:通过散列,将指纹压缩至存储器支持的范

image-20221214095018005

注意:通过hash()筛选后,还需经过严格的比对,方可最终确定是否匹配 适当选取散列函数,可以极大降低冲突的概率

image-20221214095149356

字宽

image-20221223110725687

image-20221223111049793

image-20221223111355270

O(logn)只对n比较小的时候成立

再分析

image-20221223111548926

 

12.2 键树 Trie

image-20221223100714068

其中,红色的节点表示单词的结束节点(不一定是叶子结点)

image-20221223100735471

image-20221223101344336

image-20221223101407547

 

 

13 排序

13.1 快速排序

C. A. R. Hoare:分而治之 pivot:左侧/右侧的元素,均不比它更大/更小 以轴点为界,自然划分max([0,mi))min((mi,hi)) 前缀、后缀各自递归排序之后,原序列自然有序:sorted(S)=sorted(SL)+sorted(SR)

mergesort难点在于,quicksort在于 通过培养轴点来实现上述划分

image-20221216095800404

快速排序就是将所有元素逐个转换为轴点的过程

快速划分:LUG版

任选一个候选者(如[0]),交替向内移动lohi 逐个检查当前元素:若更小/大,则转移归入L/G lo=hi时,只需将候选者嵌入与L、G之间,即成轴点 各元素最多移动一次(候选者两次):累计时间O(n)O(1)辅助空间

image-20221216100545038

不变性 + 单调性LpivotG[lo][hi]交替空闲

image-20221216101221994

复杂度分析 线性时间:尽管lo, hi交替移动,累计移动距离不过O(n) in-pace:只需O(1)附加空间 unstable:lo/hi的移动方向相反,左/右侧的大/小重读元素可能前/后颠倒

image-20221216101451643

2. 迭代、贪心与随机

空间复杂度 ~ 递归深度 最好:划分总是均衡 O(logn) 最差:划分总是偏侧 O(n) 平均:均衡不致太少 O(logn)

image-20221216101816467

非递归 + 贪心

保证递归深度可能超过O(logn),但辅助栈空间一定不超过O(logn)

时间性能 + 随机

3. 递归深度

最坏情况(Ω(n)递归深度)概率极低 平均情况(O(logn)递归深度)概率极高

image-20221216103205134

除非过于侧片的pivot,都会有效地缩短递归深度 准居中:pivot落在宽度为λ·n的居中区间(λ也是这种情况出现的概率) 每一递归路径上,至多出现log21+λn准居中的pivots

期望深度 每递归一层,都有λ(1λ)的概率准居中准偏侧 深入1λ·log21+λn层后,即可期望出现log21+λn准居中,且有极高的概率出现 相反情况的概率<(1λ)(1λ1)·log21+λn=n(1λ1)·log21+λ(1λ),且随着λ增加而下降 至少有1n2·log32(23)=1n2的概率,使得递归深度不超过1λ·log21+λn=3·log32n5.129logn

image-20221216105951522

4. 比较次数

move的次数一定不超过compare的次数

期望的比较次数为T(n)T(1),T(2)=1,... 可以证明:T(n)=O(nlogn)

image-20221216110852154

image-20221216110914312

后向分析

设经排序后得到的输出序列为:a0,a1,a2,...ai,...,aj,...,an1 这一输出与具体使用何种算法无关,故可使用Backward Analysis 比较操作的期望次数为T(n)=i=0n1j=i+1n1Pr(i,j),亦即每一对<ai,aj>在排序过程中接收比较的概率综合

quickSort的过程及结果,可理解为:按某种次序,将各元素逐个确认为pivot k[0,i)(j,n),则ak早于或晚于aiaj被确认,均与Pr(i,j)无关

image-20221216112307263

对比

image-20221221082919331

快读划分:DUP版

大量甚至全部元素重复时,轴点位置总是接近于lo,子序列的划分极度失衡,二分递归退化为线性递归,递归深度接近于O(n),运行时间接近于O(n2)

移动lohi的过程中,同时比较相邻元素若属于相邻的重复元素,则不再深入递归

LUG'版本:勤于拓展、懒于交换

DUP'版本

DUP版本:懒于拓展、勤于交换

快速化分:LGU版

不变性:S=[lo,hi)=[lo]+(lo,mi]+(mi,k)+[k,hi)=pivot+L+G+U L<pivotG

image-20221221085523437

单调性:[k]不小于轴点 ? 直接G拓展 : G滚动后移,L拓展 pivotS[k]?k++:swap(S[++mi],S[k++])

 

13.2 快速选择 QuickSelect

期望性能
  1. 记期望的比较次数为T(n) T(1)=0,T(2)=1,...
  2. 可以证明:T(n)=O(n) T(n)=(n1)+1n×k=0n1max{T(k),T(nk1)}(n1)+2n×k=n2n1T(k)
  3. 事实上,不难验证:T(n)<4·n T(n)(n1)+2n×k=n2n14k(n1)+3n<4n

 

13.3 线性选择 LinearSelect

image-20221221091549759

复杂度

T(n)=c·n+T(n/Q)+T(3n/4) min(|L|,|G|)+|E|n4 max(|L|,|G|)3n4

Q=5T(n)=O(n) whenever 1Q+34<1

image-20221221093021880

image-20221221093106118