From bc56e5dc4e39c9bedf42c9d10360962a178c739a Mon Sep 17 00:00:00 2001 From: mbinary Date: Mon, 18 Mar 2019 01:13:50 +0800 Subject: [PATCH] Add codecog for latex formulas --- utils/codecogs.py | 45 + .../notes/mbinary/algorithm-general.md" | 138 +- .../notes/mbinary/b-tree.md" | 1218 ++++++++--------- .../notes/mbinary/fib-heap.md" | 338 +++-- .../notes/mbinary/graph.md" | 961 +++++++------ .../notes/mbinary/hashTable.md" | 73 +- .../notes/mbinary/number-theory.md" | 77 +- .../notes/mbinary/red-black-tree.md" | 20 +- .../notes/mbinary/sort.md" | 92 +- .../notes/mbinary/string-matching.md" | 136 +- .../notes/mbinary/tree.md" | 79 +- ...\263\345\213\244-csapp-bomb-lab-report.md" | 428 ------ 12 files changed, 1490 insertions(+), 2115 deletions(-) create mode 100644 utils/codecogs.py diff --git a/utils/codecogs.py b/utils/codecogs.py new file mode 100644 index 0000000..05e7bd1 --- /dev/null +++ b/utils/codecogs.py @@ -0,0 +1,45 @@ +import os +import re +import sys +from translate import Translator as TR + +FORMULA = re.compile(r'\${1,2}(?P.+?)\${1,2}',re.DOTALL) +Chinese = re.compile(u"(?P[\u4e00-\u9fa5]+)") +API = 'https://latex.codecogs.com/gif.latex?' +def codecog(f): + if os.path.exists(f) and f.endswith('.md'): + with open(f) as fp: + txt = fp.read() + with open(f,'w') as fp: + fp.write(re.sub(FORMULA,covert,txt)) + else: + s = re.sub(FORMULA,covert,f) + print(s) +def covert(matched): + s = matched.group('formula').strip('$ ') + s = re.sub(Chinese,zh2en,s) + s = re.sub(r'\r+|\n+|\\n',' ',s) + s = re.sub(' +','&space;',s) + return '![]({})'.format(API+s) +def zh2en(txt): + s = txt.group('chinese').strip() + tran = TR(to_lang='en',from_lang='zh') + en = tran.translate(s) + return re.sub(' +','-',en) + +def handle(path): + if os.path.isdir(path): + for p,ds,fs in os.walk(path): + for f in fs: + if f.endswith('.md'): + codecog(os.path.join(p,f)) + else: + codecog(path) + +if __name__ == '__main__': + args = sys.argv[1:] + if not args: + s = input('Input a file: ') + args.append(s) + for f in args: + handle(f) diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/algorithm-general.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/algorithm-general.md" index bbc33c5..298feb4 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/algorithm-general.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/algorithm-general.md" @@ -69,11 +69,11 @@ top: ![](https://upload-images.jianshu.io/upload_images/7130568-d452e7efb6fb3433.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -* $\omicron,O,\Omega,\Theta$ +* ![](https://latex.codecogs.com/gif.latex?\omicron,O,\Omega,\Theta) * 最坏情况, 平均情况 -* 增长的量级$ O(1), O(log^*n), O(logn), O(n), O(n^k), O(a^n) $ +* 增长的量级![](https://latex.codecogs.com/gif.latex?O(1),&space;O(log^*n),&space;O(logn),&space;O(n),&space;O(n^k),&space;O(a^n)) -$\log^{\*}*(\log x) = log^{\*}x-1$ +![](https://latex.codecogs.com/gif.latex?\log^{\*}*(\log&space;x)&space;=&space;log^{\*}x-1) @@ -82,11 +82,11 @@ $\log^{\*}*(\log x) = log^{\*}x-1$ ## 4.1. 分治(divide and conquer) 结构上是递归的, 步骤: 分解,解决, 合并 -eg. 快排,归并排序, 矩阵乘法(Strassen $O(log_2 7)$ +eg. 快排,归并排序, 矩阵乘法(Strassen ![](https://latex.codecogs.com/gif.latex?O(log_2&space;7)) # 5. 递归式 - $T(n) = aT(\frac{n} {b})+f(n)$ + ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;aT(\frac{n}&space;{b})+f(n)) ## 5.1. 代换法 @@ -98,74 +98,58 @@ eg. 快排,归并排序, 矩阵乘法(Strassen $O(log_2 7)$ ### 5.1.2. 例子 -$T(n) = 2T(\frac{n} {2})+n$ -猜测$T(n) = O(nlogn)$ -证明 $ T(n)\leqslant cnlogn$ +![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;2T(\frac{n}&space;{2})+n) +猜测![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;O(nlogn)) +证明 ![](https://latex.codecogs.com/gif.latex?T(n)\leqslant&space;cnlogn) 归纳奠基 n=2,3 -归纳假设 $T(\frac{n} {2}) \leqslant \frac{cn}{2}$ +归纳假设 ![](https://latex.codecogs.com/gif.latex?T(\frac{n}&space;{2})&space;\leqslant&space;\frac{cn}{2}) 递归 -$ -\begin{aligned} -T(n) &\leqslant 2c\frac{n}{2}log(\frac{n}{2}) + n \leqslant cnlog(\frac{n}{2}) \\ -\end{aligned} -$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;T(n)&space;&\leqslant&space;2c\frac{n}{2}log(\frac{n}{2})&space;+&space;n&space;\leqslant&space;cnlog(\frac{n}{2})&space;\\&space;\end{aligned}&space;) ### 5.1.3. 放缩 -对于 $T(n) = 2T(\frac{cn}{2}) + 1$ -如果 直接猜测 $T(n) = O (n)$ 不能证明, -而且不要猜测更高的界 $O (n^2)$ +对于 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;2T(\frac{cn}{2})&space;+&space;1) +如果 直接猜测 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;O&space;(n)) 不能证明, +而且不要猜测更高的界 ![](https://latex.codecogs.com/gif.latex?O&space;(n^2)) 可以放缩为 n-b ### 5.1.4. 改变变量 -对于 $ T(n) = 2T(\sqrt{n})+logn $ +对于 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;2T(\sqrt{n})+logn) 可以 令 `m = logn`, 得到 -$T(2^m) = 2T(m^{\frac{m}{2}}) + m $ -令 $S(m) = T(2^m)$ -得到 $ S(m) = 2S(\frac{m}{2}) + m $ +![](https://latex.codecogs.com/gif.latex?T(2^m)&space;=&space;2T(m^{\frac{m}{2}})&space;+&space;m) +令 ![](https://latex.codecogs.com/gif.latex?S(m)&space;=&space;T(2^m)) +得到 ![](https://latex.codecogs.com/gif.latex?S(m)&space;=&space;2S(\frac{m}{2})&space;+&space;m) -$$T(n)=T(2^m)=S(m)=\Theta(m\log m)=\Theta(\log n \log^2 n)$$ +![](https://latex.codecogs.com/gif.latex?T(n)=T(2^m)=S(m)=\Theta(m\log&space;m)=\Theta(\log&space;n&space;\log^2&space;n)) ## 5.2. 递归树 -例如 $T(n) = 3T(\frac{n}{4}) + c n^2$ +例如 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;3T(\frac{n}{4})&space;+&space;c&space;n^2) 不妨假设 n 为4的幂, 则有如下递归树 ![recursive-tree.jpg](https://upload-images.jianshu.io/upload_images/7130568-4a1b9b6ee852b725.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -$$ -T(n) = \sum_{i=0}^{ {\log_4 n}-1}cn^2*(\frac{3}{16})^i + \Theta(n^{\log4 3}) -$$ +![](https://latex.codecogs.com/gif.latex?&space;T(n)&space;=&space;\sum_{i=0}^{&space;{\log_4&space;n}-1}cn^2*(\frac{3}{16})^i&space;+&space;\Theta(n^{\log4&space;3})&space;) 每个结点是代价, 将每层加起来即可 ## 5.3. 主方法(master method) -对于 $T(n) = aT(\frac{n} {b})+f(n)$ -$$ -\begin{aligned} -T(n)=\begin{cases} -\Theta(n^{log_b a}),\quad f(n)=O(n^{ {log_b a}-\epsilon}) \\ -\Theta(n^{log_b a}logn),\quad f(n)=\Theta(n^{log_b a}) \\ -\Theta(f(n)),\quad f(n)=\Omega(n^{ {log_b a}+ \epsilon}),af(\frac{n}{b})\leqslant cf(n) \\ -\qquad \qquad \quad \text{其中常数c<1,变量n任意大} \\ -unknown, \quad others -\end{cases} -\end{aligned} -$$ +对于 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;aT(\frac{n}&space;{b})+f(n)) +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;T(n)=\begin{cases}&space;\Theta(n^{log_b&space;a}),\quad&space;f(n)=O(n^{&space;{log_b&space;a}-\epsilon})&space;\\&space;\Theta(n^{log_b&space;a}logn),\quad&space;f(n)=\Theta(n^{log_b&space;a})&space;\\&space;\Theta(f(n)),\quad&space;f(n)=\Omega(n^{&space;{log_b&space;a}+&space;\epsilon}),af(\frac{n}{b})\leqslant&space;cf(n)&space;\\&space;\qquad&space;\qquad&space;\quad&space;\text{Constantc<1,VariablenArbitrarily-large}&space;\\&space;unknown,&space;\quad&space;others&space;\end{cases}&space;\end{aligned}&space;) ### 5.3.1. 记忆 -直观上, 比较 $n^{log_b a}$ 和 $f(n)$, 谁大就是谁, -相等的话就是 $\Theta(f(n))\log n$ +直观上, 比较 ![](https://latex.codecogs.com/gif.latex?n^{log_b&space;a}) 和 ![](https://latex.codecogs.com/gif.latex?f(n)), 谁大就是谁, +相等的话就是 ![](https://latex.codecogs.com/gif.latex?\Theta(f(n))\log&space;n) 这里的大是多项式上的比较, 即比较次数, 而不是渐近上的 -比如 $n$ 与 $nlogn$ 渐近上后者大, 但多项式上是不能比较的 +比如 ![](https://latex.codecogs.com/gif.latex?n) 与 ![](https://latex.codecogs.com/gif.latex?nlogn) 渐近上后者大, 但多项式上是不能比较的 ### 5.3.2. 证明 #### 5.3.2.1. 证明当 n 为 b 的正合幂时成立 -* 用递归树可以得到 总代价为 $\sum_{j=0}^{log_b n-1} a^j f(\frac{n}{b^j})$ +* 用递归树可以得到 总代价为 ![](https://latex.codecogs.com/gif.latex?\sum_{j=0}^{log_b&space;n-1}&space;a^j&space;f(\frac{n}{b^j})) * 决定上式的渐近界 * 结合前两点 @@ -184,10 +168,10 @@ $$ 则得出 B={2,3,1},因为第二个(2)优先级最小, 为4, 接着第三个,最后第1个. 优先级数组的产生, 一般在 RANDOM(1,n^3), 这样优先级各不相同的概率至少为 1-1/n -由于要排序优先级数组, 所以时间复杂度 $O(nlogn)$ +由于要排序优先级数组, 所以时间复杂度 ![](https://latex.codecogs.com/gif.latex?O(nlogn)) 如果优先级唯一, 则此算法可以 shuffle 数组 -应证明 同样排列的概率是 $\frac{1}{n!}$ +应证明 同样排列的概率是 ![](https://latex.codecogs.com/gif.latex?\frac{1}{n!}) ### 6.1.2. RANDOMIZE-IN-PLACE @@ -200,18 +184,18 @@ def myshuffle(arr): arr[i],arr[p] = arr[p],arr[i] return arr ``` -时间复杂度 $O(n)$ +时间复杂度 ![](https://latex.codecogs.com/gif.latex?O(n)) 证明 -定义循环不变式: 对每个可能的 $A_n^{i-1}$ 排列, 其在 arr[1..i-1] 中的概率为 $\frac{1}{A_n^{i-1}}$ +定义循环不变式: 对每个可能的 ![](https://latex.codecogs.com/gif.latex?A_n^{i-1}) 排列, 其在 arr[1..i-1] 中的概率为 ![](https://latex.codecogs.com/gif.latex?\frac{1}{A_n^{i-1}}) 初始化: i=1 成立 保持 : 假设 在第 i-1 次迭代之前,成立, 证明在第 i 次迭代之后, 仍然成立, -终止: 在 结束后, i=n+1, 得到 概率为 $\frac{1}{n!}$ +终止: 在 结束后, i=n+1, 得到 概率为 ![](https://latex.codecogs.com/gif.latex?\frac{1}{n!}) # 7. 组合方程的近似算法 -* Stiring's approximation: $ n! \approx \sqrt{2\pi n}\left(\frac{n}{e}\right)^n$ -* 对于 $C_n^x=a$, 有 $x=\frac{ln^2 a}{n}$ -* 对于 $C_x^n=a$, 有 $x=(a*n!)^{\frac{1}{n}}+\frac{n}{2}$ +* Stiring's approximation: ![](https://latex.codecogs.com/gif.latex?n!&space;\approx&space;\sqrt{2\pi&space;n}\left(\frac{n}{e}\right)^n) +* 对于 ![](https://latex.codecogs.com/gif.latex?C_n^x=a), 有 ![](https://latex.codecogs.com/gif.latex?x=\frac{ln^2&space;a}{n}) +* 对于 ![](https://latex.codecogs.com/gif.latex?C_x^n=a), 有 ![](https://latex.codecogs.com/gif.latex?x=(a*n!)^{\frac{1}{n}}+\frac{n}{2}) @@ -220,20 +204,18 @@ def myshuffle(arr): ## 8.1. 球与盒子 把相同的秋随机投到 b 个盒子里,问在每个盒子里至少有一个球之前,平均至少要投多少个球? 称投入一个空盒为击中, 即求取得 b 次击中的概率 -设投 n 次, 称第 i 个阶段包括第 i-1 次击中到 第 i 次击中的球, 则第 i 次击中的概率为 $p_i=\frac{b-i+1}{b}$ -用 $n_i$表示第 i 阶段的投球数,则 $n=\sum_{i=1}^b n_i$ -且 $n_i$服从几何分布, $E(n_i)=\frac{b}{b-i+1}$, +设投 n 次, 称第 i 个阶段包括第 i-1 次击中到 第 i 次击中的球, 则第 i 次击中的概率为 ![](https://latex.codecogs.com/gif.latex?p_i=\frac{b-i+1}{b}) +用 ![](https://latex.codecogs.com/gif.latex?n_i)表示第 i 阶段的投球数,则 ![](https://latex.codecogs.com/gif.latex?n=\sum_{i=1}^b&space;n_i) +且 ![](https://latex.codecogs.com/gif.latex?n_i)服从几何分布, ![](https://latex.codecogs.com/gif.latex?E(n_i)=\frac{b}{b-i+1}), 则由期望的线性性, -$$ -E(n)=E(\sum_{i=1}^b n_i)=\sum_{i=1}^b E(n_i)=\sum_{i=1}^b \frac{b}{b-i+1}=b\sum_{i=1}^b \frac{1}{i}=b(lnb+O(1)) -$$ +![](https://latex.codecogs.com/gif.latex?&space;E(n)=E(\sum_{i=1}^b&space;n_i)=\sum_{i=1}^b&space;E(n_i)=\sum_{i=1}^b&space;\frac{b}{b-i+1}=b\sum_{i=1}^b&space;\frac{1}{i}=b(lnb+O(1))&space;) 这个问题又被称为 赠券收集者问题(coupon collector's problem),即集齐 b 种不同的赠券,在随机情况下平均需要买 blnb 张 ## 8.2. 序列 抛 n 次硬币, 期望看到的连续正面的次数 -答案是 $\Theta(logn)$ +答案是 ![](https://latex.codecogs.com/gif.latex?\Theta(logn)) 记 长度至少为 k 的正面序列开始与第 i 次抛, 由于独立, 所有 k 次抛掷都是正面的 概率为 -$P(A_{ik})=\frac{1}{2^k}$,对于 $k=2\lceil lgn\rceil$ +![](https://latex.codecogs.com/gif.latex?P(A_{ik})=\frac{1}{2^k}),对于 ![](https://latex.codecogs.com/gif.latex?k=2\lceil&space;lgn\rceil) ![coin1.jpg](https://upload-images.jianshu.io/upload_images/7130568-780b9795b6d9a2bd.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![coin2.jpg](https://upload-images.jianshu.io/upload_images/7130568-7d112b304e2d78b6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) @@ -246,60 +228,60 @@ $P(A_{ik})=\frac{1}{2^k}$,对于 $k=2\lceil lgn\rceil$ # 9. 摊还分析 ## 9.1. 聚合分析(aggregate analysis) - 一个 n 个操作的序列最坏情况下花费的总时间为$T(n)$, 则在最坏情况下, 每个操作的摊还代价为 $\frac{T(n)}{n}$ + 一个 n 个操作的序列最坏情况下花费的总时间为![](https://latex.codecogs.com/gif.latex?T(n)), 则在最坏情况下, 每个操作的摊还代价为 ![](https://latex.codecogs.com/gif.latex?\frac{T(n)}{n}) -如栈中的 push, pop 操作都是 $O(1)$, 增加一个新操作 `multipop`, +如栈中的 push, pop 操作都是 ![](https://latex.codecogs.com/gif.latex?O(1)), 增加一个新操作 `multipop`, ```python def multipop(stk,k): while not stk.empty() and k>0: stk.pop() k-=1 ``` -multipop 的时间复杂度为 min(stk.size,k), 最坏情况为 $O(n)$, 则 n 个包含 push pop multipop 的操作列的最坏情况是 $O(n^2)$, 并不是这样, 注意到, 必须栈中有元素, 再 pop, 所以 push 操作与pop 操作(包含 multipop中的pop), 个数相当, 所以 实际上应为 $O(n)$, 每个操作的摊还代价 为$O(1)$ +multipop 的时间复杂度为 min(stk.size,k), 最坏情况为 ![](https://latex.codecogs.com/gif.latex?O(n)), 则 n 个包含 push pop multipop 的操作列的最坏情况是 ![](https://latex.codecogs.com/gif.latex?O(n^2)), 并不是这样, 注意到, 必须栈中有元素, 再 pop, 所以 push 操作与pop 操作(包含 multipop中的pop), 个数相当, 所以 实际上应为 ![](https://latex.codecogs.com/gif.latex?O(n)), 每个操作的摊还代价 为![](https://latex.codecogs.com/gif.latex?O(1)) ## 9.2. 核算法 (accounting method) -对不同操作赋予不同费用 cost (称为摊还代价 $c_i'$), 可能多于或者少于其实际代价 $c_i$ +对不同操作赋予不同费用 cost (称为摊还代价 ![](https://latex.codecogs.com/gif.latex?c_i')), 可能多于或者少于其实际代价 ![](https://latex.codecogs.com/gif.latex?c_i) -当 $c_i'>c_i$, 将 $c_i'-c_i$( `credit`) 存入数据结构中的特定对象.. 对于后续 $c_i'c_i), 将 ![](https://latex.codecogs.com/gif.latex?c_i'-c_i)( `credit`) 存入数据结构中的特定对象.. 对于后续 ![](https://latex.codecogs.com/gif.latex?c_i' ## 9.3. 势能法(potential method) 势能释放用来支付未来操作的代价, 势能是整个数据结构的, 不是特定对象的(核算法是). -数据结构 $D_0$为初始状态, 依次 执行 n 个操作 $op_i$进行势能转换 $D_i =op_i(D_{i-1}), i=1,2,\ldots,n$ , 各操作代价为 $c_i$ +数据结构 ![](https://latex.codecogs.com/gif.latex?D_0)为初始状态, 依次 执行 n 个操作 ![](https://latex.codecogs.com/gif.latex?op_i)进行势能转换 ![](https://latex.codecogs.com/gif.latex?D_i&space;=op_i(D_{i-1}),&space;i=1,2,\ldots,n) , 各操作代价为 ![](https://latex.codecogs.com/gif.latex?c_i) -势函数 $\Phi:D_i\rightarrow R$, $\Phi(D_i)$即为 $D_i$的势 +势函数 ![](https://latex.codecogs.com/gif.latex?\Phi:D_i\rightarrow&space;R), ![](https://latex.codecogs.com/gif.latex?\Phi(D_i))即为 ![](https://latex.codecogs.com/gif.latex?D_i)的势 则第 i 个操作的摊还代价 -$$c_i'=c_i+\Phi(D_i)-\Phi(D_{i-1})$$ +![](https://latex.codecogs.com/gif.latex?c_i'=c_i+\Phi(D_i)-\Phi(D_{i-1})) 则 -$$\sum_{i=1}^{n}c_i'=\sum_{i=1}^{n}c_i+\Phi(D_n)-\Phi(D_0)$$ +![](https://latex.codecogs.com/gif.latex?\sum_{i=1}^{n}c_i'=\sum_{i=1}^{n}c_i+\Phi(D_n)-\Phi(D_0)) -如果定义一个势函数$\Phi, st \ \Phi(D_i)\geqslant\Phi(D_0)$, 则总摊还代价给出了实际代价的一个上界 -可以简单地以 $D_0 \text{为参考状态}, then \ \Phi(D_0)=0$ +如果定义一个势函数![](https://latex.codecogs.com/gif.latex?\Phi,&space;st&space;\&space;\Phi(D_i)\geqslant\Phi(D_0)), 则总摊还代价给出了实际代价的一个上界 +可以简单地以 ![](https://latex.codecogs.com/gif.latex?D_0&space;\text{Reference-state},&space;then&space;\&space;\Phi(D_0)=0) 例如栈操作, -设空栈为 $D_0$, 势函数定义为栈的元素数 -对于push, $ \Phi(D_i)-\Phi(D_{i-1})=1$ -则 $c' = c +\Phi(D_i)-\Phi(D_{i-1}) = c+1 = 2$ +设空栈为 ![](https://latex.codecogs.com/gif.latex?D_0), 势函数定义为栈的元素数 +对于push, ![](https://latex.codecogs.com/gif.latex?\Phi(D_i)-\Phi(D_{i-1})=1) +则 ![](https://latex.codecogs.com/gif.latex?c'&space;=&space;c&space;+\Phi(D_i)-\Phi(D_{i-1})&space;=&space;c+1&space;=&space;2) -对于 multipop, $ \Phi(D_i)-\Phi(D_{i-1})=- min(k,s)$ -则 $c' = c - min(k,s) = 0$ +对于 multipop, ![](https://latex.codecogs.com/gif.latex?\Phi(D_i)-\Phi(D_{i-1})=-&space;min(k,s)) +则 ![](https://latex.codecogs.com/gif.latex?c'&space;=&space;c&space;-&space;min(k,s)&space;=&space;0) -同理 pop 的摊还代价也是0, 则总摊还代价的上界(最坏情况) 为 $O(n)$ +同理 pop 的摊还代价也是0, 则总摊还代价的上界(最坏情况) 为 ![](https://latex.codecogs.com/gif.latex?O(n)) diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/b-tree.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/b-tree.md" index 6b62b25..a9eaafb 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/b-tree.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/b-tree.md" @@ -1,609 +1,609 @@ ---- -title: 『数据结构』B树(B-Tree)及其变体 B+树,B*树 -date: 2018-08-29 15:42 -categories: 数据结构与算法 -tags: [数据结构,B树,数据库] -keywords: 数据结构,B树,数据库 -mathjax: true -description: "B 树的原理与实现,b+ 树介绍" ---- - - - -- [1. 背景](#1-背景) -- [2. 定义](#2-定义) -- [3. 查找操作](#3-查找操作) -- [4. 插入操作](#4-插入操作) -- [5. 删除操作](#5-删除操作) - - [5.1. 第一种方法](#51-第一种方法) - - [5.2. 第二种方法](#52-第二种方法) -- [6. B+树](#6-b树) -- [7. B*树](#7-b树) -- [8. 代码实现与测试](#8-代码实现与测试) - - [8.1. 测试](#81-测试) - - [8.2. python 实现](#82-python-实现) -- [9. 参考资料](#9-参考资料) - - - - ->>从此心里有了B数(●'◡'●) - - -# 1. 背景 -当有大量数据储存在磁盘时,如数据库的查找,插入, 删除等操作的实现, 如果要读取或者写入, 磁盘的寻道, 旋转时间很长, 远大于在 内存中的读取,写入时间. - -平时用的二叉排序树搜索元素的时间复杂度虽然是 $O(log_2n)$的, 但是底数还是太小, 树高太高. - -所以就出现了 B 树(英文为B-Tree, 不是B减树), 可以理解为多叉排序树. 一个结点可以有多个孩子, 于是增大了底数, 减小了高度, 虽然比较的次数多(关键字数多), 但是由于是在内存中比较, 相较于磁盘的读取还是很快的. - -# 2. 定义 -度为 **d**(degree)的 B 树(阶(order) 为 2d) 定义如下, -0. 每个结点中包含有 n 个关键字信息: $(n,P_0,K_1,P_1,K_2,\ldots,K_n,P_n)$。其中: - a) $K_i$为关键字,且关键字按顺序升序排序 $K_{i-1}< K_i$ - b) $P_i$ 为指向子树根的接点, $K_{i-1}=2); -2. 除根结点和叶子结点外,其它每个结点至少有 d个孩子; -3. 若根结点不是叶子结点,则至少有 2 个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点); -4. **所有叶子结点都出现在同一层**,叶子节点没有孩子和指向孩子的指针 - - -性质: -$h\leq \left\lfloor \log _{d}\left({\frac {n+1}{2}}\right)\right\rfloor .$ - -如下是 度为2的 B 树, 每个结点可能有2,3或4 个孩子, 所以也叫 2,3,4树, 等价于[红黑树](/red-black-tree.html#more) -![](https://upload-images.jianshu.io/upload_images/7130568-30342360fb9674b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -# 3. 查找操作 -可以看成二叉排序树的扩展,二叉排序树是二路查找,B - 树是多路查找。 -节点内进行查找的时候除了顺序查找之外,还可以用二分查找来提高效率。 - -下面是顺序查找的 python 代码 -```python - def search(self,key,withpath=False): - nd = self.root - fathers = [] - while True: - i = nd.findKey(key) - if i==len(nd): fathers.append((nd,i-1,i)) - else: fathers.append((nd,i,i)) - if i -# 4. 插入操作 -自顶向下地进行插入操作, 最终插入在叶子结点, -考虑到叶子结点如果有 2t-1 $(k_1,k_2,\ldots,k_{2t-1})$个 关键字, 则需要进行分裂, - -一个有 2t-1$(k_1,k_2,\ldots,k_{2t-1})$个关键字 结点分裂是这样进行的: 此结点分裂为 两个关键字为 t-1个的结点, 分别为 $(k_1,k_2,\ldots,k_{t-1})$, $(k_{t+1},k_{t+2},\ldots,k_{2t-1})$, 然后再插入一个关键字$k_t$到父亲结点. - -注意同时要将孩子指针移动正确. - -所以自顶向下地查找到叶子结点, 中间遇到 2t-1个关键字的结点就进行分裂, 这样如果其子结点进行分裂, 上升来的一个关键字可以插入到父结点而不会超过2t-1 - -代码如下 -```python - def insert(self,key): - if len(self.root)== self.degree*2-1: - self.root = self.root.split(node(isLeaf=False),self.degree) - self.nodeNum +=2 - nd = self.root - while True: - idx = nd.findKey(key) - if idx -# 5. 删除操作 -删除操作是有点麻烦的, 有两种方法[^1] ->1. Locate and delete the item, then restructure the tree to retain its invariants, OR ->2. Do a single pass down the tree, but before entering (visiting) a node, restructure the tree so that once the key to be deleted is encountered, it can be deleted without triggering the need for any further restructuring - - -## 5.1. 第一种方法 -有如下情况 -* 删除结点在叶子结点上 - 1. 结点内的关键字个数大于d-1,可以直接删除(大于关键字个数下限,删除不影响 B - 树特性) - 2. 结点内的关键字个数等于d-1(等于关键字个数下限,删除后将破坏 特性),此时需观察该节点左右兄弟结点的关键字个数: - a. **旋转**: 如果其左右兄弟结点中存在关键字个数大于d-1 的结点,则从关键字个数大于 d-1 的兄弟结点中借关键字:**(这里看了网上的很多说法, 都是在介绍关键字的操作,而没有提到孩子结点. 我实现的时候想了很久才想出来: 借关键字时, 比如从右兄弟借一个关键字(第一个$k_1$), 此时即为左旋, 将父亲结点对应关键字移到当前结点, 再将右兄弟的移动父亲结点(因为要满足排序性质, 类似二叉树的选择) 然后进行孩子操作, 将右兄弟的$p_0$ 插入到 当前结点的孩子指针末尾) 左兄弟类似, 而且要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟**) - - b. **合并**: 如果其左右兄弟结点中不存在关键字个数大于 t-1 的结点,进行结点合并:将其父结点中的关键字拿到下一层,与该节点的左右兄弟结点的所有关键字合并 - **同样要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟** - - 3. 自底向上地检查来到这个叶子结点的路径上的结点是否满足关键字数目的要求, 只要关键字少于d-1,则进行旋转(2a)或者合并(2b)操作 -* 删除结点在非叶子结点上 -1. 查到到该结点, 然后转化成 上述 叶子结点中情况 -2. 转化过程: - a. 找到相邻关键字:即需删除关键字的左子树中的最大关键字或右子树中的最小关键字 - b. 用相邻关键字来覆盖需删除的非叶子节点关键字,再删除原相邻关键字(在;叶子上,这即为上述情况)。 - -python 代码如下, `delete`函数中, 查找到结点, 用 `fathers::[(父节点, 关键字指针, 孩子指针)]` 记录路径, 如果不是叶子结点, 就再进行查找, 并记录结点, 转换关键字. - -rebalance 就是从叶子结点自底向上到根结点, 只要遇到关键字数少于 2d-1 的,就进行平衡操作(旋转, 合并) - -实现时要很仔细, 考虑边界条件, 还有当是左孩子的时候操作的是父结点的 chdIdx 的前一个, 是右孩子的时候是 chdIdx 的关键字. 具体实现完整代码见文末. -```python - def delete(self,key):#to do - '''search the key, delete it , and form down to up to rebalance it ''' - nd,idx ,fathers= self.search(key,withpath=True) - if nd is None : return - del nd[idx] - self.keyNum-=1 - if not nd.isLeafNode(): - chd = nd.getChd(idx) # find the predecessor key - while not chd.isLeafNode(): - fathers.append((chd,len(chd)-1,len(chd))) - chd = chd.getChd(-1) - fathers.append((chd,len(chd)-1,len(chd))) - nd.insert(idx,chd[-1]) - del chd[-1] - if len(fathers)>1:self.rebalance(fathers) - def rebalance(self,fathers): - nd,keyIdx,chdIdx = fathers.pop() - while len(nd)=self.degree: # rotate when only one sibling is deficient - keyIdx = chdIdx-1 - nd.insert(0,prt[keyIdx]) # rotate keys - prt[keyIdx] = lbro[-1] - del lbro[-1] - if not nd.isLeafNode(): # if not leaf, move children - nd.insert(0,nd=lbro.getChd(-1)) - lbro.delChd(-1) - else: - keyIdx = chdIdx - nd.insert(len(nd),prt[keyIdx]) # rotate keys - prt[keyIdx] = rbro[0] - del rbro[0] - if not nd.isLeafNode(): # if not leaf, move children - #note that insert(-1,ele) will make the ele be the last second one - nd.insert(len(nd),nd=rbro.getChd(0)) - rbro.delChd(0) - if len(fathers)==1: - if len(self.root)==0: - self.root = nd - self.nodeNum -=1 - break - nd,i,j = fathers.pop() -``` - - -## 5.2. 第二种方法 -这是算法导论[^2]上的 -![](https://upload-images.jianshu.io/upload_images/7130568-119c3bc27eee8ee6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/7130568-567cc0ffd8a4da80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -例如 -![](https://upload-images.jianshu.io/upload_images/7130568-1f3e6003a5ccf800.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -```python -B-TREE-DELETE(T,k) - -1 r ← root[T] - 2 if n[r] = 1 - 3 then DISK_READ(c1[r]) - 4 DISK_READ(c2[r]) - 5 y ←c1[r] - 6 z ←c2[r] - 7 if n[y] = n[z] = t-1 ▹ Cases 2c or 3b - 8 then B-TREE-MERGE-CHILD(r, 1, y, z) - 9 root[T] ← y - 10 FREE-NODE(r) - 11 B-TREE-DELETE-NONONE(y, k) -12 else B-TREE-DELETE-NONONE (r, k) -13 else B-TREE-DELETE-NONONE (r, k) - - -考虑到根结点的特殊性,对根结点为1,并且两个子结点都是t-1的情况进行了特殊的处理: -先对两个子结点进行合并,然后把原来的根删除,把树根指向合并后的子结点y。 -这样B树的高度就减少了1。这也是B树高度唯一会减少的情况。 -除了这种情况以外,就直接调用子过程 B-TREE-DELETE-NONONE (x, k)。 - - -B-TREE-DELETE-NONONE (x, k) - -1 i ← 1 - 2 if leaf[x] ▹ Cases 1 - 3 then while i <= n[x] and k > keyi[x] - 4 do i ← i + 1 - 5 if k = keyi[x] - 6 then for j ← i+1 to n[x] - 7 do keyj-1[x] ←keyj[x] - 8 n[x] ← n[x] - 1 - 9 DISK-WRITE(x) - 10 else error:”the key does not exist” - 11 else while i <= n[x] and k > keyi[x] -12 do i ← i + 1 - 13 DISK-READ(ci[x]) - 14 y ←ci[x] - 15 if i <= n[x] - 16 then DISK-READ(ci+1[x]) - 17 z ←ci+1[x] - 18 if k = keyi[x] ▹ Cases 2 -19 then if n[y] > t-1 ▹ Cases 2a - 20 then k′←B-TREE-SEARCH-PREDECESSOR(y) - 21 B-TREE-DELETE-NONONE (y, k′) - 22 keyi[x] ←k′ - 23 else if n[z] > t-1 ▹ Cases 2b - 24 then k′←B-TREE-SEARCH-SUCCESSOR (z) - 25 B-TREE-DELETE-NONONE (z, k′) - 26 keyi[x] ←k′ - 27 else B-TREE-MERGE-CHILD(x, i, y, z)▹ Cases 2c - 28 B-TREE-DELETE-NONONE (y, k) - 29 else ▹ Cases 3 - 30 if i >1 - 31 then DISK-READ(ci-1[x]) - 32 p ←ci-1[x] - 33 if n[y] = t-1 - 34 then if i>1 and n[p] >t-1 ▹ Cases 3a - 35 then B-TREE-SHIFT-TO-RIGHT-CHILD(x,i,p,y) - 36 else if i <= n[x] and n[z] > t-1 ▹ Cases 3a - 37 then B-TREE-SHIFT-TO-LEFT-CHILD(x,i,y,z) - 38 else if i>1 ▹ Cases 3b - 39 then B-TREE-MERGE-CHILD(x, i, p, y) - 40 y ← p - 41 else B-TREE-MERGE-CHILD(x, i, y, z)▹ Cases 3b - 42 B-TREE-DELETE-NONONE (y, k) - - - - 转移到右边的子结点 -B-TREE-SHIFT-TO-RIGHT-CHILD(x,i,y,z) -1 n[z] ← n[z] +1 -2 j ← n[z] -3 while j > 1 -4 do keyj[z] ←keyj-1[z] -5 j ← j -1 -6 key1[z] ←keyi[x] -7 keyi[x] ←keyn[y][y] -8 if not leaf[z] -9 then j ← n[z] -10 while j > 0 -11 do cj+1[z] ←cj[z] -12 j ← j -1 -13 c1[z] ←cn[y]+1[y] -14 n[y] ← n[y] -1 -15 DISK-WRITE(y) - -16 DISK-WRITE(z) - -17 DISK-WRITE(x) - -转移到左边的子结点 -B-TREE-SHIFT-TO-LEFT-CHILD(x,i,y,z) -1 n[y] ← n[y] +1 -2 keyn[y][y] ← keyi[x] -3 keyi[x] ←key1[z] -4 n[z] ← n[z] -1 -5 j ← 1 -6 while j <= n[z] -7 do keyj[z] ←keyj+1[z] -8 j ← j +1 -9 if not leaf[z] -10 then cn[y]+1[y] ←c1[z] -11 j ← 1 -12 while j <= n[z]+1 -13 do cj[z] ←cj+1[z] -14 j ← j + 1 -15 DISK-WRITE(y) - -16 DISK-WRITE(z) - -17 DISK-WRITE(x) -``` - - -# 6. B+树 - B+ 树[^3]是 B- 树的变体,与B树不同的地方在于: -1. 非叶子结点的子树指针与关键字个数相同; -2. 非叶子结点的子树指针 $p_i$指向关键字值属于 $[k_i,k_{i+1})$ 的子树(B- 树是开区间); -3. 为所有叶子结点增加一个链指针; -4. **所有关键字都在叶子结点出现** - - B+ 的搜索与 B- 树也基本相同,区别是 B+ 树只有达到叶子结点才命中(B- 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找; -下面摘自 wiki[^4] -> ->### 查找 -> ->查找以典型的方式进行,类似于[二叉查找树](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8F%89%E6%9F%A5%E6%89%BE%E6%A0%91 "二叉查找树")。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是[二分查找](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE "二分查找")来确定这个位置。 ->### 插入 -> ->节点要处于违规状态,它必须包含在可接受范围之外数目的元素。 -> ->1. 首先,查找要插入其中的节点的位置。接着把值插入这个节点中。 ->2. 如果没有节点处于违规状态则处理结束。 ->3. 如果某个节点有过多元素,则把它分裂为两个节点,每个都有最小数目的元素。在树上递归向上继续这个处理直到到达根节点,如果根节点被分裂,则创建一个新根节点。为了使它工作,元素的最小和最大数目典型的必须选择为使最小数不小于最大数的一半。 -> ->### 删除  -> ->1. 首先,查找要删除的值。接着从包含它的节点中删除这个值。 ->2. 如果没有节点处于违规状态则处理结束。 ->3. 如果节点处于违规状态则有两种可能情况: -> 1. 它的兄弟节点,就是同一个父节点的子节点,可以把一个或多个它的子节点转移到当前节点,而把它返回为合法状态。如果是这样,在更改父节点和两个兄弟节点的分离值之后处理结束。 - > 2. 它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中,而且我们递归到父节点上,因为它被删除了一个子节点。持续这个处理直到当前节点是合法状态或者到达根节点,在其上根节点的子节点被合并而且合并后的节点成为新的根节点。 - - -由于叶子结点间有指向下一个叶子的指针, 便于遍历, 以及区间查找, 所以数据库的以及操作系统文件系统的实现常用 B+树, -![](https://upload-images.jianshu.io/upload_images/7130568-6a129fb2d32bda7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - - -# 7. B*树 -B*-tree [^5] 是 B+-tree 的变体,在 B+ 树的基础上 (所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B * 树中非根和非叶子结点再增加指向兄弟的指针;B* 树定义了非叶子结点关键字个数至少为 (2/3)*M,即块的最低使用率为 2/3(代替 B+ 树的 1/2) - -![](https://upload-images.jianshu.io/upload_images/7130568-517a256d15adb70d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -# 8. 代码实现与测试 -[github地址](https://github.com/mbinary/algorithm-in-python) - - -## 8.1. 测试 -```python - - -if __name__ =='__main__': - bt = bTree() - from random import shuffle,sample - n = 20 - lst = [i for i in range(n)] - shuffle(lst) - test= sample(lst,len(lst)//4) - print(f'building b-tree with {lst}') - for i in lst: - bt.insert(i) - #print(f'inserting {i}) - #print(bt) - print(bt) - print(f'serching {test}') - for i in test: - nd,idx = bt.search(i) - print(f'node: {repr(nd)}[{idx}]== {i}') - for i in test: - print(f'deleting {i}') - bt.delete(i) - print(bt) -``` -![bTree](https://upload-images.jianshu.io/upload_images/7130568-5dd763f4b28d853c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 8.2. python 实现 -```python -class node: - def __init__(self,keys=None,isLeaf = True,children=None): - if keys is None:keys=[] - if children is None: children =[] - self.keys = keys - self.isLeaf = isLeaf - self.children = [] - def __getitem__(self,i): - return self.keys[i] - def __delitem__(self,i): - del self.keys[i] - def __setitem__(self,i,k): - self.keys[i] = k - def __len__(self): - return len(self.keys) - def __repr__(self): - return str(self.keys) - def __str__(self): - children = ','.join([str(nd.keys) for nd in self.children]) - return f'keys: {self.keys}\nchildren: {children}\nisLeaf: {self.isLeaf}' - def getChd(self,i): - return self.children[i] - def delChd(self,i): - del self.children[i] - def setChd(self,i,chd): - self.children[i] = chd - def getChildren(self,begin=0,end=None): - if end is None:return self.children[begin:] - return self.children[begin:end] - def findKey(self,key): - for i,k in enumerate(self.keys): - if k>=key: - return i - return len(self) - def update(self,keys=None,isLeaf=None,children=None): - if keys is not None:self.keys = keys - if children is not None:self.children = children - if isLeaf is not None: self.isLeaf = isLeaf - def insert(self,i,key=None,nd=None): - if key is not None:self.keys.insert(i,key) - if not self.isLeaf and nd is not None: self.children.insert(i,nd) - def isLeafNode(self):return self.isLeaf - def split(self,prt,t): - # form new two nodes - k = self[t-1] - nd1 = node() - nd2 = node() - nd1.keys,nd2.keys = self[:t-1], self[t:] # note that t is 1 bigger than key index - nd1.isLeaf = nd2.isLeaf = self.isLeaf - if not self.isLeaf: - # note that children index is one bigger than key index, and all children included - nd1.children, nd2.children = self.children[0:t], self.children[t:] - # connect them to parent - idx = prt.findKey(k) - if prt.children !=[]: prt.children.remove(self) # remove the original node - prt.insert(idx,k,nd2) - prt.insert(idx,nd = nd1) - return prt - - -class bTree: - def __init__(self,degree=2): - self.root = node() - self.degree=degree - self.nodeNum = 1 - self.keyNum = 0 - def search(self,key,withpath=False): - nd = self.root - fathers = [] - while True: - i = nd.findKey(key) - if i==len(nd): fathers.append((nd,i-1,i)) - else: fathers.append((nd,i,i)) - if i1:self.rebalance(fathers) - def rebalance(self,fathers): - nd,keyIdx,chdIdx = fathers.pop() - while len(nd)=self.degree: # rotate when only one sibling is deficient - keyIdx = chdIdx-1 - nd.insert(0,prt[keyIdx]) # rotate keys - prt[keyIdx] = lbro[-1] - del lbro[-1] - if not nd.isLeafNode(): # if not leaf, move children - nd.insert(0,nd=lbro.getChd(-1)) - lbro.delChd(-1) - else: - keyIdx = chdIdx - nd.insert(len(nd),prt[keyIdx]) # rotate keys - prt[keyIdx] = rbro[0] - del rbro[0] - if not nd.isLeafNode(): # if not leaf, move children - #note that insert(-1,ele) will make the ele be the last second one - nd.insert(len(nd),nd=rbro.getChd(0)) - rbro.delChd(0) - if len(fathers)==1: - if len(self.root)==0: - self.root = nd - self.nodeNum -=1 - break - nd,i,j = fathers.pop() - def __str__(self): - head= '\n'+'-'*30+'B Tree'+'-'*30 - tail= '-'*30+'the end'+'-'*30+'\n' - lst = [[head],[f'node num: {self.nodeNum}, key num: {self.keyNum}']] - cur = [] - ndNum =0 - ndTotal= 1 - que = [self.root] - while que!=[]: - nd = que.pop(0) - cur.append(repr(nd)) - ndNum+=1 - que+=nd.getChildren() - if ndNum==ndTotal: - lst.append(cur) - cur = [] - ndNum = 0 - ndTotal =len(que) - lst.append([tail]) - lst = [','.join(li) for li in lst] - return '\n'.join(lst) - def __iter__(self,nd = None): - if nd is None: nd = self.root - que = [nd] - while que !=[]: - nd = que.pop(0) - yield nd - if nd.isLeafNode():continue - for i in range(len(nd)+1): - que.append(nd.getChd(i)) - -``` - -# 9. 参考资料 -[^1]: [B树](https://en.wikipedia.org/wiki/B-tree) -[^2]: 算法导论 -[^3]:[B - 树特征及插入删除操作总结](https://blog.csdn.net/u010842515/article/details/68487817) -[^4]: [B+树](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91) -[^5]: [从 B 树、B + 树、B * 树谈到 R 树](https://blog.csdn.net/v_JULY_v/article/details/6530142) +--- +title: 『数据结构』B树(B-Tree)及其变体 B+树,B*树 +date: 2018-08-29 15:42 +categories: 数据结构与算法 +tags: [数据结构,B树,数据库] +keywords: 数据结构,B树,数据库 +mathjax: true +description: "B 树的原理与实现,b+ 树介绍" +--- + + + +- [1. 背景](#1-背景) +- [2. 定义](#2-定义) +- [3. 查找操作](#3-查找操作) +- [4. 插入操作](#4-插入操作) +- [5. 删除操作](#5-删除操作) + - [5.1. 第一种方法](#51-第一种方法) + - [5.2. 第二种方法](#52-第二种方法) +- [6. B+树](#6-b树) +- [7. B*树](#7-b树) +- [8. 代码实现与测试](#8-代码实现与测试) + - [8.1. 测试](#81-测试) + - [8.2. python 实现](#82-python-实现) +- [9. 参考资料](#9-参考资料) + + + + +>>从此心里有了B数(●'◡'●) + + +# 1. 背景 +当有大量数据储存在磁盘时,如数据库的查找,插入, 删除等操作的实现, 如果要读取或者写入, 磁盘的寻道, 旋转时间很长, 远大于在 内存中的读取,写入时间. + +平时用的二叉排序树搜索元素的时间复杂度虽然是 ![](https://latex.codecogs.com/gif.latex?O(log_2n))的, 但是底数还是太小, 树高太高. + +所以就出现了 B 树(英文为B-Tree, 不是B减树), 可以理解为多叉排序树. 一个结点可以有多个孩子, 于是增大了底数, 减小了高度, 虽然比较的次数多(关键字数多), 但是由于是在内存中比较, 相较于磁盘的读取还是很快的. + +# 2. 定义 +度为 **d**(degree)的 B 树(阶(order) 为 2d) 定义如下, +0. 每个结点中包含有 n 个关键字信息: ![](https://latex.codecogs.com/gif.latex?(n,P_0,K_1,P_1,K_2,\ldots,K_n,P_n))。其中: + a) ![](https://latex.codecogs.com/gif.latex?K_i)为关键字,且关键字按顺序升序排序 ![](https://latex.codecogs.com/gif.latex?K_{i-1}<&space;K_i) + b) ![](https://latex.codecogs.com/gif.latex?P_i) 为指向子树根的接点, ![](https://latex.codecogs.com/gif.latex?K_{i-1}=2); +2. 除根结点和叶子结点外,其它每个结点至少有 d个孩子; +3. 若根结点不是叶子结点,则至少有 2 个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点); +4. **所有叶子结点都出现在同一层**,叶子节点没有孩子和指向孩子的指针 + + +性质: +![](https://latex.codecogs.com/gif.latex?h\leq&space;\left\lfloor&space;\log&space;_{d}\left({\frac&space;{n+1}{2}}\right)\right\rfloor&space;.) + +如下是 度为2的 B 树, 每个结点可能有2,3或4 个孩子, 所以也叫 2,3,4树, 等价于[红黑树](/red-black-tree.html#more) +![](https://upload-images.jianshu.io/upload_images/7130568-30342360fb9674b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# 3. 查找操作 +可以看成二叉排序树的扩展,二叉排序树是二路查找,B - 树是多路查找。 +节点内进行查找的时候除了顺序查找之外,还可以用二分查找来提高效率。 + +下面是顺序查找的 python 代码 +```python + def search(self,key,withpath=False): + nd = self.root + fathers = [] + while True: + i = nd.findKey(key) + if i==len(nd): fathers.append((nd,i-1,i)) + else: fathers.append((nd,i,i)) + if i +# 4. 插入操作 +自顶向下地进行插入操作, 最终插入在叶子结点, +考虑到叶子结点如果有 2t-1 ![](https://latex.codecogs.com/gif.latex?(k_1,k_2,\ldots,k_{2t-1}))个 关键字, 则需要进行分裂, + +一个有 2t-1![](https://latex.codecogs.com/gif.latex?(k_1,k_2,\ldots,k_{2t-1}))个关键字 结点分裂是这样进行的: 此结点分裂为 两个关键字为 t-1个的结点, 分别为 ![](https://latex.codecogs.com/gif.latex?(k_1,k_2,\ldots,k_{t-1})), ![](https://latex.codecogs.com/gif.latex?(k_{t+1},k_{t+2},\ldots,k_{2t-1})), 然后再插入一个关键字![](https://latex.codecogs.com/gif.latex?k_t)到父亲结点. + +注意同时要将孩子指针移动正确. + +所以自顶向下地查找到叶子结点, 中间遇到 2t-1个关键字的结点就进行分裂, 这样如果其子结点进行分裂, 上升来的一个关键字可以插入到父结点而不会超过2t-1 + +代码如下 +```python + def insert(self,key): + if len(self.root)== self.degree*2-1: + self.root = self.root.split(node(isLeaf=False),self.degree) + self.nodeNum +=2 + nd = self.root + while True: + idx = nd.findKey(key) + if idx +# 5. 删除操作 +删除操作是有点麻烦的, 有两种方法[^1] +>1. Locate and delete the item, then restructure the tree to retain its invariants, OR +>2. Do a single pass down the tree, but before entering (visiting) a node, restructure the tree so that once the key to be deleted is encountered, it can be deleted without triggering the need for any further restructuring + + +## 5.1. 第一种方法 +有如下情况 +* 删除结点在叶子结点上 + 1. 结点内的关键字个数大于d-1,可以直接删除(大于关键字个数下限,删除不影响 B - 树特性) + 2. 结点内的关键字个数等于d-1(等于关键字个数下限,删除后将破坏 特性),此时需观察该节点左右兄弟结点的关键字个数: + a. **旋转**: 如果其左右兄弟结点中存在关键字个数大于d-1 的结点,则从关键字个数大于 d-1 的兄弟结点中借关键字:**(这里看了网上的很多说法, 都是在介绍关键字的操作,而没有提到孩子结点. 我实现的时候想了很久才想出来: 借关键字时, 比如从右兄弟借一个关键字(第一个![](https://latex.codecogs.com/gif.latex?k_1)), 此时即为左旋, 将父亲结点对应关键字移到当前结点, 再将右兄弟的移动父亲结点(因为要满足排序性质, 类似二叉树的选择) 然后进行孩子操作, 将右兄弟的![](https://latex.codecogs.com/gif.latex?p_0) 插入到 当前结点的孩子指针末尾) 左兄弟类似, 而且要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟**) + + b. **合并**: 如果其左右兄弟结点中不存在关键字个数大于 t-1 的结点,进行结点合并:将其父结点中的关键字拿到下一层,与该节点的左右兄弟结点的所有关键字合并 + **同样要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟** + + 3. 自底向上地检查来到这个叶子结点的路径上的结点是否满足关键字数目的要求, 只要关键字少于d-1,则进行旋转(2a)或者合并(2b)操作 +* 删除结点在非叶子结点上 +1. 查到到该结点, 然后转化成 上述 叶子结点中情况 +2. 转化过程: + a. 找到相邻关键字:即需删除关键字的左子树中的最大关键字或右子树中的最小关键字 + b. 用相邻关键字来覆盖需删除的非叶子节点关键字,再删除原相邻关键字(在;叶子上,这即为上述情况)。 + +python 代码如下, `delete`函数中, 查找到结点, 用 `fathers::[(父节点, 关键字指针, 孩子指针)]` 记录路径, 如果不是叶子结点, 就再进行查找, 并记录结点, 转换关键字. + +rebalance 就是从叶子结点自底向上到根结点, 只要遇到关键字数少于 2d-1 的,就进行平衡操作(旋转, 合并) + +实现时要很仔细, 考虑边界条件, 还有当是左孩子的时候操作的是父结点的 chdIdx 的前一个, 是右孩子的时候是 chdIdx 的关键字. 具体实现完整代码见文末. +```python + def delete(self,key):#to do + '''search the key, delete it , and form down to up to rebalance it ''' + nd,idx ,fathers= self.search(key,withpath=True) + if nd is None : return + del nd[idx] + self.keyNum-=1 + if not nd.isLeafNode(): + chd = nd.getChd(idx) # find the predecessor key + while not chd.isLeafNode(): + fathers.append((chd,len(chd)-1,len(chd))) + chd = chd.getChd(-1) + fathers.append((chd,len(chd)-1,len(chd))) + nd.insert(idx,chd[-1]) + del chd[-1] + if len(fathers)>1:self.rebalance(fathers) + def rebalance(self,fathers): + nd,keyIdx,chdIdx = fathers.pop() + while len(nd)=self.degree: # rotate when only one sibling is deficient + keyIdx = chdIdx-1 + nd.insert(0,prt[keyIdx]) # rotate keys + prt[keyIdx] = lbro[-1] + del lbro[-1] + if not nd.isLeafNode(): # if not leaf, move children + nd.insert(0,nd=lbro.getChd(-1)) + lbro.delChd(-1) + else: + keyIdx = chdIdx + nd.insert(len(nd),prt[keyIdx]) # rotate keys + prt[keyIdx] = rbro[0] + del rbro[0] + if not nd.isLeafNode(): # if not leaf, move children + #note that insert(-1,ele) will make the ele be the last second one + nd.insert(len(nd),nd=rbro.getChd(0)) + rbro.delChd(0) + if len(fathers)==1: + if len(self.root)==0: + self.root = nd + self.nodeNum -=1 + break + nd,i,j = fathers.pop() +``` + + +## 5.2. 第二种方法 +这是算法导论[^2]上的 +![](https://upload-images.jianshu.io/upload_images/7130568-119c3bc27eee8ee6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/7130568-567cc0ffd8a4da80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +例如 +![](https://upload-images.jianshu.io/upload_images/7130568-1f3e6003a5ccf800.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +```python +B-TREE-DELETE(T,k) + +1 r ← root[T] + 2 if n[r] = 1 + 3 then DISK_READ(c1[r]) + 4 DISK_READ(c2[r]) + 5 y ←c1[r] + 6 z ←c2[r] + 7 if n[y] = n[z] = t-1 ▹ Cases 2c or 3b + 8 then B-TREE-MERGE-CHILD(r, 1, y, z) + 9 root[T] ← y + 10 FREE-NODE(r) + 11 B-TREE-DELETE-NONONE(y, k) +12 else B-TREE-DELETE-NONONE (r, k) +13 else B-TREE-DELETE-NONONE (r, k) + + +考虑到根结点的特殊性,对根结点为1,并且两个子结点都是t-1的情况进行了特殊的处理: +先对两个子结点进行合并,然后把原来的根删除,把树根指向合并后的子结点y。 +这样B树的高度就减少了1。这也是B树高度唯一会减少的情况。 +除了这种情况以外,就直接调用子过程 B-TREE-DELETE-NONONE (x, k)。 + + +B-TREE-DELETE-NONONE (x, k) + +1 i ← 1 + 2 if leaf[x] ▹ Cases 1 + 3 then while i <= n[x] and k > keyi[x] + 4 do i ← i + 1 + 5 if k = keyi[x] + 6 then for j ← i+1 to n[x] + 7 do keyj-1[x] ←keyj[x] + 8 n[x] ← n[x] - 1 + 9 DISK-WRITE(x) + 10 else error:”the key does not exist” + 11 else while i <= n[x] and k > keyi[x] +12 do i ← i + 1 + 13 DISK-READ(ci[x]) + 14 y ←ci[x] + 15 if i <= n[x] + 16 then DISK-READ(ci+1[x]) + 17 z ←ci+1[x] + 18 if k = keyi[x] ▹ Cases 2 +19 then if n[y] > t-1 ▹ Cases 2a + 20 then k′←B-TREE-SEARCH-PREDECESSOR(y) + 21 B-TREE-DELETE-NONONE (y, k′) + 22 keyi[x] ←k′ + 23 else if n[z] > t-1 ▹ Cases 2b + 24 then k′←B-TREE-SEARCH-SUCCESSOR (z) + 25 B-TREE-DELETE-NONONE (z, k′) + 26 keyi[x] ←k′ + 27 else B-TREE-MERGE-CHILD(x, i, y, z)▹ Cases 2c + 28 B-TREE-DELETE-NONONE (y, k) + 29 else ▹ Cases 3 + 30 if i >1 + 31 then DISK-READ(ci-1[x]) + 32 p ←ci-1[x] + 33 if n[y] = t-1 + 34 then if i>1 and n[p] >t-1 ▹ Cases 3a + 35 then B-TREE-SHIFT-TO-RIGHT-CHILD(x,i,p,y) + 36 else if i <= n[x] and n[z] > t-1 ▹ Cases 3a + 37 then B-TREE-SHIFT-TO-LEFT-CHILD(x,i,y,z) + 38 else if i>1 ▹ Cases 3b + 39 then B-TREE-MERGE-CHILD(x, i, p, y) + 40 y ← p + 41 else B-TREE-MERGE-CHILD(x, i, y, z)▹ Cases 3b + 42 B-TREE-DELETE-NONONE (y, k) + + + + 转移到右边的子结点 +B-TREE-SHIFT-TO-RIGHT-CHILD(x,i,y,z) +1 n[z] ← n[z] +1 +2 j ← n[z] +3 while j > 1 +4 do keyj[z] ←keyj-1[z] +5 j ← j -1 +6 key1[z] ←keyi[x] +7 keyi[x] ←keyn[y][y] +8 if not leaf[z] +9 then j ← n[z] +10 while j > 0 +11 do cj+1[z] ←cj[z] +12 j ← j -1 +13 c1[z] ←cn[y]+1[y] +14 n[y] ← n[y] -1 +15 DISK-WRITE(y) + +16 DISK-WRITE(z) + +17 DISK-WRITE(x) + +转移到左边的子结点 +B-TREE-SHIFT-TO-LEFT-CHILD(x,i,y,z) +1 n[y] ← n[y] +1 +2 keyn[y][y] ← keyi[x] +3 keyi[x] ←key1[z] +4 n[z] ← n[z] -1 +5 j ← 1 +6 while j <= n[z] +7 do keyj[z] ←keyj+1[z] +8 j ← j +1 +9 if not leaf[z] +10 then cn[y]+1[y] ←c1[z] +11 j ← 1 +12 while j <= n[z]+1 +13 do cj[z] ←cj+1[z] +14 j ← j + 1 +15 DISK-WRITE(y) + +16 DISK-WRITE(z) + +17 DISK-WRITE(x) +``` + + +# 6. B+树 + B+ 树[^3]是 B- 树的变体,与B树不同的地方在于: +1. 非叶子结点的子树指针与关键字个数相同; +2. 非叶子结点的子树指针 ![](https://latex.codecogs.com/gif.latex?p_i)指向关键字值属于 ![](https://latex.codecogs.com/gif.latex?[k_i,k_{i+1})) 的子树(B- 树是开区间); +3. 为所有叶子结点增加一个链指针; +4. **所有关键字都在叶子结点出现** + + B+ 的搜索与 B- 树也基本相同,区别是 B+ 树只有达到叶子结点才命中(B- 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找; +下面摘自 wiki[^4] +> +>### 查找 +> +>查找以典型的方式进行,类似于[二叉查找树](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8F%89%E6%9F%A5%E6%89%BE%E6%A0%91 "二叉查找树")。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是[二分查找](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE "二分查找")来确定这个位置。 +>### 插入 +> +>节点要处于违规状态,它必须包含在可接受范围之外数目的元素。 +> +>1. 首先,查找要插入其中的节点的位置。接着把值插入这个节点中。 +>2. 如果没有节点处于违规状态则处理结束。 +>3. 如果某个节点有过多元素,则把它分裂为两个节点,每个都有最小数目的元素。在树上递归向上继续这个处理直到到达根节点,如果根节点被分裂,则创建一个新根节点。为了使它工作,元素的最小和最大数目典型的必须选择为使最小数不小于最大数的一半。 +> +>### 删除  +> +>1. 首先,查找要删除的值。接着从包含它的节点中删除这个值。 +>2. 如果没有节点处于违规状态则处理结束。 +>3. 如果节点处于违规状态则有两种可能情况: +> 1. 它的兄弟节点,就是同一个父节点的子节点,可以把一个或多个它的子节点转移到当前节点,而把它返回为合法状态。如果是这样,在更改父节点和两个兄弟节点的分离值之后处理结束。 + > 2. 它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中,而且我们递归到父节点上,因为它被删除了一个子节点。持续这个处理直到当前节点是合法状态或者到达根节点,在其上根节点的子节点被合并而且合并后的节点成为新的根节点。 + + +由于叶子结点间有指向下一个叶子的指针, 便于遍历, 以及区间查找, 所以数据库的以及操作系统文件系统的实现常用 B+树, +![](https://upload-images.jianshu.io/upload_images/7130568-6a129fb2d32bda7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + + +# 7. B*树 +B*-tree [^5] 是 B+-tree 的变体,在 B+ 树的基础上 (所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B * 树中非根和非叶子结点再增加指向兄弟的指针;B* 树定义了非叶子结点关键字个数至少为 (2/3)*M,即块的最低使用率为 2/3(代替 B+ 树的 1/2) + +![](https://upload-images.jianshu.io/upload_images/7130568-517a256d15adb70d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# 8. 代码实现与测试 +[github地址](https://github.com/mbinary/algorithm-in-python) + + +## 8.1. 测试 +```python + + +if __name__ =='__main__': + bt = bTree() + from random import shuffle,sample + n = 20 + lst = [i for i in range(n)] + shuffle(lst) + test= sample(lst,len(lst)//4) + print(f'building b-tree with {lst}') + for i in lst: + bt.insert(i) + #print(f'inserting {i}) + #print(bt) + print(bt) + print(f'serching {test}') + for i in test: + nd,idx = bt.search(i) + print(f'node: {repr(nd)}[{idx}]== {i}') + for i in test: + print(f'deleting {i}') + bt.delete(i) + print(bt) +``` +![bTree](https://upload-images.jianshu.io/upload_images/7130568-5dd763f4b28d853c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 8.2. python 实现 +```python +class node: + def __init__(self,keys=None,isLeaf = True,children=None): + if keys is None:keys=[] + if children is None: children =[] + self.keys = keys + self.isLeaf = isLeaf + self.children = [] + def __getitem__(self,i): + return self.keys[i] + def __delitem__(self,i): + del self.keys[i] + def __setitem__(self,i,k): + self.keys[i] = k + def __len__(self): + return len(self.keys) + def __repr__(self): + return str(self.keys) + def __str__(self): + children = ','.join([str(nd.keys) for nd in self.children]) + return f'keys: {self.keys}\nchildren: {children}\nisLeaf: {self.isLeaf}' + def getChd(self,i): + return self.children[i] + def delChd(self,i): + del self.children[i] + def setChd(self,i,chd): + self.children[i] = chd + def getChildren(self,begin=0,end=None): + if end is None:return self.children[begin:] + return self.children[begin:end] + def findKey(self,key): + for i,k in enumerate(self.keys): + if k>=key: + return i + return len(self) + def update(self,keys=None,isLeaf=None,children=None): + if keys is not None:self.keys = keys + if children is not None:self.children = children + if isLeaf is not None: self.isLeaf = isLeaf + def insert(self,i,key=None,nd=None): + if key is not None:self.keys.insert(i,key) + if not self.isLeaf and nd is not None: self.children.insert(i,nd) + def isLeafNode(self):return self.isLeaf + def split(self,prt,t): + # form new two nodes + k = self[t-1] + nd1 = node() + nd2 = node() + nd1.keys,nd2.keys = self[:t-1], self[t:] # note that t is 1 bigger than key index + nd1.isLeaf = nd2.isLeaf = self.isLeaf + if not self.isLeaf: + # note that children index is one bigger than key index, and all children included + nd1.children, nd2.children = self.children[0:t], self.children[t:] + # connect them to parent + idx = prt.findKey(k) + if prt.children !=[]: prt.children.remove(self) # remove the original node + prt.insert(idx,k,nd2) + prt.insert(idx,nd = nd1) + return prt + + +class bTree: + def __init__(self,degree=2): + self.root = node() + self.degree=degree + self.nodeNum = 1 + self.keyNum = 0 + def search(self,key,withpath=False): + nd = self.root + fathers = [] + while True: + i = nd.findKey(key) + if i==len(nd): fathers.append((nd,i-1,i)) + else: fathers.append((nd,i,i)) + if i1:self.rebalance(fathers) + def rebalance(self,fathers): + nd,keyIdx,chdIdx = fathers.pop() + while len(nd)=self.degree: # rotate when only one sibling is deficient + keyIdx = chdIdx-1 + nd.insert(0,prt[keyIdx]) # rotate keys + prt[keyIdx] = lbro[-1] + del lbro[-1] + if not nd.isLeafNode(): # if not leaf, move children + nd.insert(0,nd=lbro.getChd(-1)) + lbro.delChd(-1) + else: + keyIdx = chdIdx + nd.insert(len(nd),prt[keyIdx]) # rotate keys + prt[keyIdx] = rbro[0] + del rbro[0] + if not nd.isLeafNode(): # if not leaf, move children + #note that insert(-1,ele) will make the ele be the last second one + nd.insert(len(nd),nd=rbro.getChd(0)) + rbro.delChd(0) + if len(fathers)==1: + if len(self.root)==0: + self.root = nd + self.nodeNum -=1 + break + nd,i,j = fathers.pop() + def __str__(self): + head= '\n'+'-'*30+'B Tree'+'-'*30 + tail= '-'*30+'the end'+'-'*30+'\n' + lst = [[head],[f'node num: {self.nodeNum}, key num: {self.keyNum}']] + cur = [] + ndNum =0 + ndTotal= 1 + que = [self.root] + while que!=[]: + nd = que.pop(0) + cur.append(repr(nd)) + ndNum+=1 + que+=nd.getChildren() + if ndNum==ndTotal: + lst.append(cur) + cur = [] + ndNum = 0 + ndTotal =len(que) + lst.append([tail]) + lst = [','.join(li) for li in lst] + return '\n'.join(lst) + def __iter__(self,nd = None): + if nd is None: nd = self.root + que = [nd] + while que !=[]: + nd = que.pop(0) + yield nd + if nd.isLeafNode():continue + for i in range(len(nd)+1): + que.append(nd.getChd(i)) + +``` + +# 9. 参考资料 +[^1]: [B树](https://en.wikipedia.org/wiki/B-tree) +[^2]: 算法导论 +[^3]:[B - 树特征及插入删除操作总结](https://blog.csdn.net/u010842515/article/details/68487817) +[^4]: [B+树](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91) +[^5]: [从 B 树、B + 树、B * 树谈到 R 树](https://blog.csdn.net/v_JULY_v/article/details/6530142) diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/fib-heap.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/fib-heap.md" index 271440f..6d9df17 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/fib-heap.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/fib-heap.md" @@ -1,170 +1,168 @@ ---- -title: 『数据结构』Fibonacci-heap -date: 2018-09-06 19:09 -categories: 数据结构与算法 -tags: [数据结构,斐波那契堆] -keywords: 数据结构,斐波那契堆 -mathjax: true -description: "介绍 fibnacci heap 的原理" ---- - - - -- [1. 结构](#1-结构) -- [2. 势函数](#2-势函数) -- [3. 最大度数](#3-最大度数) -- [4. 操作](#4-操作) - - [4.1. 创建一个斐波那契堆](#41-创建一个斐波那契堆) - - [4.2. 插入一个结点](#42-插入一个结点) - - [4.3. 寻找最小结点](#43-寻找最小结点) - - [4.4. 合并两个斐波那契堆](#44-合并两个斐波那契堆) - - [4.5. 抽取最小值](#45-抽取最小值) - - [4.6. 关键字减值](#46-关键字减值) - - [4.7. 删除结点](#47-删除结点) -- [5. 最大度数的证明](#5-最大度数的证明) - - - -![](https://upload-images.jianshu.io/upload_images/7130568-22531846a72b0d83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -# 1. 结构 -斐波那契堆是一系列具有最小堆序的有根树的集合, 同一代(层)结点由双向循环链表链接, **为了便于删除最小结点, 还需要维持链表为升序, 即nd<=nd.right(nd==nd.right时只有一个结点或为 None)**, 父子之间都有指向对方的指针. - -结点有degree 属性, 记录孩子的个数, mark 属性用来标记(为了满足势函数, 达到摊还需求的) - -还有一个最小值指针 H.min 指向最小根结点 -![](https://upload-images.jianshu.io/upload_images/7130568-d4e8a85754fdbc14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -# 2. 势函数 -下面用势函数来分析摊还代价, 如果你不明白, 可以看[摊还分析](https://www.jianshu.com/p/052fbe9d92a4) - -$\Phi(H) = t(H) + 2m(h)$ -t 是根链表中树的数目,m(H) 表示被标记的结点数 - -最初没有结点 - -# 3. 最大度数 -结点的最大度数(即孩子数)$D(n)\leqslant \lfloor lgn \rfloor$, 证明放在最后 - -# 4. 操作 - -## 4.1. 创建一个斐波那契堆 -$O(1)$ - -## 4.2. 插入一个结点 -```python -nd = new node -nd.prt = nd.chd = None -if H.min is None: - creat H with nd - H.min = nd -else: - insert nd into H's root list - if H.min -## 4.3. 寻找最小结点 -直接用 H.min, $O(1)$ - -## 4.4. 合并两个斐波那契堆 -```python -def union(H1,H2): - if H1.min ==None or (H1.min and H2.min and H1.min>H2.min): - H1.min = H2.min - link H2.rootList to H1.rootList - return H1 -``` -易知 $\Delta \Phi = 0$ - -## 4.5. 抽取最小值 -抽取最小值, 一定是在根结点, 然后将此根结点的所有子树的根放在 根结点双向循环链表中, 之后还要进行**树的合并. 以使每个根结点的度不同,** -```python -def extract-min(H): - z = H.min - if z!=None: - for chd of z: - link chd to H.rootList - chd.prt = None - remove z from the rootList of H - if z==z.right: - H.min = None - else: - H.min = z.right - consolidate(H) - H.n -=1 - return z -``` -consolidate 函数使用一个 辅助数组degree来记录所有根结点(不超过lgn)对应的度数, degree[i] = nd 表示.有且只有一个结点 nd 的度数为 i. -```python -def consolidate(H): - initialize degree with None - for nd in H.rootList: - d = nd.degree - while degree[d] !=None: - nd2 = degree[d] - if nd2.degree < nd.degree: - nd2,nd = nd,nd2 - - make nd2 child of nd - nd.degree = d+1 - nd.mark = False # to balace the potential - - remove nd2 from H.rootList - degree[d] = None - d+=1 - else: degree[d] = nd - for i in degree: - if i!=None: - link i to H.rootList - if H.min ==None: H.min = i - else if H.min>i: H.min = i -``` -时间复杂度为$O(lgn)$ 即数组移动的长度, 而最多有 lgn个元素 - - -## 4.6. 关键字减值 -```python -def decrease-key(H,x,k): - if k>x.key: error - x.key = k - y=x.p - if y!=None and x.key < y.key: - cut(H,x,y) - cascading-cut(H,y) - if x.key < H.min.key: - H.min = x -def cut(H,x,y): - remove x from the child list of y, decrementing y.degree - add x to H.rootList - x.prt = None - x.mark = False - -def cascading-cut(H,y): - z- y,prt - if z !=None: - if y.mark ==False:y.mark = True - else: - cut(H,y,z) - cascading-cut(H,z) -``` -![](https://upload-images.jianshu.io/upload_images/7130568-0a29221f8a1fbfbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 4.7. 删除结点 -```python -decrease(H,nd, MIN) -extract-min(H) -``` - - -# 5. 最大度数的证明 -这也是`斐波那契`这个名字的由来, -$D(n)\leqslant \lfloor lgn \rfloor$ -![](https://upload-images.jianshu.io/upload_images/7130568-c9e0cd3be4e98c4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +--- +title: 『数据结构』Fibonacci-heap +date: 2018-09-06 19:09 +categories: 数据结构与算法 +tags: [数据结构,斐波那契堆] +keywords: 数据结构,斐波那契堆 +mathjax: true +description: "介绍 fibnacci heap 的原理" +--- + + + +- [1. 结构](#1-结构) +- [2. 势函数](#2-势函数) +- [3. 最大度数](#3-最大度数) +- [4. 操作](#4-操作) + - [4.1. 创建一个斐波那契堆](#41-创建一个斐波那契堆) + - [4.2. 插入一个结点](#42-插入一个结点) + - [4.3. 寻找最小结点](#43-寻找最小结点) + - [4.4. 合并两个斐波那契堆](#44-合并两个斐波那契堆) + - [4.5. 抽取最小值](#45-抽取最小值) + - [4.6. 关键字减值](#46-关键字减值) + - [4.7. 删除结点](#47-删除结点) +- [5. 最大度数的证明](#5-最大度数的证明) + + + +![](https://upload-images.jianshu.io/upload_images/7130568-22531846a72b0d83.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# 1. 结构 +斐波那契堆是一系列具有最小堆序的有根树的集合, 同一代(层)结点由双向循环链表链接, **为了便于删除最小结点, 还需要维持链表为升序, 即nd<=nd.right(nd==nd.right时只有一个结点或为 None)**, 父子之间都有指向对方的指针. + +结点有degree 属性, 记录孩子的个数, mark 属性用来标记(为了满足势函数, 达到摊还需求的) + +还有一个最小值指针 H.min 指向最小根结点 +![](https://upload-images.jianshu.io/upload_images/7130568-d4e8a85754fdbc14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# 2. 势函数 +下面用势函数来分析摊还代价, 如果你不明白, 可以看[摊还分析](https://www.jianshu.com/p/052fbe9d92a4) + +![](https://latex.codecogs.com/gif.latex?\Phi(H)&space;=&space;t(H)&space;+&space;2m(h)) +t 是根链表中树的数目,m(H) 表示被标记的结点数 + +最初没有结点 + +# 3. 最大度数 +结点的最大度数(即孩子数)![](https://latex.codecogs.com/gif.latex?D(n)\leqslant&space;\lfloor&space;lgn&space;\rfloor), 证明放在最后 + +# 4. 操作 + +## 4.1. 创建一个斐波那契堆 +![](https://latex.codecogs.com/gif.latex?O(1)) + +## 4.2. 插入一个结点 +```python +nd = new node +nd.prt = nd.chd = None +if H.min is None: + creat H with nd + H.min = nd +else: + insert nd into H's root list + if H.min +## 4.3. 寻找最小结点 +直接用 H.min, ![](https://latex.codecogs.com/gif.latex?O(1)) + +## 4.4. 合并两个斐波那契堆 +```python +def union(H1,H2): + if H1.min ==None or (H1.min and H2.min and H1.min>H2.min): + H1.min = H2.min + link H2.rootList to H1.rootList + return H1 +``` +易知 ![](https://latex.codecogs.com/gif.latex?\Delta&space;\Phi&space;=&space;0) + +## 4.5. 抽取最小值 +抽取最小值, 一定是在根结点, 然后将此根结点的所有子树的根放在 根结点双向循环链表中, 之后还要进行**树的合并. 以使每个根结点的度不同,** +```python +def extract-min(H): + z = H.min + if z!=None: + for chd of z: + link chd to H.rootList + chd.prt = None + remove z from the rootList of H + if z==z.right: + H.min = None + else: + H.min = z.right + consolidate(H) + H.n -=1 + return z +``` +consolidate 函数使用一个 辅助数组degree来记录所有根结点(不超过lgn)对应的度数, degree[i] = nd 表示.有且只有一个结点 nd 的度数为 i. +```python +def consolidate(H): + initialize degree with None + for nd in H.rootList: + d = nd.degree + while degree[d] !=None: + nd2 = degree[d] + if nd2.degree < nd.degree: + nd2,nd = nd,nd2 + + make nd2 child of nd + nd.degree = d+1 + nd.mark = False # to balace the potential + + remove nd2 from H.rootList + degree[d] = None + d+=1 + else: degree[d] = nd + for i in degree: + if i!=None: + link i to H.rootList + if H.min ==None: H.min = i + else if H.min>i: H.min = i +``` +时间复杂度为![](https://latex.codecogs.com/gif.latex?O(lgn)) 即数组移动的长度, 而最多有 lgn个元素 + + +## 4.6. 关键字减值 +```python +def decrease-key(H,x,k): + if k>x.key: error + x.key = k + y=x.p + if y!=None and x.key < y.key: + cut(H,x,y) + cascading-cut(H,y) + if x.key < H.min.key: + H.min = x +def cut(H,x,y): + remove x from the child list of y, decrementing y.degree + add x to H.rootList + x.prt = None + x.mark = False + +def cascading-cut(H,y): + z- y,prt + if z !=None: + if y.mark ==False:y.mark = True + else: + cut(H,y,z) + cascading-cut(H,z) +``` +![](https://upload-images.jianshu.io/upload_images/7130568-0a29221f8a1fbfbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 4.7. 删除结点 +```python +decrease(H,nd, MIN) +extract-min(H) +``` + + +# 5. 最大度数的证明 +这也是`斐波那契`这个名字的由来, +![](https://latex.codecogs.com/gif.latex?D(n)\leqslant&space;\lfloor&space;lgn&space;\rfloor) +![](https://upload-images.jianshu.io/upload_images/7130568-c9e0cd3be4e98c4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/graph.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/graph.md" index 0e7dcd5..597f34e 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/graph.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/graph.md" @@ -1,495 +1,466 @@ ---- -title: 图算法 -date: 2018-09-06 19:10 -categories: 数据结构与算法 -tags: [图,算法] -keywords: 图,算法 -mathjax: true -description: "算法导论上常用的图算法, 代码, 原理等" - ---- - - - -- [1. 图](#1-图) - - [1.1. 概念](#11-概念) - - [1.1.1. 性质](#111-性质) - - [1.2. 图的表示](#12-图的表示) - - [1.3. 树](#13-树) -- [2. 图的搜索](#2-图的搜索) - - [2.1. BFS](#21-bfs) - - [2.2. DFS](#22-dfs) - - [2.2.1. DFS 的性质](#221-dfs-的性质) - - [2.3. 拓扑排序](#23-拓扑排序) - - [2.4. 强连通分量](#24-强连通分量) -- [3. 最小生成树](#3-最小生成树) - - [3.1. Kruskal 算法](#31-kruskal-算法) - - [3.2. Prim 算法](#32-prim-算法) -- [4. 单源最短路](#4-单源最短路) - - [4.1. 最短路的子路径也是最短路径](#41-最短路的子路径也是最短路径) - - [4.2. 负权重的边](#42-负权重的边) - - [4.3. 初始化](#43-初始化) - - [4.4. 松弛操作](#44-松弛操作) - - [4.5. 有向无环图的单源最短路问题](#45-有向无环图的单源最短路问题) - - [4.6. Bellman-Ford 算法](#46-bellman-ford-算法) - - [4.7. Dijkstra 算法](#47-dijkstra-算法) -- [5. 所有结点对的最短路问题](#5-所有结点对的最短路问题) - - [5.1. 矩阵乘法](#51-矩阵乘法) - - [5.2. Floyd-Warshall 算法](#52-floyd-warshall-算法) - - [5.3. Johnson 算法](#53-johnson-算法) -- [6. 最大流](#6-最大流) - - [6.1. 最大流最小截定理](#61-最大流最小截定理) - - [6.2. 多个源,汇](#62-多个源汇) - - [6.3. Ford-Fulkerson 方法](#63-ford-fulkerson-方法) - - [6.3.1. 残存网络](#631-残存网络) - - [6.3.2. 增广路径](#632-增广路径) - - [6.3.3. 割](#633-割) - - [6.4. 基本的 Ford-Fulkerson算法](#64-基本的-ford-fulkerson算法) - - [6.5. TBD](#65-tbd) -- [7. 参考资料](#7-参考资料) - - - - -# 1. 图 - -## 1.1. 概念 -* 顶 -* 顶点的度 d -* 边 -* 相邻 -* 重边 -* 环 -* 完全图: 所有顶都相邻 -* 二分图: $V(G) = X \cup Y, X\cap Y = \varnothing$, X中, Y 中任两顶不相邻 -* 轨道 -* 圈 - - -### 1.1.1. 性质 -* $\sum_{v\in V} d(v) = 2|E|$ -* G是二分图 $\Leftrightarrow$ G无奇圈 -* 树是无圈连通图 -* 树中, $|E| = |V| -1$ - - -## 1.2. 图的表示 -* 邻接矩阵 -* 邻接链表 -![](https://upload-images.jianshu.io/upload_images/7130568-57ce6db904992656.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 1.3. 树 -无圈连通图, $E = V-1$, 详细见[树](/tree.html), - - -# 2. 图的搜索 -Introduction to algorithm[^1] - -## 2.1. BFS -```python -for v in V: - v.d = MAX - v.pre = None - v.isFind = False -root. isFind = True -root.d = 0 -que = [root] -while que !=[]: - nd = que.pop(0) - for v in Adj(nd): - if not v.isFind : - v.d = nd.d+1 - v.pre = nd - v.isFind = True - que.append(v) -``` -时间复杂度 $O(V+E)$ - -## 2.2. DFS -$\Theta(V+E)$ -```python -def dfs(G): - time = 0 - for v in V: - v.pre = None - v.isFind = False - for v in V : # note this, - if not v.isFind: - dfsVisit(v) - def dfsVisit(G,u): - time =time+1 - u.begin = time - u.isFind = True - for v in Adj(u): - if not v.isFind: - v.pre = u - dfsVisit(G,v) - time +=1 - u.end = time -``` -begin, end 分别是结点的发现时间与完成时间 - -### 2.2.1. DFS 的性质 -* 其生成的前驱子图$G_{pre}$ 形成一个由多棵树构成的森林, 这是因为其与 dfsVisit 的递归调用树相对应 -* 括号化结构 -![](https://upload-images.jianshu.io/upload_images/7130568-ba62e68e5b883b6c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -* 括号化定理: - 考察两个结点的发现时间与结束时间的区间 [u,begin,u.end] 与 [v.begin,v.end] - * 如果两者没有交集, 则两个结点在两个不同的子树上(递归树) - * 如果 u 的区间包含在 v 的区间, 则 u 是v 的后代 - - -## 2.3. 拓扑排序 -利用 DFS, 结点的完成时间的逆序就是拓扑排序 - -同一个图可能有不同的拓扑排序 - -## 2.4. 强连通分量 -在有向图中, 强连通分量中的结点互达 -定义 $Grev$ 为 $G$ 中所有边反向后的图 - -将图分解成强连通分量的算法 -在 Grev 上根据 G 中结点的拓扑排序来 dfsVisit, 即 -```python -compute Grev -initalization -for v in topo-sort(G.V): - if not v.isFind: dfsVisit(Grev,v) -``` -然后得到的DFS 森林(也是递归树森林)中每个树就是一个强连通分量 - - -# 3. 最小生成树 -利用了贪心算法, -```python -Generate-Minimum-spanning-tree(G) - A = [] - while len(A)!=len(G.V)-1: - add a safe edge for A to A - return A -``` - -## 3.1. Kruskal 算法 -总体上, 从最开始 每个结点就是一颗树的森林中(不相交集合, 并查集), 逐渐添加不形成圈的(两个元素不再同一个集合),最小边权的边. -```python -edges=[] -for edge as u,v in sorted(G.E): - if find-set(u) != find-set(v): - edges.append(edge) - union(u,v) -return edges -``` -如果并查集的实现采用了 按秩合并与路径压缩技巧, 则 find 与 union 的时间接近常数 -所以时间复杂度在于排序边, 即 $O(ElgE)$, 而 $E\lt V^2$, 所以 $lgE = O(lgV)$, 时间复杂度为 $O(ElgV)$ - -## 3.2. Prim 算法 -用了 BFS, 类似 Dijkstra 算法 -从根结点开始 BFS, 一直保持成一颗树 -```python -for v in V: - v.minAdjEdge = MAX - v.pre = None -root.minAdjEdge = 0 -que = priority-queue (G.V) # sort by minAdjEdge -while not que.isempty(): - u = que.extractMin() - for v in Adj(u): - if v in que and v.minAdjEdge>w(u,v): - v.pre = u - v.minAdjEdge = w(u,v) -``` -* 建堆 $O(V)$ `//note it's v, not vlgv` -* 主循环中 - * extractMin: $O(VlgV)$ - * in 操作 可以另设标志位, 在常数时间完成, 总共 $O(E)$ - * 设置结点的 minAdjEdge, 需要$O(lgv)$, 循环 E 次,则 总共$O(ElgV)$ - -综上, 时间复杂度为$O(ElgV)$ -如果使用的是 [斐波那契堆](/fib-heap.html), 在 设置 minAdjEdge时 调用 `decrease-key`, 这个操作摊还代价为 $O(1)$, 所以时间复杂度可改进到 $O(E+VlgV)$ - - -# 4. 单源最短路 -求一个结点到其他结点的最短路径, 可以用 Bellman-ford算法, 或者 Dijkstra算法. -定义两个结点u,v间的最短路 -$$ -\delta(u,v) = \begin{cases} -\min(w(path)),\quad u\xrightarrow{path} v\\ -\infty, \quad u\nrightarrow v -\end{cases} -$$ -问题的变体 -* 单目的地最短路问题: 可以将所有边反向转换成求单源最短路问题 -* 单结点对的最短路径 -* 所有结点对最短路路径 - - -## 4.1. 最短路的子路径也是最短路径 -$p=(v_0,v_1,\ldots,v_k)$为从结点$v_0$到$v_k$的一条最短路径, 对于任意$0\le i\le j \le k$, 记$p_{ij}=(v_i,v_{i+1},\ldots,v_j)$为 p 中 $v_i$到$v_j$的子路径, 则 $p_{ij}$为 $v_i$到$v_j$的一条最短路径 - - -## 4.2. 负权重的边 -Dijkstra 算法不能处理负值边, 只能用 Bellman-Ford 算法, -而且如果有负值圈, 则没有最短路, bellman-ford算法也可以检测出来 - -## 4.3. 初始化 -```python -def initialaize(G,s): - for v in G.V: - v.pre = None - v.distance = MAX - s.distance = 0 -``` - -## 4.4. 松弛操作 -```python -def relax(u,v,w): - if v.distance > u.distance + w: - v.distance = u.distance + w: - v.pre = u -``` -性质 -* 三角不等式: $\delta(s,v) \leqslant \delta(s,u) + w(u,v)$ -* 上界: $v.distance \geqslant \delta(s,v)$ -* 收敛: 对于某些结点u,v 如果s->...->u->v是图G中的一条最短路径,并且在对边,进行松弛前任意时间有 $u.distance=\delta(s,u)$则在之后的所有时间有 $v.distance=\delta(s,v)$ -* 路径松弛性质: 如果$p=v_0 v_1 \ldots v_k$是从源结点下v0到结点vk的一条最短路径,并且对p中的边所进行松弛的次序为$(v_0,v_1),(v_1,v_2), \ldots ,(v_{k-1},v_k)$, 则 $v_k.distance = \delta(s,v_k)$ -该性质的成立与任何其他的松弛操作无关,即使这些松弛操作是与对p上的边所进行的松弛操作穿插进行的。 - -证明 -![](https://upload-images.jianshu.io/upload_images/7130568-424a6929bd389825.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 4.5. 有向无环图的单源最短路问题 -$\Theta(V+E)$ -```python -def dag-shortest-path(G,s): - initialize(G,s) - for u in topo-sort(G.V): - for v in Adj(v): - relax(u,v,w(u,v)) -``` - -## 4.6. Bellman-Ford 算法 -$O(VE)$ -```python -def bellman-ford(G,s): - initialize(G,s) - for ct in range(|V|-1): # v-1 times - for u,v as edge in E: - relax(u,v,w(u,v)) - for u,v as edge in E: - if v.distance > u.distance + w(u,v): - return False - return True -``` -第一个 for 循环就是进行松弛操作, 最后结果已经存储在 结点的distance 和 pre 属性中了, 第二个 for 循环利用三角不等式检查有不有负值圈. - -下面是证明该算法的正确性![](https://upload-images.jianshu.io/upload_images/7130568-f84e00ac35aadc81.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 4.7. Dijkstra 算法 -$O(ElogV)$, 要求不能有负值边 - -Dijkstra算法既类似于广度优先搜索(,也有点类似于计算最小生成树的Prim算法。它与广度优先搜索的类似点在于集合S对应的是广度优先搜索中的黑色结点集合:正如集合S中的结点的最短路径权重已经计算出来一样,在广度优先搜索中,黑色结点的正确的广度优先距离也已经计算出来。Dijkstra算法像Prim算法的地方是,两个算法都使用最小优先队列来寻找给定集合(Dijkstra算法中的S集合与Prim算法中逐步增长的树)之外的“最轻”结点,将该结点加入到集合里,并对位于集合外面的结点的权重进行相应调整。 - -```python -def dijkstra(G,s): - initialize(G,s) - paths=[] - q = priority-queue(G.V) # sort by distance - while not q.empty(): - u = q.extract-min() - paths.append(u) - for v in Adj(u): - relax(u,v,w(u,v)) -``` - - -# 5. 所有结点对的最短路问题 - -## 5.1. 矩阵乘法 -使用动态规划算法, 可以得到最短路径的结构 -设 $l_{ij}^{(m)}$为从结点i 到结点 j 的至多包含 m 条边的任意路径的最小权重,当m = 0, 此时i=j, 则 为0, -可以得到递归定义 - $$ -l_{ij}^{(m)} =\min( l_{ij}^{(m-1)}, \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj})) = \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj})) -$$ -由于对于所有 j, 有 $w_{jj}=0$,所以上式后面的等式成立. - -由于是简单路径, 则包含的边最多为 |V|-1 条, 所以 -$$ -\delta(i,j) = l_{ij}^{(|V|-1)} = l_{ij}^{(|V|)} =l_{ij}^{(|V| + 1)}= ... -$$ -所以可以从自底向上计算, 如下 -输入权值矩阵 $W(w_{ij})), L^{(m-1)}$,输出$ L^{(m)}$, 其中 $L^{(1)} = W$, -```python -def f(L, W): - n = L.rows - L_new = new matrix(row=n ,col = n) - for i in range(n): - for j in range(n): - L_new[i][j] = MAX - for k in range(n): - L_new[i][j] = min(L_new[i][j], L[i][k]+w[k][j]) - return L_new -``` -可以看出该算法与矩阵乘法的关系 -$L^{(m)} = W^m$, -所以可以直接计算乘法, 每次计算一个乘积是 $O(V^3)$, 计算 V 次, 所以总体 $O(V^4)$, 使用矩阵快速幂可以将时间复杂度降低为$O(V^3lgV)$ -```python -def f(W): - L = W - i = 1 - while i -## 5.2. Floyd-Warshall 算法 -同样要求可以存在负权边, 但不能有负值圈. 用动态规划算法: -设 $ d_{ij}^{(k)}$ 为 从 i 到 j 所有中间结点来自集合 ${\{1,2,\ldots,k\}}$ 的一条最短路径的权重. 则有 -$$ -d_{ij}^{(k)} = \begin{cases} -w_{ij},\quad k=0\\ -min(d_{ij}^{(k-1)},d_{ik}^{(k-1)}+d_{kj}^{(k-1)}),\quad k\geqslant 1 -\end{cases} -$$ -而且为了找出路径, 需要记录前驱结点, 定义如下前驱矩阵 $\Pi$, 设 $ \pi_{ij}^{(k)}$ 为 从 i 到 j 所有中间结点来自集合 ${\{1,2,\ldots,k\}}$ 的最短路径上 j 的前驱结点 -则 -$$ -\pi_{ij}^{(0)} = \begin{cases} -nil,\quad i=j \ or \ w_{ij}=\infty \\ -i, \quad i\neq j\ and \ w_{ij}<\infty -\end{cases} -$$ -对 $k\geqslant 1$ -$$ -\pi_{ij}^{(k)} = \begin{cases} -\pi_{ij}^{(k-1)} ,\quad d_{ij}^{(k-1)}\leqslant d_{ik}^{(k-1)}+d_{kj}^{(k-1)}\\ -\pi_{kj}^{(k-1)} ,\quad otherwise -\end{cases} -$$ - -由此得出此算法 -```python -def floyd-warshall(W): - n = len(W) - D= W - initialize pre - for k in range(n): - pre2 = pre.copy() - for i in range(n): - for j in range(n) - if d[i][j] > d[i][k]+d[k][j]: - d[i][j] =d[i][k]+d[k][j] - pre2[i][j] = pre[k][j] - pre = pre2 -return d,pre -``` - -## 5.3. Johnson 算法 -思路是通过重新赋予权重, 将图中负权边转换为正权,然后就可以用 dijkstra 算法(要求是正值边)来计算一个结点到其他所有结点的, 然后对所有结点用dijkstra - -1. 首先构造一个新图 G' - 先将G拷贝到G', 再添加一个新结点 s, 添加 G.V条边, s 到G中顶点的, 权赋值为 0 -2. 用 Bellman-Ford 算法检查是否有负值圈, 如果没有, 同时求出 $\delta(s,v) 记为 h(v)$ -3. 求新的非负值权, w'(u,v) = w(u,v)+h(u)-h(v) -4. 对所有结点在 新的权矩阵w'上 用 Dijkstra 算法 -![image.png](https://upload-images.jianshu.io/upload_images/7130568-6c2146ad64d692f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -```python -JOHNSON (G, u) - -s = newNode -G' = G.copy() -G'.addNode(s) -for v in G.V: G'.addArc(s,v,w=0) - -if BELLMAN-FORD(G' , w, s) ==FALSE - error "the input graph contains a negative-weight cycle" - -for v in G'.V: - # computed by the bellman-ford algorithm, delta(s,v) is the shortest distance from s to v - h(v) = delta(s,v) -for edge(u,v) in G'.E: - w' = w(u,v)+h(u)-h(v) -d = matrix(n,n) -for u in G: - dijkstra(G,w',u) # compute delta' for all v in G.V - for v in G.V: - d[u][v] = delta'(u,v) + h(v)-h(u) -return d -``` - -# 6. 最大流 -G 是弱连通严格有向加权图, s为源, t 为汇, 每条边e容量 c(e), 由此定义了网络N(G,s,t,c(e)), -* 流函数 $f(e):E \rightarrow R$ -$$ -\begin{aligned} -(1)\quad & 0\leqslant f(e) \leqslant c(e),\quad e \in E\\ -(2)\quad & \sum_{e\in \alpha(v)} f(e)= \sum_{e\in \beta(v)}f(e),\quad v \in V-\{s,t\} -\end{aligned} -$$ -其中 $\alpha(v)$ 是以 v 为头的边集, $\beta(v)$是以 v 为尾的边集 -* 流量: $F = \sum_{e\in \alpha(t)} f(e)- \sum_{e\in -\beta(t)}f(e),$ -* 截$(S,\overline S)$: $S\subset V,s\in S, t\in \overline S =V-S$ -* 截量$C(S) = \sum_{e\in(S,\overline S)}c(e)$ - -## 6.1. 最大流最小截定理 -<<图论>> 王树禾[^2] -* 对于任一截$(S,\overline S)$, 有 $F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),$ -![prove](https://upload-images.jianshu.io/upload_images/7130568-19bf6cc3c7d6ce06.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -* $F\leqslant C(S)$ -证明: 由上面定理 - $$F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),$$ -而 $0\leqslant f(e) \leqslant c(e)$, 则 -$$F\leqslant \sum_{e\in (S,\overline S)} f(e) \leqslant \sum_{e\in (S,\overline S)} c(e) = C(S) $$ -* 最大流,最小截: 若$F= C(S) $, 则F'是最大流量, C(S) 是最小截量 - -## 6.2. 多个源,汇 -可以新增一个总的源,一个总的汇, -![](https://upload-images.jianshu.io/upload_images/7130568-3e9e87fdf9655883.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 6.3. Ford-Fulkerson 方法 -由于其实现可以有不同的运行时间, 所以称其为方法, 而不是算法. -思路是 循环增加流的值, 在一个关联的"残存网络" 中寻找一条"增广路径", 然后对这些边进行修改流量. 重复直至残存网络上不再存在增高路径为止. -```python -def ford-fulkerson(G,s,t): - initialize flow f to 0 - while exists an augmenting path p in residual network Gf: - augment flow f along p - return f -``` - -### 6.3.1. 残存网络 -![](https://upload-images.jianshu.io/upload_images/7130568-c74a571b9121dbbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -### 6.3.2. 增广路径 -![](https://upload-images.jianshu.io/upload_images/7130568-b9e841cfa4d04b57.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 6.3.3. 割 -![](https://upload-images.jianshu.io/upload_images/7130568-74b065e86eb285b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 6.4. 基本的 Ford-Fulkerson算法 -```python -def ford-fulkerson(G,s,t): - for edge in G.E: edge.f = 0 - while exists path p:s->t in Gf: - cf(p) = min{cf(u,v):(u,v) is in p} - for edge in p: - if edge in E: - edge.f +=cf(p) - else: reverse_edge.f -=cf(p) -``` - - -## 6.5. TBD - - -# 7. 参考资料 -[^1]: 算法导论 -[^2]: 图论, 王树禾 +--- +title: 图算法 +date: 2018-09-06 19:10 +categories: 数据结构与算法 +tags: [图,算法] +keywords: 图,算法 +mathjax: true +description: "算法导论上常用的图算法, 代码, 原理等" + +--- + + + +- [1. 图](#1-图) + - [1.1. 概念](#11-概念) + - [1.1.1. 性质](#111-性质) + - [1.2. 图的表示](#12-图的表示) + - [1.3. 树](#13-树) +- [2. 图的搜索](#2-图的搜索) + - [2.1. BFS](#21-bfs) + - [2.2. DFS](#22-dfs) + - [2.2.1. DFS 的性质](#221-dfs-的性质) + - [2.3. 拓扑排序](#23-拓扑排序) + - [2.4. 强连通分量](#24-强连通分量) +- [3. 最小生成树](#3-最小生成树) + - [3.1. Kruskal 算法](#31-kruskal-算法) + - [3.2. Prim 算法](#32-prim-算法) +- [4. 单源最短路](#4-单源最短路) + - [4.1. 最短路的子路径也是最短路径](#41-最短路的子路径也是最短路径) + - [4.2. 负权重的边](#42-负权重的边) + - [4.3. 初始化](#43-初始化) + - [4.4. 松弛操作](#44-松弛操作) + - [4.5. 有向无环图的单源最短路问题](#45-有向无环图的单源最短路问题) + - [4.6. Bellman-Ford 算法](#46-bellman-ford-算法) + - [4.7. Dijkstra 算法](#47-dijkstra-算法) +- [5. 所有结点对的最短路问题](#5-所有结点对的最短路问题) + - [5.1. 矩阵乘法](#51-矩阵乘法) + - [5.2. Floyd-Warshall 算法](#52-floyd-warshall-算法) + - [5.3. Johnson 算法](#53-johnson-算法) +- [6. 最大流](#6-最大流) + - [6.1. 最大流最小截定理](#61-最大流最小截定理) + - [6.2. 多个源,汇](#62-多个源汇) + - [6.3. Ford-Fulkerson 方法](#63-ford-fulkerson-方法) + - [6.3.1. 残存网络](#631-残存网络) + - [6.3.2. 增广路径](#632-增广路径) + - [6.3.3. 割](#633-割) + - [6.4. 基本的 Ford-Fulkerson算法](#64-基本的-ford-fulkerson算法) + - [6.5. TBD](#65-tbd) +- [7. 参考资料](#7-参考资料) + + + + +# 1. 图 + +## 1.1. 概念 +* 顶 +* 顶点的度 d +* 边 +* 相邻 +* 重边 +* 环 +* 完全图: 所有顶都相邻 +* 二分图: ![](https://latex.codecogs.com/gif.latex?V(G)&space;=&space;X&space;\cup&space;Y,&space;X\cap&space;Y&space;=&space;\varnothing), X中, Y 中任两顶不相邻 +* 轨道 +* 圈 + + +### 1.1.1. 性质 +* ![](https://latex.codecogs.com/gif.latex?\sum_{v\in&space;V}&space;d(v)&space;=&space;2|E|) +* G是二分图 ![](https://latex.codecogs.com/gif.latex?\Leftrightarrow) G无奇圈 +* 树是无圈连通图 +* 树中, ![](https://latex.codecogs.com/gif.latex?|E|&space;=&space;|V|&space;-1) + + +## 1.2. 图的表示 +* 邻接矩阵 +* 邻接链表 +![](https://upload-images.jianshu.io/upload_images/7130568-57ce6db904992656.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 1.3. 树 +无圈连通图, ![](https://latex.codecogs.com/gif.latex?E&space;=&space;V-1), 详细见[树](/tree.html), + + +# 2. 图的搜索 +Introduction to algorithm[^1] + +## 2.1. BFS +```python +for v in V: + v.d = MAX + v.pre = None + v.isFind = False +root. isFind = True +root.d = 0 +que = [root] +while que !=[]: + nd = que.pop(0) + for v in Adj(nd): + if not v.isFind : + v.d = nd.d+1 + v.pre = nd + v.isFind = True + que.append(v) +``` +时间复杂度 ![](https://latex.codecogs.com/gif.latex?O(V+E)) + +## 2.2. DFS +![](https://latex.codecogs.com/gif.latex?\Theta(V+E)) +```python +def dfs(G): + time = 0 + for v in V: + v.pre = None + v.isFind = False + for v in V : # note this, + if not v.isFind: + dfsVisit(v) + def dfsVisit(G,u): + time =time+1 + u.begin = time + u.isFind = True + for v in Adj(u): + if not v.isFind: + v.pre = u + dfsVisit(G,v) + time +=1 + u.end = time +``` +begin, end 分别是结点的发现时间与完成时间 + +### 2.2.1. DFS 的性质 +* 其生成的前驱子图![](https://latex.codecogs.com/gif.latex?G_{pre}) 形成一个由多棵树构成的森林, 这是因为其与 dfsVisit 的递归调用树相对应 +* 括号化结构 +![](https://upload-images.jianshu.io/upload_images/7130568-ba62e68e5b883b6c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +* 括号化定理: + 考察两个结点的发现时间与结束时间的区间 [u,begin,u.end] 与 [v.begin,v.end] + * 如果两者没有交集, 则两个结点在两个不同的子树上(递归树) + * 如果 u 的区间包含在 v 的区间, 则 u 是v 的后代 + + +## 2.3. 拓扑排序 +利用 DFS, 结点的完成时间的逆序就是拓扑排序 + +同一个图可能有不同的拓扑排序 + +## 2.4. 强连通分量 +在有向图中, 强连通分量中的结点互达 +定义 ![](https://latex.codecogs.com/gif.latex?Grev) 为 ![](https://latex.codecogs.com/gif.latex?G) 中所有边反向后的图 + +将图分解成强连通分量的算法 +在 Grev 上根据 G 中结点的拓扑排序来 dfsVisit, 即 +```python +compute Grev +initalization +for v in topo-sort(G.V): + if not v.isFind: dfsVisit(Grev,v) +``` +然后得到的DFS 森林(也是递归树森林)中每个树就是一个强连通分量 + + +# 3. 最小生成树 +利用了贪心算法, +```python +Generate-Minimum-spanning-tree(G) + A = [] + while len(A)!=len(G.V)-1: + add a safe edge for A to A + return A +``` + +## 3.1. Kruskal 算法 +总体上, 从最开始 每个结点就是一颗树的森林中(不相交集合, 并查集), 逐渐添加不形成圈的(两个元素不再同一个集合),最小边权的边. +```python +edges=[] +for edge as u,v in sorted(G.E): + if find-set(u) != find-set(v): + edges.append(edge) + union(u,v) +return edges +``` +如果并查集的实现采用了 按秩合并与路径压缩技巧, 则 find 与 union 的时间接近常数 +所以时间复杂度在于排序边, 即 ![](https://latex.codecogs.com/gif.latex?O(ElgE)), 而 ![](https://latex.codecogs.com/gif.latex?E\lt&space;V^2), 所以 ![](https://latex.codecogs.com/gif.latex?lgE&space;=&space;O(lgV)), 时间复杂度为 ![](https://latex.codecogs.com/gif.latex?O(ElgV)) + +## 3.2. Prim 算法 +用了 BFS, 类似 Dijkstra 算法 +从根结点开始 BFS, 一直保持成一颗树 +```python +for v in V: + v.minAdjEdge = MAX + v.pre = None +root.minAdjEdge = 0 +que = priority-queue (G.V) # sort by minAdjEdge +while not que.isempty(): + u = que.extractMin() + for v in Adj(u): + if v in que and v.minAdjEdge>w(u,v): + v.pre = u + v.minAdjEdge = w(u,v) +``` +* 建堆 ![](https://latex.codecogs.com/gif.latex?O(V)) `//note it's v, not vlgv` +* 主循环中 + * extractMin: ![](https://latex.codecogs.com/gif.latex?O(VlgV)) + * in 操作 可以另设标志位, 在常数时间完成, 总共 ![](https://latex.codecogs.com/gif.latex?O(E)) + * 设置结点的 minAdjEdge, 需要![](https://latex.codecogs.com/gif.latex?O(lgv)), 循环 E 次,则 总共![](https://latex.codecogs.com/gif.latex?O(ElgV)) + +综上, 时间复杂度为![](https://latex.codecogs.com/gif.latex?O(ElgV)) +如果使用的是 [斐波那契堆](/fib-heap.html), 在 设置 minAdjEdge时 调用 `decrease-key`, 这个操作摊还代价为 ![](https://latex.codecogs.com/gif.latex?O(1)), 所以时间复杂度可改进到 ![](https://latex.codecogs.com/gif.latex?O(E+VlgV)) + + +# 4. 单源最短路 +求一个结点到其他结点的最短路径, 可以用 Bellman-ford算法, 或者 Dijkstra算法. +定义两个结点u,v间的最短路 +![](https://latex.codecogs.com/gif.latex?&space;\delta(u,v)&space;=&space;\begin{cases}&space;\min(w(path)),\quad&space;u\xrightarrow{path}&space;v\\&space;\infty,&space;\quad&space;u&space;rightarrow&space;v&space;\end{cases}&space;) +问题的变体 +* 单目的地最短路问题: 可以将所有边反向转换成求单源最短路问题 +* 单结点对的最短路径 +* 所有结点对最短路路径 + + +## 4.1. 最短路的子路径也是最短路径 +![](https://latex.codecogs.com/gif.latex?p=(v_0,v_1,\ldots,v_k))为从结点![](https://latex.codecogs.com/gif.latex?v_0)到![](https://latex.codecogs.com/gif.latex?v_k)的一条最短路径, 对于任意![](https://latex.codecogs.com/gif.latex?0\le&space;i\le&space;j&space;\le&space;k), 记![](https://latex.codecogs.com/gif.latex?p_{ij}=(v_i,v_{i+1},\ldots,v_j))为 p 中 ![](https://latex.codecogs.com/gif.latex?v_i)到![](https://latex.codecogs.com/gif.latex?v_j)的子路径, 则 ![](https://latex.codecogs.com/gif.latex?p_{ij})为 ![](https://latex.codecogs.com/gif.latex?v_i)到![](https://latex.codecogs.com/gif.latex?v_j)的一条最短路径 + + +## 4.2. 负权重的边 +Dijkstra 算法不能处理负值边, 只能用 Bellman-Ford 算法, +而且如果有负值圈, 则没有最短路, bellman-ford算法也可以检测出来 + +## 4.3. 初始化 +```python +def initialaize(G,s): + for v in G.V: + v.pre = None + v.distance = MAX + s.distance = 0 +``` + +## 4.4. 松弛操作 +```python +def relax(u,v,w): + if v.distance > u.distance + w: + v.distance = u.distance + w: + v.pre = u +``` +性质 +* 三角不等式: ![](https://latex.codecogs.com/gif.latex?\delta(s,v)&space;\leqslant&space;\delta(s,u)&space;+&space;w(u,v)) +* 上界: ![](https://latex.codecogs.com/gif.latex?v.distance&space;\geqslant&space;\delta(s,v)) +* 收敛: 对于某些结点u,v 如果s->...->u->v是图G中的一条最短路径,并且在对边,进行松弛前任意时间有 ![](https://latex.codecogs.com/gif.latex?u.distance=\delta(s,u))则在之后的所有时间有 ![](https://latex.codecogs.com/gif.latex?v.distance=\delta(s,v)) +* 路径松弛性质: 如果![](https://latex.codecogs.com/gif.latex?p=v_0&space;v_1&space;\ldots&space;v_k)是从源结点下v0到结点vk的一条最短路径,并且对p中的边所进行松弛的次序为![](https://latex.codecogs.com/gif.latex?(v_0,v_1),(v_1,v_2),&space;\ldots&space;,(v_{k-1},v_k)), 则 ![](https://latex.codecogs.com/gif.latex?v_k.distance&space;=&space;\delta(s,v_k)) +该性质的成立与任何其他的松弛操作无关,即使这些松弛操作是与对p上的边所进行的松弛操作穿插进行的。 + +证明 +![](https://upload-images.jianshu.io/upload_images/7130568-424a6929bd389825.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 4.5. 有向无环图的单源最短路问题 +![](https://latex.codecogs.com/gif.latex?\Theta(V+E)) +```python +def dag-shortest-path(G,s): + initialize(G,s) + for u in topo-sort(G.V): + for v in Adj(v): + relax(u,v,w(u,v)) +``` + +## 4.6. Bellman-Ford 算法 +![](https://latex.codecogs.com/gif.latex?O(VE)) +```python +def bellman-ford(G,s): + initialize(G,s) + for ct in range(|V|-1): # v-1 times + for u,v as edge in E: + relax(u,v,w(u,v)) + for u,v as edge in E: + if v.distance > u.distance + w(u,v): + return False + return True +``` +第一个 for 循环就是进行松弛操作, 最后结果已经存储在 结点的distance 和 pre 属性中了, 第二个 for 循环利用三角不等式检查有不有负值圈. + +下面是证明该算法的正确性![](https://upload-images.jianshu.io/upload_images/7130568-f84e00ac35aadc81.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 4.7. Dijkstra 算法 +![](https://latex.codecogs.com/gif.latex?O(ElogV)), 要求不能有负值边 + +Dijkstra算法既类似于广度优先搜索(,也有点类似于计算最小生成树的Prim算法。它与广度优先搜索的类似点在于集合S对应的是广度优先搜索中的黑色结点集合:正如集合S中的结点的最短路径权重已经计算出来一样,在广度优先搜索中,黑色结点的正确的广度优先距离也已经计算出来。Dijkstra算法像Prim算法的地方是,两个算法都使用最小优先队列来寻找给定集合(Dijkstra算法中的S集合与Prim算法中逐步增长的树)之外的“最轻”结点,将该结点加入到集合里,并对位于集合外面的结点的权重进行相应调整。 + +```python +def dijkstra(G,s): + initialize(G,s) + paths=[] + q = priority-queue(G.V) # sort by distance + while not q.empty(): + u = q.extract-min() + paths.append(u) + for v in Adj(u): + relax(u,v,w(u,v)) +``` + + +# 5. 所有结点对的最短路问题 + +## 5.1. 矩阵乘法 +使用动态规划算法, 可以得到最短路径的结构 +设 ![](https://latex.codecogs.com/gif.latex?l_{ij}^{(m)})为从结点i 到结点 j 的至多包含 m 条边的任意路径的最小权重,当m = 0, 此时i=j, 则 为0, +可以得到递归定义 + ![](https://latex.codecogs.com/gif.latex?&space;l_{ij}^{(m)}&space;=\min(&space;l_{ij}^{(m-1)},&space;\min_{1\leqslant&space;k\leqslant&space;n}(&space;l_{ik}^{(m-1)}+w_{kj}))&space;=&space;\min_{1\leqslant&space;k\leqslant&space;n}(&space;l_{ik}^{(m-1)}+w_{kj}))&space;) +由于对于所有 j, 有 ![](https://latex.codecogs.com/gif.latex?w_{jj}=0),所以上式后面的等式成立. + +由于是简单路径, 则包含的边最多为 |V|-1 条, 所以 +![](https://latex.codecogs.com/gif.latex?&space;\delta(i,j)&space;=&space;l_{ij}^{(|V|-1)}&space;=&space;l_{ij}^{(|V|)}&space;=l_{ij}^{(|V|&space;+&space;1)}=&space;...&space;) +所以可以从自底向上计算, 如下 +输入权值矩阵 ![](https://latex.codecogs.com/gif.latex?W(w_{ij})),&space;L^{(m-1)}),输出![](https://latex.codecogs.com/gif.latex?L^{(m)}), 其中 ![](https://latex.codecogs.com/gif.latex?L^{(1)}&space;=&space;W), +```python +def f(L, W): + n = L.rows + L_new = new matrix(row=n ,col = n) + for i in range(n): + for j in range(n): + L_new[i][j] = MAX + for k in range(n): + L_new[i][j] = min(L_new[i][j], L[i][k]+w[k][j]) + return L_new +``` +可以看出该算法与矩阵乘法的关系 +![](https://latex.codecogs.com/gif.latex?L^{(m)}&space;=&space;W^m), +所以可以直接计算乘法, 每次计算一个乘积是 ![](https://latex.codecogs.com/gif.latex?O(V^3)), 计算 V 次, 所以总体 ![](https://latex.codecogs.com/gif.latex?O(V^4)), 使用矩阵快速幂可以将时间复杂度降低为![](https://latex.codecogs.com/gif.latex?O(V^3lgV)) +```python +def f(W): + L = W + i = 1 + while i +## 5.2. Floyd-Warshall 算法 +同样要求可以存在负权边, 但不能有负值圈. 用动态规划算法: +设 ![](https://latex.codecogs.com/gif.latex?d_{ij}^{(k)}) 为 从 i 到 j 所有中间结点来自集合 ![](https://latex.codecogs.com/gif.latex?{\{1,2,\ldots,k\}}) 的一条最短路径的权重. 则有 +![](https://latex.codecogs.com/gif.latex?&space;d_{ij}^{(k)}&space;=&space;\begin{cases}&space;w_{ij},\quad&space;k=0\\&space;min(d_{ij}^{(k-1)},d_{ik}^{(k-1)}+d_{kj}^{(k-1)}),\quad&space;k\geqslant&space;1&space;\end{cases}&space;) +而且为了找出路径, 需要记录前驱结点, 定义如下前驱矩阵 ![](https://latex.codecogs.com/gif.latex?\Pi), 设 ![](https://latex.codecogs.com/gif.latex?\pi_{ij}^{(k)}) 为 从 i 到 j 所有中间结点来自集合 ![](https://latex.codecogs.com/gif.latex?{\{1,2,\ldots,k\}}) 的最短路径上 j 的前驱结点 +则 +![](https://latex.codecogs.com/gif.latex?&space;\pi_{ij}^{(0)}&space;=&space;\begin{cases}&space;nil,\quad&space;i=j&space;\&space;or&space;\&space;w_{ij}=\infty&space;\\&space;i,&space;\quad&space;i&space;eq&space;j\&space;and&space;\&space;w_{ij}<\infty&space;\end{cases}&space;) +对 ![](https://latex.codecogs.com/gif.latex?k\geqslant&space;1) +![](https://latex.codecogs.com/gif.latex?&space;\pi_{ij}^{(k)}&space;=&space;\begin{cases}&space;\pi_{ij}^{(k-1)}&space;,\quad&space;d_{ij}^{(k-1)}\leqslant&space;d_{ik}^{(k-1)}+d_{kj}^{(k-1)}\\&space;\pi_{kj}^{(k-1)}&space;,\quad&space;otherwise&space;\end{cases}&space;) + +由此得出此算法 +```python +def floyd-warshall(W): + n = len(W) + D= W + initialize pre + for k in range(n): + pre2 = pre.copy() + for i in range(n): + for j in range(n) + if d[i][j] > d[i][k]+d[k][j]: + d[i][j] =d[i][k]+d[k][j] + pre2[i][j] = pre[k][j] + pre = pre2 +return d,pre +``` + +## 5.3. Johnson 算法 +思路是通过重新赋予权重, 将图中负权边转换为正权,然后就可以用 dijkstra 算法(要求是正值边)来计算一个结点到其他所有结点的, 然后对所有结点用dijkstra + +1. 首先构造一个新图 G' + 先将G拷贝到G', 再添加一个新结点 s, 添加 G.V条边, s 到G中顶点的, 权赋值为 0 +2. 用 Bellman-Ford 算法检查是否有负值圈, 如果没有, 同时求出 ![](https://latex.codecogs.com/gif.latex?\delta(s,v)&space;Recorded-as&space;h(v)) +3. 求新的非负值权, w'(u,v) = w(u,v)+h(u)-h(v) +4. 对所有结点在 新的权矩阵w'上 用 Dijkstra 算法 +![image.png](https://upload-images.jianshu.io/upload_images/7130568-6c2146ad64d692f3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +```python +JOHNSON (G, u) + +s = newNode +G' = G.copy() +G'.addNode(s) +for v in G.V: G'.addArc(s,v,w=0) + +if BELLMAN-FORD(G' , w, s) ==FALSE + error "the input graph contains a negative-weight cycle" + +for v in G'.V: + # computed by the bellman-ford algorithm, delta(s,v) is the shortest distance from s to v + h(v) = delta(s,v) +for edge(u,v) in G'.E: + w' = w(u,v)+h(u)-h(v) +d = matrix(n,n) +for u in G: + dijkstra(G,w',u) # compute delta' for all v in G.V + for v in G.V: + d[u][v] = delta'(u,v) + h(v)-h(u) +return d +``` + +# 6. 最大流 +G 是弱连通严格有向加权图, s为源, t 为汇, 每条边e容量 c(e), 由此定义了网络N(G,s,t,c(e)), +* 流函数 ![](https://latex.codecogs.com/gif.latex?f(e):E&space;\rightarrow&space;R) +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;(1)\quad&space;&&space;0\leqslant&space;f(e)&space;\leqslant&space;c(e),\quad&space;e&space;\in&space;E\\&space;(2)\quad&space;&&space;\sum_{e\in&space;\alpha(v)}&space;f(e)=&space;\sum_{e\in&space;\beta(v)}f(e),\quad&space;v&space;\in&space;V-\{s,t\}&space;\end{aligned}&space;) +其中 ![](https://latex.codecogs.com/gif.latex?\alpha(v)) 是以 v 为头的边集, ![](https://latex.codecogs.com/gif.latex?\beta(v))是以 v 为尾的边集 +* 流量: ![](https://latex.codecogs.com/gif.latex?F&space;=&space;\sum_{e\in&space;\alpha(t)}&space;f(e)-&space;\sum_{e\in&space;-\beta(t)}f(e),) +* 截![](https://latex.codecogs.com/gif.latex?(S,\overline&space;S)): ![](https://latex.codecogs.com/gif.latex?S\subset&space;V,s\in&space;S,&space;t\in&space;\overline&space;S&space;=V-S) +* 截量![](https://latex.codecogs.com/gif.latex?C(S)&space;=&space;\sum_{e\in(S,\overline&space;S)}c(e)) + +## 6.1. 最大流最小截定理 +<<图论>> 王树禾[^2] +* 对于任一截![](https://latex.codecogs.com/gif.latex?(S,\overline&space;S)), 有 ![](https://latex.codecogs.com/gif.latex?F&space;=&space;\sum_{e\in&space;(S,\overline&space;S)}&space;f(e)-&space;\sum_{e\in(\overline&space;S,S)}f(e),) +![prove](https://upload-images.jianshu.io/upload_images/7130568-19bf6cc3c7d6ce06.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +* ![](https://latex.codecogs.com/gif.latex?F\leqslant&space;C(S)) +证明: 由上面定理 + ![](https://latex.codecogs.com/gif.latex?F&space;=&space;\sum_{e\in&space;(S,\overline&space;S)}&space;f(e)-&space;\sum_{e\in(\overline&space;S,S)}f(e),) +而 ![](https://latex.codecogs.com/gif.latex?0\leqslant&space;f(e)&space;\leqslant&space;c(e)), 则 +![](https://latex.codecogs.com/gif.latex?F\leqslant&space;\sum_{e\in&space;(S,\overline&space;S)}&space;f(e)&space;\leqslant&space;\sum_{e\in&space;(S,\overline&space;S)}&space;c(e)&space;=&space;C(S)) +* 最大流,最小截: 若![](https://latex.codecogs.com/gif.latex?F=&space;C(S)), 则F'是最大流量, C(S) 是最小截量 + +## 6.2. 多个源,汇 +可以新增一个总的源,一个总的汇, +![](https://upload-images.jianshu.io/upload_images/7130568-3e9e87fdf9655883.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +## 6.3. Ford-Fulkerson 方法 +由于其实现可以有不同的运行时间, 所以称其为方法, 而不是算法. +思路是 循环增加流的值, 在一个关联的"残存网络" 中寻找一条"增广路径", 然后对这些边进行修改流量. 重复直至残存网络上不再存在增高路径为止. +```python +def ford-fulkerson(G,s,t): + initialize flow f to 0 + while exists an augmenting path p in residual network Gf: + augment flow f along p + return f +``` + +### 6.3.1. 残存网络 +![](https://upload-images.jianshu.io/upload_images/7130568-c74a571b9121dbbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +### 6.3.2. 增广路径 +![](https://upload-images.jianshu.io/upload_images/7130568-b9e841cfa4d04b57.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +### 6.3.3. 割 +![](https://upload-images.jianshu.io/upload_images/7130568-74b065e86eb285b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 6.4. 基本的 Ford-Fulkerson算法 +```python +def ford-fulkerson(G,s,t): + for edge in G.E: edge.f = 0 + while exists path p:s->t in Gf: + cf(p) = min{cf(u,v):(u,v) is in p} + for edge in p: + if edge in E: + edge.f +=cf(p) + else: reverse_edge.f -=cf(p) +``` + + +## 6.5. TBD + + +# 7. 参考资料 +[^1]: 算法导论 +[^2]: 图论, 王树禾 diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/hashTable.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/hashTable.md" index 0e80135..7192d64 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/hashTable.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/hashTable.md" @@ -29,7 +29,7 @@ description: "散列表的原理与实现, 包括直接寻址, 链接法, 开放 -哈希表 (hash table) , 可以实现 $O(1)$ 的 read, write, update +哈希表 (hash table) , 可以实现 ![](https://latex.codecogs.com/gif.latex?O(1)) 的 read, write, update 相对应 python 中的 dict, c语言中的 map 其实数组也能实现, 只是数组用来索引的关键字是下标, 是整数. @@ -43,7 +43,7 @@ description: "散列表的原理与实现, 包括直接寻址, 链接法, 开放 # 2. 映射 ## 2.1. 散列函数(hash) -将关键字 k 进行映射, 映射函数 $h$, 映射后的数组地址 $h(k)$. +将关键字 k 进行映射, 映射函数 ![](https://latex.codecogs.com/gif.latex?h), 映射后的数组地址 ![](https://latex.codecogs.com/gif.latex?h(k)). ### 2.1.1. 简单一致散列 @@ -53,14 +53,7 @@ description: "散列表的原理与实现, 包括直接寻址, 链接法, 开放 好的散列函数应 满足简单一致假设 例如 -$$ -\begin{aligned} -&(1) \text{除法散列} \quad h(k) = k \ mod\ m \\ -&(2) \text{乘法散列} \quad h(k) = \lfloor {m(kA \ mod\ 1)\rfloor} \text{,(0< A< 1)}\\ -&\quad\text{任何 A 都适用,最佳的选择与散列的数据特征有关.}\\ -&\quad\text{ Knuth 认为,最理想的是黄金分割数}\frac{\sqrt{5} -1}{2} \approx 0.618 -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;&(1)&space;\text{Division-hash}&space;\quad&space;h(k)&space;=&space;k&space;\&space;mod\&space;m&space;\\&space;&(2)&space;\text{Multiplication-hash}&space;\quad&space;h(k)&space;=&space;\lfloor&space;{m(kA&space;\&space;mod\&space;1)\rfloor}&space;\text{,(0<&space;A<&space;1)}\\&space;&\quad\text{Any-All-Access-Pass&space;A&space;Both-apply,The-best-choice-is-related-to-the-hashed-data-characteristics.}\\&space;&\quad\text{&space;Knuth&space;Holding:,The-most-ideal-is-the-number-of-golden-sections.}\frac{\sqrt{5}&space;-1}{2}&space;\approx&space;0.618&space;\end{aligned}&space;) ### 2.1.2. 碰撞(collision) @@ -78,7 +71,7 @@ $$ ## 2.2. 直接寻址法 -将关键字直接对应到数组地址, 即 $h(k)=k$ +将关键字直接对应到数组地址, 即 ![](https://latex.codecogs.com/gif.latex?h(k)=k) 缺点: 如果关键字值域范围大, 但是数量小, 就会浪费空间, 有可能还不能储存这么大的值域范围. @@ -92,10 +85,10 @@ $$ -记有 m 个链表, n 个元素 $\alpha = \frac{n}{m}$ 为每个链表的期望元素个数(长度) +记有 m 个链表, n 个元素 ![](https://latex.codecogs.com/gif.latex?\alpha&space;=&space;\frac{n}{m}) 为每个链表的期望元素个数(长度) -则查找成功,或者不成功的时间复杂度为 $\Theta(1+\alpha)$ -如果 $n=O(m), namely \quad \alpha=\frac{O(m)}{m}=O(1)$, 则上面的链接法满足 $O(1)$的速度 +则查找成功,或者不成功的时间复杂度为 ![](https://latex.codecogs.com/gif.latex?\Theta(1+\alpha)) +如果 ![](https://latex.codecogs.com/gif.latex?n=O(m),&space;namely&space;\quad&space;\alpha=\frac{O(m)}{m}=O(1)), 则上面的链接法满足 ![](https://latex.codecogs.com/gif.latex?O(1))的速度 @@ -104,41 +97,39 @@ $$ 随机地选择散列函数, 使之独立于要存储的关键字 #### 2.3.1.1. 定义 -设一组散列函数 $H=\{h_1,h_2,\ldots,h_i\}$, 将 关键字域 U 映射到 $\{0,1,\ldots,m-1\}$ , 全域的函数组, 满足 -$$ -for \ k \neq l \ \in U, h(k) = h(l), \text{这样的 h 的个数不超过}\frac{|H|}{m} -$$ -即从 H 中任选一个散列函数, 当关键字不相等时, 发生碰撞的概率不超过 $\frac{1}{m}$ +设一组散列函数 ![](https://latex.codecogs.com/gif.latex?H=\{h_1,h_2,\ldots,h_i\}), 将 关键字域 U 映射到 ![](https://latex.codecogs.com/gif.latex?\{0,1,\ldots,m-1\}) , 全域的函数组, 满足 +![](https://latex.codecogs.com/gif.latex?&space;for&space;\&space;k&space;eq&space;l&space;\&space;\in&space;U,&space;h(k)&space;=&space;h(l),&space;\text{Such&space;h&space;The-number-does-not-exceed}\frac{|H|}{m}&space;) +即从 H 中任选一个散列函数, 当关键字不相等时, 发生碰撞的概率不超过 ![](https://latex.codecogs.com/gif.latex?\frac{1}{m}) #### 2.3.1.2. 性质 -对于 m 个槽位的表, 只需 $\Theta(n)$的期望时间来处理 n 个元素的 insert, search, delete,其中 有$O(m)$个insert 操作 +对于 m 个槽位的表, 只需 ![](https://latex.codecogs.com/gif.latex?\Theta(n))的期望时间来处理 n 个元素的 insert, search, delete,其中 有![](https://latex.codecogs.com/gif.latex?O(m))个insert 操作 #### 2.3.1.3. 实现 -选择足够大的 prime p, 记 $Z_p=\{0,1,\ldots,p-1\}$, $Z_p^{*}=\{1,\ldots,p-1\}$ -令$h_{a,b}(k) = ((ak+b)mod\ p) mod\ m$ -则 $H_{p,m}=\{h_{a,b}|a\in Z_p^{*},b\in Z_p\}$ +选择足够大的 prime p, 记 ![](https://latex.codecogs.com/gif.latex?Z_p=\{0,1,\ldots,p-1\}), ![](https://latex.codecogs.com/gif.latex?Z_p^{*}=\{1,\ldots,p-1\}) +令![](https://latex.codecogs.com/gif.latex?h_{a,b}(k)&space;=&space;((ak+b)mod\&space;p)&space;mod\&space;m) +则 ![](https://latex.codecogs.com/gif.latex?H_{p,m}=\{h_{a,b}|a\in&space;Z_p^{*},b\in&space;Z_p\}) -每一个散列函数 $h\_{a,b}$ 都将 $Z_p$ 映射到 $Z_m$, m 可以是任意的, 不用是一个素数 +每一个散列函数 ![](https://latex.codecogs.com/gif.latex?h\_{a,b}) 都将 ![](https://latex.codecogs.com/gif.latex?Z_p) 映射到 ![](https://latex.codecogs.com/gif.latex?Z_m), m 可以是任意的, 不用是一个素数 ## 2.4. 开放寻址法 所有表项都在散列表中, 没有链表. -且散列表装载因子$\alpha=\frac{n}{m}\leqslant1$ +且散列表装载因子![](https://latex.codecogs.com/gif.latex?\alpha=\frac{n}{m}\leqslant1) 这里散列函数再接受一个参数, 作为探测序号 -逐一试探 $h(k,0),h(k,1),\ldots,h(k,m-1)$,这要有满足的,就插入, 不再计算后面的 hash值 +逐一试探 ![](https://latex.codecogs.com/gif.latex?h(k,0),h(k,1),\ldots,h(k,m-1)),这要有满足的,就插入, 不再计算后面的 hash值 探测序列一般分有三种 -* 线性$\ 0,1,\ldots,m-1$ +* 线性![](https://latex.codecogs.com/gif.latex?\&space;0,1,\ldots,m-1) 存在一次聚集问题 -* 二次$\ 0,1,\ldots,(m-1)^2$ +* 二次![](https://latex.codecogs.com/gif.latex?\&space;0,1,\ldots,(m-1)^2) 存在二次聚集问题 * 双重探查 -$h(k,i) = (h_1(k)+i*h_2(k))mod\ m$ +![](https://latex.codecogs.com/gif.latex?h(k,i)&space;=&space;(h_1(k)+i*h_2(k))mod\&space;m) 为了能查找整个表, 即要为模 m 的完系, 则 h_2(k)要与 m 互质. -如可以取 $h_1(k) = k\ mod \ m,h_2(k) = 1+(k\ mod\ {m-1})$ +如可以取 ![](https://latex.codecogs.com/gif.latex?h_1(k)&space;=&space;k\&space;mod&space;\&space;m,h_2(k)&space;=&space;1+(k\&space;mod\&space;{m-1})) @@ -146,32 +137,24 @@ $h(k,i) = (h_1(k)+i*h_2(k))mod\ m$ ### 2.4.1. 不成功查找的探查数的期望 -对于开放寻址散列表,且 $\alpha<1$,一次不成功的查找,是这样的: 已经装填了 n 个, 总共有m 个,则空槽有 m-n 个. +对于开放寻址散列表,且 ![](https://latex.codecogs.com/gif.latex?\alpha<1),一次不成功的查找,是这样的: 已经装填了 n 个, 总共有m 个,则空槽有 m-n 个. 不成功的探查是这样的: 一直探查到已经装填的元素(但是不是要找的元素), 直到遇到没有装填的空槽. 所以这服从几何分布, 即 -$$ -p(\text{不成功探查})=p(\text{第一次找到空槽})=\frac{m-n}{m} -$$ +![](https://latex.codecogs.com/gif.latex?&space;p(\text{Unsuccessful-exploration})=p(\text{Find-the-empty-slot-for-the-first-time})=\frac{m-n}{m}&space;) 有 - $$ E(\text{探查数})=\frac{1}{p}\leqslant \frac{1}{1-\alpha}$$ + ![](https://latex.codecogs.com/gif.latex?E(\text{Probe-number})=\frac{1}{p}\leqslant&space;\frac{1}{1-\alpha}) ![](https://upload-images.jianshu.io/upload_images/7130568-8d659aa8fe7de1a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #### 2.4.1.1. 插入探查数的期望 -所以, 插入一个关键字, 也最多需要 $\frac{1}{1-\alpha}$次, 因为插入过程就是前面都是被占用了的槽, 最后遇到一个空槽.与探查不成功是一样的过程 +所以, 插入一个关键字, 也最多需要 ![](https://latex.codecogs.com/gif.latex?\frac{1}{1-\alpha})次, 因为插入过程就是前面都是被占用了的槽, 最后遇到一个空槽.与探查不成功是一样的过程 #### 2.4.1.2. 成功查找的探查数的期望 -成功查找的探查过程与插入是一样的. 所以查找关键字 k 相当于 插入它, 设为第 i+1 个插入的(前面插入了i个,装载因子$\alpha=\frac{i}{m}$. 那么期望探查数就是 -$$\frac{1}{1-\alpha}=\frac{1}{1-\frac{i}{m}}=\frac{m}{m-i}$$ +成功查找的探查过程与插入是一样的. 所以查找关键字 k 相当于 插入它, 设为第 i+1 个插入的(前面插入了i个,装载因子![](https://latex.codecogs.com/gif.latex?\alpha=\frac{i}{m}). 那么期望探查数就是 +![](https://latex.codecogs.com/gif.latex?\frac{1}{1-\alpha}=\frac{1}{1-\frac{i}{m}}=\frac{m}{m-i}) 则成功查找的期望探查数为 -$$ -\begin{aligned} -\frac{1}{n}\sum_{i=0}^{n-1}\frac{m}{m-i}=\frac{m}{n}\sum_{i=0}^{n-1}\frac{1}{m-i} &= \frac{m}{n}\sum_{i=m-n+1}^{m}\frac{1}{i}\\ -&\leqslant \frac{1}{\alpha} \int_{m-n}^m\frac{1}{x}dx\\ -&=\frac{1}{\alpha}ln\frac{1}{1-\alpha} -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;\frac{1}{n}\sum_{i=0}^{n-1}\frac{m}{m-i}=\frac{m}{n}\sum_{i=0}^{n-1}\frac{1}{m-i}&space;&=&space;\frac{m}{n}\sum_{i=m-n+1}^{m}\frac{1}{i}\\&space;&\leqslant&space;\frac{1}{\alpha}&space;\int_{m-n}^m\frac{1}{x}dx\\&space;&=\frac{1}{\alpha}ln\frac{1}{1-\alpha}&space;\end{aligned}&space;) 代码 diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/number-theory.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/number-theory.md" index fd5d834..f490625 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/number-theory.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/number-theory.md" @@ -25,7 +25,7 @@ description: "bezouts-identity, primality_test, miller-rabin, prime-sieve, polla ## 0.1. gcd, co-primes `gcd` is short for `greatest common divisor` -If `a`,`b` are co-primes, we denote as $(a,b)=1, \text{which means } gcd(a,b)=1 $ +If `a`,`b` are co-primes, we denote as ![](https://latex.codecogs.com/gif.latex?(a,b)=1,&space;\text{which&space;means&space;}&space;gcd(a,b)=1) We can use `Euclid algorithm` to calculate `gcd` of two numbers. ```python def gcd(a,b): @@ -87,8 +87,8 @@ def twoDivideFind(x,li): Just like the Fermat and Solovay–Strassen tests, the Miller–Rabin test relies on an equality or set of equalities that hold true for prime values, then checks whether or not they hold for a number that we want to test for primality. First, a [lemma](https://en.wikipedia.org/wiki/Lemma_(mathematics) "Lemma (mathematics)") about square [roots of unity](https://en.wikipedia.org/wiki/Root_of_unity "Root of unity") in the [finite field](https://en.wikipedia.org/wiki/Finite_field "Finite field") **Z**/*p***Z**, where *p* is prime and *p* > 2\. Certainly 1 and −1 always yield 1 when squared modulo *p*; call these [trivial](https://en.wikipedia.org/wiki/Trivial_(mathematics) "Trivial (mathematics)") [square roots](https://en.wikipedia.org/wiki/Square_root "Square root") of 1\. There are no *nontrivial* square roots of 1 modulo *p* (a special case of the result that, in a field, a [polynomial](https://en.wikipedia.org/wiki/Polynomial "Polynomial") has no more zeroes than its degree). To show this, suppose that *x* is a square root of 1 modulo *p*. Then: -$$ x^2\equiv1\ (mod\ p)$$ -$$(x-1)(x+1) \equiv 0\ (mod\ p)$$ +![](https://latex.codecogs.com/gif.latex?x^2\equiv1\&space;(mod\&space;p)) +![](https://latex.codecogs.com/gif.latex?(x-1)(x+1)&space;\equiv&space;0\&space;(mod\&space;p)) @@ -96,22 +96,22 @@ In other words, prime *p* divides the product (*x* − 1)(*x* + 1). By [E Now, let *n* be prime, and odd, with *n* > 2\. It follows that *n* − 1 is even and we can write it as 2*s*·*d*, where *s* and *d* are positive integers and *d* is odd. For each *a* in (**Z**/*n***Z**)*, either -$$a^d\equiv 1\ (mod\ n)$$ +![](https://latex.codecogs.com/gif.latex?a^d\equiv&space;1\&space;(mod\&space;n)) or -$$a^{2^r*d}\equiv -1\ (mod\ n), \text{where } 0\le r*n*−1, we will get either 1 or −1\. If we get −1 then the second equality holds and it is done. If we never get −1, then when we have taken out every power of 2, we are left with the first equality. The Miller–Rabin primality test is based on the [contrapositive](https://en.wikipedia.org/wiki/Contrapositive "Contrapositive") of the above claim. That is, if we can find an *a* such that -$$a^d\not\equiv 1\ (mod\ n)$$ +![](https://latex.codecogs.com/gif.latex?a^d&space;ot\equiv&space;1\&space;(mod\&space;n)) and -$$a^{2^r*d}\not\equiv -1\ (mod\ n), \text{where } 0\le r ## 0.4. Euler function -Euler function, denoted as $\phi(n)$, mapping n as the number of number which is smaller than n and is the co-prime of n. +Euler function, denoted as ![](https://latex.codecogs.com/gif.latex?\phi(n)), mapping n as the number of number which is smaller than n and is the co-prime of n. -e.g.: $\phi(3)=2$ since 1,2 are coprimes of 3 and smaller than 3, $\phi(4)=2$ ,(1,3) +e.g.: ![](https://latex.codecogs.com/gif.latex?\phi(3)=2) since 1,2 are coprimes of 3 and smaller than 3, ![](https://latex.codecogs.com/gif.latex?\phi(4)=2) ,(1,3) Euler function is a kind of productive function and has two properties as follows: -1. $\phi(p^k) = p^k-p^{k-1}$, where p is a prime -2. $\phi(mn) = \phi(m)*\phi(n)$ where $(m,n)=1$ +1. ![](https://latex.codecogs.com/gif.latex?\phi(p^k)&space;=&space;p^k-p^{k-1}), where p is a prime +2. ![](https://latex.codecogs.com/gif.latex?\phi(mn)&space;=&space;\phi(m)*\phi(n)) where ![](https://latex.codecogs.com/gif.latex?(m,n)=1) -Thus, for every narural number *n*, we can evaluate $\phi(n)$ using the following method. +Thus, for every narural number *n*, we can evaluate ![](https://latex.codecogs.com/gif.latex?\phi(n)) using the following method. 1. factorize n: -$$n = \prod _{i=1}^{l} p_i^{k_i}$$, where $p_i$ is a prime and $k_i,l > 0$ . -2. calculate $\phi(n) $ using the two properties. - -$$ -\begin{aligned} -\phi(n) &=\phi( \prod _{i=1}^{l} p_i^{k_i}) \\ - &=\prod _{i=1}^{l} \phi( p_i^{k_i}) \\ - &=\prod _{i=1}^{l} ( p_i^{k_i}-p_i^{ {k_i}-1})\\ - &=\prod _{i=1}^{l}p_i^{k_i} \prod _{i=1}^{l} ( 1-\frac{1}{p_i})\\ - &=n \prod _{i=1}^{l} ( 1-\frac{1}{p_i})\\ -\end{aligned} -$$ - -And , $\sigma(n)$ represents the sum of all factors of n. -e.g. : $\sigma(9) = 1+3+9 = 14$ -$$ -\begin{aligned} -\sigma(n) &= \prod _{i=1}^{l} \sum_{j=0}^{k_i} p_i^j \\ - &=\prod _{i=1}^{l} \frac{p_i^{k_i+1}-1}{p_i-1}\\ -\end{aligned} -$$ - -A `perfect number` _n_ is defined as $\sigma(n) = 2n$ +![](https://latex.codecogs.com/gif.latex?n&space;=&space;\prod&space;_{i=1}^{l}&space;p_i^{k_i}), where ![](https://latex.codecogs.com/gif.latex?p_i) is a prime and ![](https://latex.codecogs.com/gif.latex?k_i,l&space;>&space;0) . +2. calculate ![](https://latex.codecogs.com/gif.latex?\phi(n)) using the two properties. + +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;\phi(n)&space;&=\phi(&space;\prod&space;_{i=1}^{l}&space;p_i^{k_i})&space;\\&space;&=\prod&space;_{i=1}^{l}&space;\phi(&space;p_i^{k_i})&space;\\&space;&=\prod&space;_{i=1}^{l}&space;(&space;p_i^{k_i}-p_i^{&space;{k_i}-1})\\&space;&=\prod&space;_{i=1}^{l}p_i^{k_i}&space;\prod&space;_{i=1}^{l}&space;(&space;1-\frac{1}{p_i})\\&space;&=n&space;\prod&space;_{i=1}^{l}&space;(&space;1-\frac{1}{p_i})\\&space;\end{aligned}&space;) + +And , ![](https://latex.codecogs.com/gif.latex?\sigma(n)) represents the sum of all factors of n. +e.g. : ![](https://latex.codecogs.com/gif.latex?\sigma(9)&space;=&space;1+3+9&space;=&space;14) +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;\sigma(n)&space;&=&space;\prod&space;_{i=1}^{l}&space;\sum_{j=0}^{k_i}&space;p_i^j&space;\\&space;&=\prod&space;_{i=1}^{l}&space;\frac{p_i^{k_i+1}-1}{p_i-1}\\&space;\end{aligned}&space;) + +A `perfect number` _n_ is defined as ![](https://latex.codecogs.com/gif.latex?\sigma(n)&space;=&space;2n) The following is the implementation of this two functions. ```python @@ -280,7 +267,7 @@ def sigma(n): ## 0.5. Modulo equation The following codes can solve a linear, group modulo equation. More details and explanations will be supplied if I am not too busy. -Note that I use `--` to represent $\equiv$ in the python codes. +Note that I use `--` to represent ![](https://latex.codecogs.com/gif.latex?\equiv) in the python codes. ![](https://upload-images.jianshu.io/upload_images/7130568-be31bdaf6b67f883.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/red-black-tree.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/red-black-tree.md" index af7df43..f718870 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/red-black-tree.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/red-black-tree.md" @@ -63,24 +63,24 @@ description: "红黑树的原理与实现, 包括插入, 删除, 以及数据结 ## 1.3. 黑高度 -从某个结点 x 到叶结点的黑色结点数,称为此结点的黑高度, 记为 $h_b(x)$ +从某个结点 x 到叶结点的黑色结点数,称为此结点的黑高度, 记为 ![](https://latex.codecogs.com/gif.latex?h_b(x)) 树的黑高度是根的黑高度 ->1. 以 x 为 根的子树至少包含 $2^{h_b(x)}-1$个结点 ->2. 一颗有 n 个内结点的红黑树高度至多为$2lg(n+1)$ +>1. 以 x 为 根的子树至少包含 ![](https://latex.codecogs.com/gif.latex?2^{h_b(x)}-1)个结点 +>2. 一颗有 n 个内结点的红黑树高度至多为![](https://latex.codecogs.com/gif.latex?2lg(n+1)) 可用归纳法证明1 证明 2: 设树高 h -由红黑性质4, 根结点到叶子路径上的黑结点数至少 $\frac{h}{2}$,即 $h_b(root)\geqslant \frac{h}{2}$ +由红黑性质4, 根结点到叶子路径上的黑结点数至少 ![](https://latex.codecogs.com/gif.latex?\frac{h}{2}),即 ![](https://latex.codecogs.com/gif.latex?h_b(root)\geqslant&space;\frac{h}{2}) 再由1, -$$n \geqslant 2^{h_b(x)} -1 \geqslant 2^{\frac{h}{2}} -1$$ +![](https://latex.codecogs.com/gif.latex?n&space;\geqslant&space;2^{h_b(x)}&space;-1&space;\geqslant&space;2^{\frac{h}{2}}&space;-1) -即 $ h\leqslant 2lg(n+1)$ +即 ![](https://latex.codecogs.com/gif.latex?h\leqslant&space;2lg(n+1)) # 2. 旋转 -由于上面证明的红黑树高为 $O(logn)$,红黑树的 insert, delete, search 等操作都是, $O(logn)$. +由于上面证明的红黑树高为 ![](https://latex.codecogs.com/gif.latex?O(logn)),红黑树的 insert, delete, search 等操作都是, ![](https://latex.codecogs.com/gif.latex?O(logn)). 进行了 insert, delete 后可能破坏红黑性质, 可以通过旋转来保持. @@ -345,15 +345,15 @@ w 表示 x 的相抵. w 不能为 nil(因为 x 是双重黑色) # 5. 数据结构的扩张 ## 5.1. 平衡树的扩张 -通过在平衡树(如红黑树上的每个结点 加上 一个数据域 size (表示以此结点为根的子树的结点数.) 可以使`获得第 i 大的数` 的时间复杂度为 $O(logn)$ +通过在平衡树(如红黑树上的每个结点 加上 一个数据域 size (表示以此结点为根的子树的结点数.) 可以使`获得第 i 大的数` 的时间复杂度为 ![](https://latex.codecogs.com/gif.latex?O(logn)) -在 $O(n)$ 时间内建立, python代码如下 +在 ![](https://latex.codecogs.com/gif.latex?O(n)) 时间内建立, python代码如下 ```python def setSize(root): if root is None:return 0 root.size = setSize(root.left) + setSize(root.right)+1 ``` -在$O(logn)$时间查找, +在![](https://latex.codecogs.com/gif.latex?O(logn))时间查找, ```python def find(root,i): r = root.left.size +1 diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/sort.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/sort.md" index 3ae32b7..8748fd6 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/sort.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/sort.md" @@ -43,7 +43,7 @@ description: "七大排序算法" * 比较排序 如希尔排序,堆排序, 快速排序, 合并排序等 -可以证明 比较排序的下界 是 $\Omega(nlogn)$ +可以证明 比较排序的下界 是 ![](https://latex.codecogs.com/gif.latex?\Omega(nlogn)) * 非比较排序 @@ -52,7 +52,7 @@ description: "七大排序算法" # 1. 希尔排序(shellSort) -希尔排序是选择排序的改进, 通过在较远的距离进行交换, 可以更快的减少逆序数. 这个距离即增量, 由自己选择一组, 从大到小进行, 而且最后一个增量必须是 1. 要选得到好的性能, 一般选择$2^k-1$ +希尔排序是选择排序的改进, 通过在较远的距离进行交换, 可以更快的减少逆序数. 这个距离即增量, 由自己选择一组, 从大到小进行, 而且最后一个增量必须是 1. 要选得到好的性能, 一般选择![](https://latex.codecogs.com/gif.latex?2^k-1) ```pythonn def shellSort(s,inc = None): if inc is None: inc = [1,3,5,7,11,13,17,19] @@ -67,33 +67,26 @@ def shellSort(s,inc = None): s[cur] = s[j] return s ``` -可以证明 希尔排序时间复杂度可以达到$O(n^{\frac{4}{3}})$ +可以证明 希尔排序时间复杂度可以达到![](https://latex.codecogs.com/gif.latex?O(n^{\frac{4}{3}})) # 2. 堆排序(heapSort) ## 2.1. 建堆 是将一个数组(列表) heapify 的过程. 方法就是对每一个结点, 都自底向上的比较,然后操作,这个过程称为 上浮. 粗略的计算, 每个结点上浮的比较次数的上界是 层数, 即 logn, 则 n 个结点, 总的比较次数为 nlogn -但是可以发现, 不同高度 h 的结点比较的次数不同, 上界实际上应该是 $O(h)$,每层结点数上界 $\lfloor 2^h \rfloor$ +但是可以发现, 不同高度 h 的结点比较的次数不同, 上界实际上应该是 ![](https://latex.codecogs.com/gif.latex?O(h)),每层结点数上界 ![](https://latex.codecogs.com/gif.latex?\lfloor&space;2^h&space;\rfloor) 则 总比较次数为 -$$ -\begin{aligned} -\sum_{h=1}^{\lfloor{log_2 n}\rfloor} O(h)\lceil 2^{h} \rceil & = \sum_{h=0}^{ {log_2 n}-1} O(h\frac{n}{2^h})\\ - & = n*O(\sum_{h=0}^{log_2 n}\frac{h}{2^h}) \\ - & = n*O(1) \\ - & = O(n) -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;\sum_{h=1}^{\lfloor{log_2&space;n}\rfloor}&space;O(h)\lceil&space;2^{h}&space;\rceil&space;&&space;=&space;\sum_{h=0}^{&space;{log_2&space;n}-1}&space;O(h\frac{n}{2^h})\\&space;&&space;=&space;n*O(\sum_{h=0}^{log_2&space;n}\frac{h}{2^h})&space;\\&space;&&space;=&space;n*O(1)&space;\\&space;&&space;=&space;O(n)&space;\end{aligned}&space;) ## 2.2. 访问最元 -最大堆对应最大元,最小堆对于最小元, 可以 $O(1)$ 内实现 +最大堆对应最大元,最小堆对于最小元, 可以 ![](https://latex.codecogs.com/gif.latex?O(1)) 内实现 ## 2.3. 取出最元 最大堆取最大元,最小堆取最小元,由于元素取出了, 要进行调整. 从堆顶开始, 依次和其两个孩子比较, 如果是最大堆, 就将此结点(父亲)的值赋为较大的孩子的值,最小堆反之. 然后对那个孩子进行同样的操作,一直到达堆底,即最下面的一层. 这个过程称为 下滤. 最后将最后一个元素与最下面一层那个元素(与上一层交换的)交换, 再删除最后一个元素. -时间复杂度为 $O(logn)$ +时间复杂度为 ![](https://latex.codecogs.com/gif.latex?O(logn)) ## 2.4. 堆排序 建立堆之后, 一直进行 `取出最元`操作, 即得有序序列 @@ -269,16 +262,16 @@ def quickSort(lst): 快速排序性能取决于划分的对称性(即枢纽元的选择), 以及partition 的实现. 如果每次划分很对称(大概在当前序列的中位数为枢纽元), 则与合并算法一样快, 但是如果不对称,在渐近上就和插入算法一样慢 ### 3.3.1. 最坏情况 -试想,如果每次划分两个区域分别包含 n-1, 1则易知时间复杂度为 $\Theta(n^2)$, 此外, 如果输入序序列已经排好序,且枢纽元没选好, 比如选的端点, 则同样是这样复杂, 而此时插入排序只需 $O(n)$. +试想,如果每次划分两个区域分别包含 n-1, 1则易知时间复杂度为 ![](https://latex.codecogs.com/gif.latex?\Theta(n^2)), 此外, 如果输入序序列已经排好序,且枢纽元没选好, 比如选的端点, 则同样是这样复杂, 而此时插入排序只需 ![](https://latex.codecogs.com/gif.latex?O(n)). ### 3.3.2. 最佳情况 -有 $T(n) = 2T(\frac{n}{2})+\Theta(n)$ -则由主方法为$O(nlogn)$ +有 ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;2T(\frac{n}{2})+\Theta(n)) +则由主方法为![](https://latex.codecogs.com/gif.latex?O(nlogn)) ### 3.3.3. 平衡的划分 -如果每次 9:1, $T(n) = T(\frac{9n}{10})+T(\frac{n}{10})+\Theta(n)$ -用递归树求得在渐近上仍然是 $O(nlogn)$ +如果每次 9:1, ![](https://latex.codecogs.com/gif.latex?T(n)&space;=&space;T(\frac{9n}{10})+T(\frac{n}{10})+\Theta(n)) +用递归树求得在渐近上仍然是 ![](https://latex.codecogs.com/gif.latex?O(nlogn)) 所以任何比值 k:1, 都有如上的渐近时间复杂度 然而每次划分是不可能完全相同的 @@ -287,40 +280,27 @@ def quickSort(lst): ## 3.4. 期望运行时间 对于 randomized-quicksort, 即随机选择枢纽元 -设 n 个元素, 从小到大记为 $z_1,z_2,\ldots,z_n$,指示器变量 $X_{ij}$表示 $z_i,z_j$是否进行比较 +设 n 个元素, 从小到大记为 ![](https://latex.codecogs.com/gif.latex?z_1,z_2,\ldots,z_n),指示器变量 ![](https://latex.codecogs.com/gif.latex?X_{ij})表示 ![](https://latex.codecogs.com/gif.latex?z_i,z_j)是否进行比较 即 -$$ -X_{ij} = -\begin{cases} -1,\quad z_i,z_j\text{进行比较}\\ -0,\quad z_i,z_j\text{不进行比较} -\end{cases} -$$ +![](https://latex.codecogs.com/gif.latex?&space;X_{ij}&space;=&space;\begin{cases}&space;1,\quad&space;z_i,z_j\text{Making-Comparisons}\\&space;0,\quad&space;z_i,z_j\text{No-comparison}&space;\end{cases}&space;) 考察比较次数, 可以发现两个元素进行比较, 一定是一个是枢纽元的情况, 两个元素间不可能进行两次比较. -所有总的比较次数不超过,$\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}$ +所有总的比较次数不超过,![](https://latex.codecogs.com/gif.latex?\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}) 求均值 -$$E(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij})=\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}E(X_{ij})=\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}P(z_i,z_j\text{进行比较})$$ +![](https://latex.codecogs.com/gif.latex?E(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij})=\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}E(X_{ij})=\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}P(z_i,z_j\text{Making-Comparisons})) -再分析,$z_i,z_j$ 在$Z_{ij} = \{z_i,z_{i+1},\ldots,z_j\} $中, 如果集合中的非此两元素,$z_k, i< k< j$作为了枢纽元, 则$z_k$将集合划分{z_i,z_{i+1},\ldots,z_{k-1}},{z_{k+1},\ldots,z_j}, 这两个集合中的元素都不会再和对方中的元素进行比较, -所以要使 $z_i,z_j$进行比较, 则两者之一(只能是一个,即互斥)是 $Z_{ij}$上的枢纽元 +再分析,![](https://latex.codecogs.com/gif.latex?z_i,z_j) 在![](https://latex.codecogs.com/gif.latex?Z_{ij}&space;=&space;\{z_i,z_{i+1},\ldots,z_j\})中, 如果集合中的非此两元素,![](https://latex.codecogs.com/gif.latex?z_k,&space;i<&space;k<&space;j)作为了枢纽元, 则![](https://latex.codecogs.com/gif.latex?z_k)将集合划分{z_i,z_{i+1},\ldots,z_{k-1}},{z_{k+1},\ldots,z_j}, 这两个集合中的元素都不会再和对方中的元素进行比较, +所以要使 ![](https://latex.codecogs.com/gif.latex?z_i,z_j)进行比较, 则两者之一(只能是一个,即互斥)是 ![](https://latex.codecogs.com/gif.latex?Z_{ij})上的枢纽元 则 -$$ -\begin{aligned} -P(z_i,z_j\text{进行比较}) & = P(z_i,z_j\text{做为}Z_{ij}\text{上的枢纽元}) \\ - & = P(z_j\text{做为}Z_{ij}\text{上的枢纽元})+P(z_i\text{做为}Z_{ij}\text{上的枢纽元})\\ - & = \frac{1}{j-i+1}+\frac{1}{j-i+1} -\\ & = \frac{2}{j-i+1}\\ -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;P(z_i,z_j\text{Making-Comparisons})&space;&&space;=&space;P(z_i,z_j\text{As}Z_{ij}\text{Hub-element})&space;\\&space;&&space;=&space;P(z_j\text{As}Z_{ij}\text{Hub-element})+P(z_i\text{As}Z_{ij}\text{Hub-element})\\&space;&&space;=&space;\frac{1}{j-i+1}+\frac{1}{j-i+1}&space;\\&space;&&space;=&space;\frac{2}{j-i+1}\\&space;\end{aligned}&space;) 注意第二步是因为两事件互斥才可以直接概率相加 然后就可以将此概率代入求期望比较次数了, -为 $O(nlogn)$ (由于是 O, 放缩一下就行) +为 ![](https://latex.codecogs.com/gif.latex?O(nlogn)) (由于是 O, 放缩一下就行) ## 3.5. 堆栈深度 -考察快速排序的堆栈深度,可以从递归树思考,实际上的堆栈变化过程就是前序访问二叉树, 所以深度为 $O(logn)$ +考察快速排序的堆栈深度,可以从递归树思考,实际上的堆栈变化过程就是前序访问二叉树, 所以深度为 ![](https://latex.codecogs.com/gif.latex?O(logn)) 为了减少深度, 可以进行 尾递归优化, 将函数返回前的递归通过迭代完成 ```python QUICKSORT(A,a,b) @@ -363,7 +343,7 @@ def countSort(lst,mn,mx): 由我们平时的直觉, 我们比较两个数时, 是从最高位比较起, 一位一位比较, 直到不相等时就能判断大小,或者相等(位数比完了). 基数排序有点不一样, 它是从低位比到高位, 这样才能把相同位有相同值的不同数排序. -对于 n 个数, 最高 d 位, 用下面的实现, 可时间复杂度为 $\Theta((n+d)*d)$ +对于 n 个数, 最高 d 位, 用下面的实现, 可时间复杂度为 ![](https://latex.codecogs.com/gif.latex?\Theta((n+d)*d)) ## 5.2. 实现 @@ -417,26 +397,18 @@ if __name__ == '__main__': 设有 n 个元素, 则设立 n 个桶 将各元素通过数值线性映射到桶地址, 类似 hash 链表. -然后在每个桶内, 进行插入排序($O(n_i^2)$) +然后在每个桶内, 进行插入排序(![](https://latex.codecogs.com/gif.latex?O(n_i^2))) 最后合并所有桶. -这里的特点是 n 个桶实现了 $\Theta(n)$的时间复杂度, 但是耗费的空间 为 $\Theta(n)$ +这里的特点是 n 个桶实现了 ![](https://latex.codecogs.com/gif.latex?\Theta(n))的时间复杂度, 但是耗费的空间 为 ![](https://latex.codecogs.com/gif.latex?\Theta(n)) 证明 -* 线性映射部分: $\Theta(n)$ -* 桶合并部分: $\Theta(n)$ -* 桶内插入排序部分: 设每个桶内的元素数为随机变量 $n_i$, 易知 $n_i \sim B(n,\frac{1}{n})$ 记 $p=\frac{1}{n}$ +* 线性映射部分: ![](https://latex.codecogs.com/gif.latex?\Theta(n)) +* 桶合并部分: ![](https://latex.codecogs.com/gif.latex?\Theta(n)) +* 桶内插入排序部分: 设每个桶内的元素数为随机变量 ![](https://latex.codecogs.com/gif.latex?n_i), 易知 ![](https://latex.codecogs.com/gif.latex?n_i&space;\sim&space;B(n,\frac{1}{n})) 记 ![](https://latex.codecogs.com/gif.latex?p=\frac{1}{n}) -$$ -\begin{aligned} -E(\sum_{i=1}^{n}n_i^2) &=\sum_{i=1}^{n}E(n_i^2) \\ -&=\sum_{i=1}^{n}( Var(n_i)+E^2(n_i) ) \\ -&= \sum_{i=1}^{n}( np(1-p)+ (np)^2 )\\ -&= \sum_{i=1}^{n}( 2-\frac{1}{n} )\\ -&= 2n-1 -\end{aligned} -$$ -将以上各部分加起来即得时间复杂度 $\Theta(n)$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;E(\sum_{i=1}^{n}n_i^2)&space;&=\sum_{i=1}^{n}E(n_i^2)&space;\\&space;&=\sum_{i=1}^{n}(&space;Var(n_i)+E^2(n_i)&space;)&space;\\&space;&=&space;\sum_{i=1}^{n}(&space;np(1-p)+&space;(np)^2&space;)\\&space;&=&space;\sum_{i=1}^{n}(&space;2-\frac{1}{n}&space;)\\&space;&=&space;2n-1&space;\end{aligned}&space;) +将以上各部分加起来即得时间复杂度 ![](https://latex.codecogs.com/gif.latex?\Theta(n)) @@ -445,11 +417,11 @@ $$ 输入个序列 lst, 以及一个数 i, 输出 lst 中 第 i 小的数,即从小到大排列第 i 解决方法 -* 全部排序, 取第 i 个, $O(nlogn)$ +* 全部排序, 取第 i 个, ![](https://latex.codecogs.com/gif.latex?O(nlogn)) * 长度为 i 的队列(这是得到 lst 中 前 -i 个元素的方法) 仍然 $O(nlogn)$ -* randomized-select(仿造快排) 平均情况$O(n)$,最坏情况同上(快排), $\Theta(n^2)$ +i 个元素的方法) 仍然 ![](https://latex.codecogs.com/gif.latex?O(nlogn)) +* randomized-select(仿造快排) 平均情况![](https://latex.codecogs.com/gif.latex?O(n)),最坏情况同上(快排), ![](https://latex.codecogs.com/gif.latex?\Theta(n^2)) ```python from random import randint diff --git "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/string-matching.md" "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/string-matching.md" index b67ab8b..f97e7d0 100644 --- "a/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/string-matching.md" +++ "b/\347\256\227\346\263\225\345\237\272\347\241\200/notes/mbinary/string-matching.md" @@ -19,143 +19,25 @@ In this article, I will show you some kinds of popular string matching algorithm ## Rabin-Karp We can view a string of k characters (digits) as a length-k decimal number. E.g., the string “31425” corresponds to the decimal number 31,425. - Given a pattern P [1..m], let p denote the corresponding decimal value. -- Given a text T [1..n], let $t_s$ denote the decimal value of the length-m substring T [(s+1)..(s+m)] for s=0,1,…,(n-m). -- let `d` be the radix of num, thus $d = len(set(s))$ -- $t_s$ = p iff T [(s+1)..(s+m)] = P [1..m]. +- Given a text T [1..n], let ![](https://latex.codecogs.com/gif.latex?t_s) denote the decimal value of the length-m substring T [(s+1)..(s+m)] for s=0,1,…,(n-m). +- let `d` be the radix of num, thus ![](https://latex.codecogs.com/gif.latex?d&space;=&space;len(set(s))) +- ![](https://latex.codecogs.com/gif.latex?t_s) = p iff T [(s+1)..(s+m)] = P [1..m]. - p can be computed in O(m) time. p = P[m] + d\*(P[m-1] + d\*(P[m-2]+…)). -- $t_0$ can similarly be computed in O(m) time. -- Other $t_1,\ldots,t_{n-m}$ can be computed in O(n-m) time since $t_{s+1} can be computed from ts in constant time. - -Namely, -$$ +- ![](https://latex.codecogs.com/gif.latex?t_0) can similarly be computed in O(m) time. +- Other ![](https://latex.codecogs.com/gif.latex?t_1,\ldots,t_{n-m}) can be computed in O(n-m) time since ![](https://latex.codecogs.com/gif.latex?t_{s+1}&space;can&space;be&space;computed&space;from&space;ts&space;in&space;constant&space;time.&space;Namely,&space;) t_{s+1} = d*(t_s-d^{m-1} * T[s+1])+T[s+m+1] -$$ -However, it's no need to calculate $t_{s+1}$ directly. We can use modulus operation to reduce the work of caculation. - -We choose a small prime number. Eg 13 for radix( denoted as d) 10. -Generally, $d*q$ should fit within one computer word. - -We firstly caculate $t_0$ mod q. -Then, for every $t_i (i>1)$ -assume -$$ +![](https://latex.codecogs.com/gif.latex?&space;However,&space;it's&space;no&space;need&space;to&space;calculate)t_{s+1}![](https://latex.codecogs.com/gif.latex?directly.&space;We&space;can&space;use&space;modulus&space;operation&space;to&space;reduce&space;the&space;work&space;of&space;caculation.&space;We&space;choose&space;a&space;small&space;prime&space;number.&space;Eg&space;13&space;for&space;radix(&space;denoted&space;as&space;d)&space;10.&space;Generally,)d*q![](https://latex.codecogs.com/gif.latex?should&space;fit&space;within&space;one&space;computer&space;word.&space;We&space;firstly&space;caculate)t_0![](https://latex.codecogs.com/gif.latex?mod&space;q.&space;Then,&space;for&space;every)t_i (i>1)![](https://latex.codecogs.com/gif.latex?&space;assume&space;) t_{i-1} = T[i+m-1] + d*T[i+m-2]+\ldots+d^{m-1}*T[i-1] -$$ -denote $ d' = d^{m-1}\ mod\ q$ -thus, -$$ +![](https://latex.codecogs.com/gif.latex?&space;denote) d' = d^{m-1}\ mod\ q![](https://latex.codecogs.com/gif.latex?&space;thus,&space;) \begin{aligned} t_i &= (t_{i-1} - d^{m-1}*T[i-1]) * d + T[i+m]\\ &\equiv (t_{i-1} - d^{m-1}*T[i-1]) * d + T[i+m] (mod\ q)\\ &\equiv (t_{i-1}- ( d^{m-1} mod \ q) *T[i-1]) * d + T[i+m] (mod\ q)\\ &\equiv (t_{i-1}- d'*T[i-1]) * d + T[i+m] (mod\ q) \end{aligned} -$$ - -So we can compare the modular value of each $t_i$ with p's. -Only if they are the same, then we compare the origin chracters, namely -$$T[i],T[i+1],\ldots,T[i+m-1]$$ -and the pattern characters. -Gernerally, this algorithm's time approximation is O(n+m), and the worst case is O((n-m+1)*m) - -**Problem: this is assuming p and $t_s$ are small numbers. They may be too large to work with easily.** - - -python implementation -```python -#coding: utf-8 -''' mbinary -######################################################################### -# File : rabin_karp.py -# Author: mbinary -# Mail: zhuheqin1@gmail.com -# Blog: https://mbinary.github.io -# Github: https://github.com/mbinary -# Created Time: 2018-12-11 00:01 -# Description: rabin-karp algorithm -######################################################################### -''' - -def isPrime(x): - for i in range(2,int(x**0.5)+1): - if x%i==0:return False - return True -def getPrime(x): - '''return a prime which is bigger than x''' - for i in range(x,2*x): - if isPrime(i):return i -def findAll(s,p): - '''s: string p: pattern''' - dic={} - n,m = len(s),len(p) - d=0 #radix - for c in s: - if c not in dic: - dic[c]=d - d+=1 - sm = 0 - for c in p: - if c not in dic:return [] - sm = sm*d+dic[c] - - ret = [] - cur = 0 - for i in range(m): cur=cur*d + dic[s[i]] - if cur==sm:ret.append(0) - tmp = n-m - q = getPrime(m) - cur = cur%q - sm = sm%q - exp = d**(m-1) % q - for i in range(m,n): - cur = ((cur-dic[s[i-m]]*exp)*d+dic[s[i]]) % q - if cur == sm and p==s[i-m+1:i+1]: - ret.append(i-m+1) - return ret - -def randStr(n=3): - return [randint(ord('a'),ord('z')) for i in range(n)] - -if __name__ =='__main__': - from random import randint - s = randStr(50) - p = randStr(1) - print(s) - print(p) - print(findAll(s,p)) -``` -## FSM -A FSM can be represented as $(Q,q_0,A,S,C)$, where -- Q is the set of all states -- $q_0$ is the start state -- $A\in Q$ is a set of accepting states. -- S is a finite input alphabet. -- C is the set of transition functions: namely $q_j = c(s,q_i)$. - -Given a pattern string S, we can build a FSM for string matching. -Assume S has m chars, and there should be m+1 states. One is for the begin state, and the others are for matching state of each position of S. - -Once we have built the FSM, we can run it on any input string. -## KMP ->Knuth-Morris-Pratt method - -The idea is inspired by FSM. We can avoid computing the transition functions. Instead, we compute a prefix function P in O(m) time, which has only m entries. -> Prefix funtion stores info about how the pattern matches against shifts of itself. - -- String w is a prefix of string x, if x=wy for some string y -- String w is a suffix of string x, if x=yw for some string y -- The k-character prefix of the pattern P [1..m] denoted by Pk. -- Given that pattern prefix P [1..q] matches text characters T [(s+1)..(s+q)], what is the least shift s'> s such that P [1..k] = T [(s'+1)..(s'+k)] where s'+k=s+q? -- At the new shift s', no need to compare the first k characters of P with corresponding characters of T. -Method: For prefix $p_i$, find the longest proper prefix of $p_i$ that is also a suffix of $p_i$. -$$ +![](https://latex.codecogs.com/gif.latex?&space;So&space;we&space;can&space;compare&space;the&space;modular&space;value&space;of&space;each)t_i![](https://latex.codecogs.com/gif.latex?with&space;p's.&space;Only&space;if&space;they&space;are&space;the&space;same,&space;then&space;we&space;compare&space;the&space;origin&space;chracters,&space;namely&space;)T[i],T[i+1],\ldots,T[i+m-1]![](https://latex.codecogs.com/gif.latex?&space;and&space;the&space;pattern&space;characters.&space;Gernerally,&space;this&space;algorithm's&space;time&space;approximation&space;is&space;O(n+m),&space;and&space;the&space;worst&space;case&space;is&space;O((n-m+1)*m)&space;**Problem:&space;this&space;is&space;assuming&space;p&space;and)t_s![](https://latex.codecogs.com/gif.latex?are&space;small&space;numbers.&space;They&space;may&space;be&space;too&space;large&space;to&space;work&space;with&space;easily.**&space;python&space;implementation&space;```python&space;#coding:&space;utf-8&space;'''&space;mbinary&space;#########################################################################&space;#&space;File&space;:&space;rabin_karp.py&space;#&space;Author:&space;mbinary&space;#&space;Mail:&space;zhuheqin1@gmail.com&space;#&space;Blog:&space;https://mbinary.github.io&space;#&space;Github:&space;https://github.com/mbinary&space;#&space;Created&space;Time:&space;2018-12-11&space;00:01&space;#&space;Description:&space;rabin-karp&space;algorithm&space;#########################################################################&space;'''&space;def&space;isPrime(x):&space;for&space;i&space;in&space;range(2,int(x**0.5)+1):&space;if&space;x%i==0:return&space;False&space;return&space;True&space;def&space;getPrime(x):&space;'''return&space;a&space;prime&space;which&space;is&space;bigger&space;than&space;x'''&space;for&space;i&space;in&space;range(x,2*x):&space;if&space;isPrime(i):return&space;i&space;def&space;findAll(s,p):&space;'''s:&space;string&space;p:&space;pattern'''&space;dic={}&space;n,m&space;=&space;len(s),len(p)&space;d=0&space;#radix&space;for&space;c&space;in&space;s:&space;if&space;c&space;not&space;in&space;dic:&space;dic[c]=d&space;d+=1&space;sm&space;=&space;0&space;for&space;c&space;in&space;p:&space;if&space;c&space;not&space;in&space;dic:return&space;[]&space;sm&space;=&space;sm*d+dic[c]&space;ret&space;=&space;[]&space;cur&space;=&space;0&space;for&space;i&space;in&space;range(m):&space;cur=cur*d&space;+&space;dic[s[i]]&space;if&space;cur==sm:ret.append(0)&space;tmp&space;=&space;n-m&space;q&space;=&space;getPrime(m)&space;cur&space;=&space;cur%q&space;sm&space;=&space;sm%q&space;exp&space;=&space;d**(m-1)&space;%&space;q&space;for&space;i&space;in&space;range(m,n):&space;cur&space;=&space;((cur-dic[s[i-m]]*exp)*d+dic[s[i]])&space;%&space;q&space;if&space;cur&space;==&space;sm&space;and&space;p==s[i-m+1:i+1]:&space;ret.append(i-m+1)&space;return&space;ret&space;def&space;randStr(n=3):&space;return&space;[randint(ord('a'),ord('z'))&space;for&space;i&space;in&space;range(n)]&space;if&space;__name__&space;=='__main__':&space;from&space;random&space;import&space;randint&space;s&space;=&space;randStr(50)&space;p&space;=&space;randStr(1)&space;print(s)&space;print(p)&space;print(findAll(s,p))&space;```&space;##&space;FSM&space;A&space;FSM&space;can&space;be&space;represented&space;as)(Q,q_0,A,S,C)![](https://latex.codecogs.com/gif.latex?,&space;where&space;-&space;Q&space;is&space;the&space;set&space;of&space;all&space;states&space;-)q_0![](https://latex.codecogs.com/gif.latex?is&space;the&space;start&space;state&space;-)A\in Q![](https://latex.codecogs.com/gif.latex?is&space;a&space;set&space;of&space;accepting&space;states.&space;-&space;S&space;is&space;a&space;finite&space;input&space;alphabet.&space;-&space;C&space;is&space;the&space;set&space;of&space;transition&space;functions:&space;namely)q_j = c(s,q_i)![](https://latex.codecogs.com/gif.latex?.&space;Given&space;a&space;pattern&space;string&space;S,&space;we&space;can&space;build&space;a&space;FSM&space;for&space;string&space;matching.&space;Assume&space;S&space;has&space;m&space;chars,&space;and&space;there&space;should&space;be&space;m+1&space;states.&space;One&space;is&space;for&space;the&space;begin&space;state,&space;and&space;the&space;others&space;are&space;for&space;matching&space;state&space;of&space;each&space;position&space;of&space;S.&space;Once&space;we&space;have&space;built&space;the&space;FSM,&space;we&space;can&space;run&space;it&space;on&space;any&space;input&space;string.&space;##&space;KMP&space;>Knuth-Morris-Pratt&space;method&space;The&space;idea&space;is&space;inspired&space;by&space;FSM.&space;We&space;can&space;avoid&space;computing&space;the&space;transition&space;functions.&space;Instead,&space;we&space;compute&space;a&space;prefix&space;function&space;P&space;in&space;O(m)&space;time,&space;which&space;has&space;only&space;m&space;entries.&space;>&space;Prefix&space;funtion&space;stores&space;info&space;about&space;how&space;the&space;pattern&space;matches&space;against&space;shifts&space;of&space;itself.&space;-&space;String&space;w&space;is&space;a&space;prefix&space;of&space;string&space;x,&space;if&space;x=wy&space;for&space;some&space;string&space;y&space;-&space;String&space;w&space;is&space;a&space;suffix&space;of&space;string&space;x,&space;if&space;x=yw&space;for&space;some&space;string&space;y&space;-&space;The&space;k-character&space;prefix&space;of&space;the&space;pattern&space;P&space;[1..m]&space;denoted&space;by&space;Pk.&space;-&space;Given&space;that&space;pattern&space;prefix&space;P&space;[1..q]&space;matches&space;text&space;characters&space;T&space;[(s+1)..(s+q)],&space;what&space;is&space;the&space;least&space;shift&space;s'>&space;s&space;such&space;that&space;P&space;[1..k]&space;=&space;T&space;[(s'+1)..(s'+k)]&space;where&space;s'+k=s+q?&space;-&space;At&space;the&space;new&space;shift&space;s',&space;no&space;need&space;to&space;compare&space;the&space;first&space;k&space;characters&space;of&space;P&space;with&space;corresponding&space;characters&space;of&space;T.&space;Method:&space;For&space;prefix)p_i![](https://latex.codecogs.com/gif.latex?,&space;find&space;the&space;longest&space;proper&space;prefix&space;of)p_i![](https://latex.codecogs.com/gif.latex?that&space;is&space;also&space;a&space;suffix&space;of)p_i![](https://latex.codecogs.com/gif.latex?.&space;) pre[q] = max\{k|k ## 2.1. 随机构造的二叉查找树 -下面可以证明,随机构造,即输入序列有 $n!$中, 每种概率相同的情况下, 期望的树高 $h=O(logn)$ +下面可以证明,随机构造,即输入序列有 ![](https://latex.codecogs.com/gif.latex?n!)中, 每种概率相同的情况下, 期望的树高 ![](https://latex.codecogs.com/gif.latex?h=O(logn)) (直接搬运算法导论上面的啦>_<) ![](https://upload-images.jianshu.io/upload_images/7130568-69c57614410f6abd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) @@ -67,7 +67,7 @@ h是树高, 但是由于插入,删除而导致树不平衡, 即可能 $h\geqslan ## 2.2. 平均结点深度 一个较 上面定理 弱的结论: ->一棵随机构造的二叉查找树,n 个结点的平均深度为 $O(logn)$ +>一棵随机构造的二叉查找树,n 个结点的平均深度为 ![](https://latex.codecogs.com/gif.latex?O(logn)) 类似 RANDOMIZED-QUICKSORT 的证明过程, 因为快排 递归的过程就是一个递归 二叉树. 随机选择枢纽元就相当于这里的某个子树的根结点 在所有结点的大小随机排名, 如 i. 然后根结点将剩下的结点划分为左子树(i-1)个结点, 右子树(n-i)个结点. @@ -75,44 +75,29 @@ h是树高, 但是由于插入,删除而导致树不平衡, 即可能 $h\geqslan ![](https://upload-images.jianshu.io/upload_images/7130568-6bf2b5a6d286adca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 2.3. 不同的二叉树数目(Catalan num) -给定$\{1,2,\ldots,n\}$,组成二叉查找树的数目. +给定![](https://latex.codecogs.com/gif.latex?\{1,2,\ldots,n\}),组成二叉查找树的数目. 由上面的证明过程, 可以容易地分析得出, 任选第 i 个数作为根, 由于二叉查找树的性质, 其左子树 应该有 i-1个结点, 右子树有 n-i个结点. -如果记 n 个结点 的二叉查找树的数目为$b_n$ +如果记 n 个结点 的二叉查找树的数目为![](https://latex.codecogs.com/gif.latex?b_n) 则有递推公式 -$$ -b_n=\begin{cases} -1 &n=0 \\ -\sum_{i=1}^{n}b_{i-1}b_{n-i} & n\geqslant 1 -\end{cases} -$$ +![](https://latex.codecogs.com/gif.latex?&space;b_n=\begin{cases}&space;1&space;&n=0&space;\\&space;\sum_{i=1}^{n}b_{i-1}b_{n-i}&space;&&space;n\geqslant&space;1&space;\end{cases}&space;) 然后我们来看`<<算法导论>>`(p162,思考题12-4)上怎么求的吧( •̀ ω •́ )y 设生成函数 -$$B(x)=\sum_{n=0}^{\infty}b_n x^n$$ -下面证明$B(x)=xB(x)^2+1$ -易得$$xB(x)^2=\sum_{i=1}^{\infty}\sum_{n=i}^{\infty}b_{i-1}b_{n-i}x^n$$ -对比$B(x), xB(x)^2+1$的 x 的各次系数,分别是 $b_k,a_{k}$ -当 k=0, $a_k=1=b_k$ +![](https://latex.codecogs.com/gif.latex?B(x)=\sum_{n=0}^{\infty}b_n&space;x^n) +下面证明![](https://latex.codecogs.com/gif.latex?B(x)=xB(x)^2+1) +易得![](https://latex.codecogs.com/gif.latex?xB(x)^2=\sum_{i=1}^{\infty}\sum_{n=i}^{\infty}b_{i-1}b_{n-i}x^n) +对比![](https://latex.codecogs.com/gif.latex?B(x),&space;xB(x)^2+1)的 x 的各次系数,分别是 ![](https://latex.codecogs.com/gif.latex?b_k,a_{k}) +当 k=0, ![](https://latex.codecogs.com/gif.latex?a_k=1=b_k) 当 k>0 -$$a_{k} = \sum_{i=1}^{k}b_{i-1}b_{k-i} = b_k$$ -所以$B(x)=xB(x)^2+1$ +![](https://latex.codecogs.com/gif.latex?a_{k}&space;=&space;\sum_{i=1}^{k}b_{i-1}b_{k-i}&space;=&space;b_k) +所以![](https://latex.codecogs.com/gif.latex?B(x)=xB(x)^2+1) 由此解得 -$$B(x)=\frac{1-\sqrt{1-4x} }{2x}$$ +![](https://latex.codecogs.com/gif.latex?B(x)=\frac{1-\sqrt{1-4x}&space;}{2x}) 在点 x=0 处, 用泰勒公式得 -$$ -\begin{aligned} -\lim_{x\to 0}\sqrt{1-4x}&=1+\sum_{n=1}^{\infty}C_n^{\frac{1}{2}}{(-4)}^nx^n \\ -&=1+\sum_{n=1}^{\infty}\frac{(2n-3)!!{(-4x)}^n}{n!} -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;\lim_{x\to&space;0}\sqrt{1-4x}&=1+\sum_{n=1}^{\infty}C_n^{\frac{1}{2}}{(-4)}^nx^n&space;\\&space;&=1+\sum_{n=1}^{\infty}\frac{(2n-3)!!{(-4x)}^n}{n!}&space;\end{aligned}&space;) 所以对应系数 -$$ -\begin{aligned} -b_n&=\frac{1}{2}\frac{4^{n+1}(2n-1)!!}{2^{n+1}n!} \\ - &=\frac{C_{2n}^{n}}{n+1} -\end{aligned} -$$ +![](https://latex.codecogs.com/gif.latex?&space;\begin{aligned}&space;b_n&=\frac{1}{2}\frac{4^{n+1}(2n-1)!!}{2^{n+1}n!}&space;\\&space;&=\frac{C_{2n}^{n}}{n+1}&space;\end{aligned}&space;) 这个数叫做 `Catalan 数` ## 2.4. 好括号列 @@ -124,28 +109,26 @@ $$ * 若A是好括号列, 则 (A)是好括号列 ->充要条件: 好括号列 $\Longleftrightarrow$ 左右括号数相等, 且从左向右看, 看到的右括号数不超过左括号数 +>充要条件: 好括号列 ![](https://latex.codecogs.com/gif.latex?\Longleftrightarrow) 左右括号数相等, 且从左向右看, 看到的右括号数不超过左括号数 ->定理: 由 n个左括号,n个右括号组成的好括号列个数为$c(n)=\frac{C_{2n}^{n}}{n+1}$ +>定理: 由 n个左括号,n个右括号组成的好括号列个数为![](https://latex.codecogs.com/gif.latex?c(n)=\frac{C_{2n}^{n}}{n+1}) 证明: -由 n左n右组成的括号列有 $\frac{2n}{n!n!}=C_{2n}^{n}$个. - 设括号列$a_1a_2\ldots a_{2n}$为坏括号列, -由充要条件, 存在最小的 j, 使得$a_1a_2\ldots a_{j}$中右括号比左括号多一个, -由于是最小的 j, 所以 $a_j$为右括号, $a_{j+1}$为右括号 -把$a_{j+1}a_{j+2}\ldots a_{2n}$中的左括号变为右括号, 右变左,记为$\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$ +由 n左n右组成的括号列有 ![](https://latex.codecogs.com/gif.latex?\frac{2n}{n!n!}=C_{2n}^{n})个. + 设括号列![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{2n})为坏括号列, +由充要条件, 存在最小的 j, 使得![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{j})中右括号比左括号多一个, +由于是最小的 j, 所以 ![](https://latex.codecogs.com/gif.latex?a_j)为右括号, ![](https://latex.codecogs.com/gif.latex?a_{j+1})为右括号 +把![](https://latex.codecogs.com/gif.latex?a_{j+1}a_{j+2}\ldots&space;a_{2n})中的左括号变为右括号, 右变左,记为![](https://latex.codecogs.com/gif.latex?\bar&space;a_{j+1}\bar&space;a_{j+2}\ldots&space;\bar&space;a_{2n}) -则括号列$a_1a_2\ldots a_{j}\bar a_{j+1}$为好括号列 -$a_1a_2\ldots a_{j}\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$可好可坏,且有n-1个右,n+1个左, 共有$\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1}$个. +则括号列![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{j}\bar&space;a_{j+1})为好括号列 +![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{j}\bar&space;a_{j+1}\bar&space;a_{j+2}\ldots&space;\bar&space;a_{2n})可好可坏,且有n-1个右,n+1个左, 共有![](https://latex.codecogs.com/gif.latex?\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1})个. -所以坏括号列$a_1a_2\ldots a_{2n}$ 与括号列 $a_1a_2\ldots a_{j}\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$, 有$\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1}$个 +所以坏括号列![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{2n}) 与括号列 ![](https://latex.codecogs.com/gif.latex?a_1a_2\ldots&space;a_{j}\bar&space;a_{j+1}\bar&space;a_{j+2}\ldots&space;\bar&space;a_{2n}), 有![](https://latex.codecogs.com/gif.latex?\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1})个 那么好括号列有 -$$ -c(n)=C_{2n}^{n} - C_{2n}^{n+1} =\frac{C_{2n}^{n}}{n+1} -$$ +![](https://latex.codecogs.com/gif.latex?&space;c(n)=C_{2n}^{n}&space;-&space;C_{2n}^{n+1}&space;=\frac{C_{2n}^{n}}{n+1}&space;) >推论: n个字符,进栈出栈(出栈可以在栈不为空的时候随时进行), 则出栈序列有 c(n)种 @@ -184,7 +167,7 @@ Aho-Corasick automation,是在字典树上添加匹配失败边(失配指针), # 5. 平衡二叉树 -上面的二叉查找树不平衡,即经过多次插入,删除后, 其高度变化大, 不能保持$\Theta(n)$的性能 +上面的二叉查找树不平衡,即经过多次插入,删除后, 其高度变化大, 不能保持![](https://latex.codecogs.com/gif.latex?\Theta(n))的性能 而平衡二叉树就能. 平衡二叉树都是经过一些旋转操作, 使左右子树的结点高度相差不大,达到平衡 有如下几种 @@ -199,7 +182,7 @@ Aho-Corasick automation,是在字典树上添加匹配失败边(失配指针), ## 5.2. splayTree 伸展树, 它的特点是每次将访问的结点通过旋转旋转到根结点. -其实它并不平衡. 但是插入,查找,删除操作 的平摊时间是$O(logn)$ +其实它并不平衡. 但是插入,查找,删除操作 的平摊时间是![](https://latex.codecogs.com/gif.latex?O(logn)) 有三种旋转,下面都是将访问过的 x 旋转到 根部 ### 5.2.1. Zig-step @@ -216,7 +199,7 @@ Aho-Corasick automation,是在字典树上添加匹配失败边(失配指针), ## 5.4. treap -[前面提到](#21-随机构造的二叉查找树), 随机构造的二叉查找树高度为 $h=O(logn)$,以及在[算法 general](/alg-genral.html) 中说明了怎样 随机化(shuffle)一个给定的序列. +[前面提到](#21-随机构造的二叉查找树), 随机构造的二叉查找树高度为 ![](https://latex.codecogs.com/gif.latex?h=O(logn)),以及在[算法 general](/alg-genral.html) 中说明了怎样 随机化(shuffle)一个给定的序列. 所以,为了得到一个平衡的二叉排序树,我们可以将给定的序列随机化, 然后再进行构造二叉排序树. diff --git "a/\350\256\241\347\256\227\346\234\272\347\263\273\347\273\237\350\257\246\350\247\243/codes/mbinary/PB16030899 -\346\234\261\346\262\263\345\213\244-csapp-bomb-lab-report.md" "b/\350\256\241\347\256\227\346\234\272\347\263\273\347\273\237\350\257\246\350\247\243/codes/mbinary/PB16030899 -\346\234\261\346\262\263\345\213\244-csapp-bomb-lab-report.md" index cc8b6b1..e69de29 100644 --- "a/\350\256\241\347\256\227\346\234\272\347\263\273\347\273\237\350\257\246\350\247\243/codes/mbinary/PB16030899 -\346\234\261\346\262\263\345\213\244-csapp-bomb-lab-report.md" +++ "b/\350\256\241\347\256\227\346\234\272\347\263\273\347\273\237\350\257\246\350\247\243/codes/mbinary/PB16030899 -\346\234\261\346\262\263\345\213\244-csapp-bomb-lab-report.md" @@ -1,428 +0,0 @@ - -**PB16030899-朱河勤
2018-3-16** - - -#
CSAPP-BOMB-LAB - - -下载得到bomb.tar文件,解压后只有bomb二进制文件,以及一个bomb.c文件,bomb.c没有对应的头文件. 所有思路只有是反汇编bomb,分析汇编代码. - -这里用到两个非常强大的工具objdump,gdb -* objdump用来反汇编的,-d参数得到x86汇编, -M参数还可以选择不同的汇编形式, 比如 -M 8086 得到8086汇编, 详细内容可以man objdump. -* gdb是强大的GNU DEBUGGER 用法如下 -``` - (gdb) b(breakpoint):用法:b 函数名 :对此函数进行中断 ;b 文件名:行号; - (gdb) run:启动程序,运行至程序的断点或者结束; - (gdb) l(list):用法:l funcname,制定函数的源码 - (gdb) s(step):进入函数,逐语句运行; - (gdb) n(next):不进入函数,逐过程运行; - (gdb) c(continue):继续运行,跳至下一个断点; - (gdb) p(print):打印显示变量值; - (gdb) set variable=value,为变量赋值; - (gdb) kill:终止调试的程序; - (gdb) h(help):列出gdb详细命令帮助列表; - (gdb) clear filename.c:30:清除30行处的断点; - (gdb) info break:显示断点信息; - (gdb) delete 断点编号:断点编号是info break 后显示出来的; - (gdb) bt(backtrace):回溯到段出错的位置; - (gdb) frame 帧号:帧号是bt命令产生的堆栈针; - (gdb) q:退出; - (gdb) x(examine):查看内存中的值等//详细内容在gdb中输入 help x查看 -``` -下面开始拆 :bomb: 之旅 - -## general -观察汇编代码,可以看到有main, phase1--6, 等, 重点看这几个函数, 从main开始, 结合bomb.c,可以明白程序的控制流, 每个阶段用phase函数判断输入是否正确,不正确就boon,结束程序 - -![](src/main.png) - -## phase1 -来到phase1, - -![](src/p1.png) - -第一行准备栈帧,第二行就是将地址存入$esi, 这是一个字符串的地址, 可以猜测下面string_not_equal就是比较这个字符串与输入字符串是否相等的函数.(最开始我还去分析了这个函数的汇编代码,确实是那样,先比较长度,然后逐一比较. 所以找到这个地址`0x402400`存储的字符串就行了,在asm文件中搜索,没有,所以要在程序运行时才可以到达这个虚拟地址, 未来address space 的堆中. 这时就要用到强大的gdb了, - -切换到bomb文件夹,依次输入 -```shell -gdb -(gdb) file bomb -(gdb) x /s 0x402400 # x(examine) s参数是string的意思 -``` -即得**Border relations with Canada have never been better.** - -## phase2 -![](src/p2.png) - -所以答案是 `1 2 4 8 16 32` - -## phase3 - -``` - 0000000000400f43 : - 400f43: sub $0x18,%rsp - 400f47: lea 0xc(%rsp),%rcx - 400f4c: lea 0x8(%rsp),%rdx - 400f51: mov $0x4025cf,%esi # 又是一个字符串,可以用gdb查看, 得到`"%d %d",格式化字符串,说明输入两个数字 - 400f56: mov $0x0,%eax - 400f5b: callq 400bf0 <__isoc99_sscanf@plt> # 输入 - 400f60: cmp $0x1,%eax # 判断输入成功 - 400f63: jg 400f6a - 400f65: callq 40143a - 400f6a: cmpl $0x7,0x8(%rsp) # 第一个参数是否小于等于7,大于则boom - 400f6f: ja 400fad - 400f71: mov 0x8(%rsp),%eax - 400f75: jmpq *0x402470(,%rax,8) # 以下是switch, 根据rax,即第一个输入的参数跳转 - 400f7c: mov $0xcf,%eax # 由此容易得到答案, 比如这里是rax=0时, 则 另一个参数为0xcf = 207 - 400f81: jmp 400fbe - 400f83: mov $0x2c3,%eax - 400f88: jmp 400fbe - 400f8a: mov $0x100,%eax - 400f8f: jmp 400fbe - 400f91: mov $0x185,%eax - 400f96: jmp 400fbe - 400f98: mov $0xce,%eax - 400f9d: jmp 400fbe - 400f9f: mov $0x2aa,%eax - 400fa4: jmp 400fbe - 400fa6: mov $0x147,%eax - 400fab: jmp 400fbe - 400fad: callq 40143a - 400fb2: mov $0x0,%eax - 400fb7: jmp 400fbe - 400fb9: mov $0x137,%eax - 400fbe: cmp 0xc(%rsp),%eax - 400fc2: je 400fc9 - 400fc4: callq 40143a - 400fc9: add $0x18,%rsp - 400fcd: retq -``` -swith跳转表 -%rax 跳转地址 0xc(%rsp) -0 0x0000000000400f7c 0xcf 207 -1 0x0000000000400fb9 0x137 311 -2 0x0000000000400f83 0x2c3 707 -3 0x0000000000400f8a 0x100 256 -4 0x0000000000400f91 0x185 389 -5 0x0000000000400f98 0xce 206 -6 0x0000000000400f9f 0x2aa 682 -7 0x0000000000400fa6 0x147 327 -所以结果为`0 207` ... - -## phase4 - -``` - 000000000040100c : - 40100c: sub $0x18,%rsp - 401010: lea 0xc(%rsp),%rcx - 401015: lea 0x8(%rsp),%rdx - 40101a: mov $0x4025cf,%esi #同样,gdb 中x /s 知道输入两个数字 - 40101f: mov $0x0,%eax - 401024: callq 400bf0 <__isoc99_sscanf@plt> - 401029: cmp $0x2,%eax # 判断是否输入两个数 - 40102c: jne 401035 - 40102e: cmpl $0xe,0x8(%rsp) # 判断每个数是否≤14 ,大于则boom - 401033: jbe 40103a # 跳转 - 401035: callq 40143a - 40103a: mov $0xe,%edx # 构造func4的参数 (phase4调用的) - 40103f: mov $0x0,%esi # 构造func4的参数 - 401044: mov 0x8(%rsp),%edi # 构造func4的参数 - 401048: callq 400fce - 40104d: test %eax,%eax # 测试, func4返回0, 若不,则boom - 40104f: jne 401058 - 401051: cmpl $0x0,0xc(%rsp) - 401056: je 40105d - 401058: callq 40143a - 40105d: add $0x18,%rsp - 401061: retq - ``` - 将func4转换为c语言,并用0--14测试, 这点很难, 需要翻译汇编语言,花很多时间,得熟悉汇编代码才行 - ```c - int func4(int a, int b, int c) -{ - int result; - result = c; - result = result - b; - int tmp = result; - tmp = (unsigned)tmp >> 31; - result = result + tmp; - result = result / 2; - tmp = result + b; - if(tmp > a) - { - c = tmp - 1; - result = func4(a, b, c); - return (2 * result); - } - result = 0; - if(tmp < a) - { - b = tmp + 1; - result = func4(a, b, c); - return (1 + 2 * result); - } - return result; -} -//测试从0~14范围内满足条件的值 - -int main() -{ - for(int input = 0; input < 15; ++input) - { - int result = func4(input, 0, 14); - if(result == 0) - { - printf("input = %d, func4 = %d\n", input, result); - } - } - return 0; -} - -``` -得到可行解 -因此phase4可能结果为: -0 0 -1 0 -3 0 -7 0 - - -## phase5 -嗯, 加油, 还有两关了. (●ˇ∀ˇ●) -```asm - 0000000000401062 : - 401062: push %rbx - 401063: sub $0x20,%rsp - 401067: mov %rdi,%rbx - 40106a: mov %fs:0x28,%rax - 401071: - 401073: mov %rax,0x18(%rsp) - 401078: xor %eax,%eax - 40107a: callq 40131b - 40107f: cmp $0x6,%eax # 说明输入是六个字符 - 401082: je 4010d2 - 401084: callq 40143a - 401089: jmp 4010d2 - 40108b: movzbl (%rbx,%rax,1),%ecx # 从栈帧中取出各个字符,记为x - 40108f: mov %cl,(%rsp) - 401092: mov (%rsp),%rdx - 401096: and $0xf,%edx # y=0xf & x, 即将一个byte的高4位置0 - 401099: movzbl 0x4024b0(%rdx),%edx # 用gdb查看x /s 0x4024b0 得到字符串"maduiersnfotvbyl",所以这一行是以y作为偏移量,取字符数组的第几个字符 - 4010a0: mov %dl,0x10(%rsp,%rax,1) # 将取得的存于栈帧中 //后面用string_not_equl 比较 - 4010a4: add $0x1,%rax - 4010a8: cmp $0x6,%rax # 循环6次 - 4010ac: jne 40108b - 4010ae: movb $0x0,0x16(%rsp) - 4010b3: mov $0x40245e,%esi # 这是要比较的字符串, 同样用gdb查看得到 "flyers" - 4010b8: lea 0x10(%rsp),%rdi - 4010bd: callq 401338 - 4010c2: test %eax,%eax - 4010c4: je 4010d9 - 4010c6: callq 40143a - 4010cb: nopl 0x0(%rax,%rax,1) - 4010d0: jmp 4010d9 - 4010d2: mov $0x0,%eax - 4010d7: jmp 40108b - 4010d9: mov 0x18(%rsp),%rax - 4010de: xor %fs:0x28,%rax - 4010e5: - 4010e7: je 4010ee - 4010e9: callq 400b30 <__stack_chk_fail@plt> - 4010ee: add $0x20,%rsp - 4010f2: pop %rbx - 4010f3: retq - ``` - 解释在上面, 反向得到需要的输入的思路是: 对flyers的每个字符, 得到在字符数组中的index, 也就是输入的字符的后4位bit, 而键盘输入一般是字母, 所以很可能有两种可能,字符byte的高四位为`0100`或`0110`,而且可以发现刚好这是大写字母/小写字母开始的前一个ascii,所以 - - ![](src/flyers.png) - - ## phase6 - phase6很难了,这真的要熟练汇编语言, 翻译一下,知道输入的是六个不相同的数字, 而且≤6 ,~~所以可以试全排列了~~ - ``` - (gdb) disas phase_6 -Dump of assembler code for function phase_6: - 0x00000000004010f4 <+0>: push %r14 将被调用者保存寄存器压入栈 - 0x00000000004010f6 <+2>: push %r13 - 0x00000000004010f8 <+4>: push %r12 - 0x00000000004010fa <+6>: push %rbp - 0x00000000004010fb <+7>: push %rbx %rsp = 0x7fffffffe2c0 - 0x00000000004010fc <+8>: sub $0x50,%rsp 分配栈空间 %rsp = 0x7fffffffe270 - 0x0000000000401100 <+12>: mov %rsp,%r13 - - 0x0000000000401103 <+15>: mov %rsp,%rsi - 0x0000000000401106 <+18>: callq 0x40145c 读入6个值,保存至从 %rsi 开始的地址 - - 0x000000000040110b <+23>: mov %rsp,%r14 - 0x000000000040110e <+26>: mov $0x0,%r12d %r12 置0,并且%r13 %r14 %rbp 均和 %rsp 指向相同地址 0x7fffffffe270 - - 0x0000000000401114 <+32>: mov %r13,%rbp - 0x0000000000401117 <+35>: mov 0x0(%r13),%eax 将第 %r13 指向的输入数复制到 %eax - 0x000000000040111b <+39>: sub $0x1,%eax 将输入数减1 - 0x000000000040111e <+42>: cmp $0x5,%eax 判断输入数是否小于等于6,因为上一步中减1操作 - 0x0000000000401121 <+45>: jbe 0x401128 若大于6,则调用 explode_bomb - 0x0000000000401123 <+47>: callq 0x40143a -========================================================================================================================================================= - 0x0000000000401128 <+52>: add $0x1,%r12d 将 %r12 加1 - 0x000000000040112c <+56>: cmp $0x6,%r12d 判断 %r12 是否等于6 - 0x0000000000401130 <+60>: je 0x401153 若等于6,跳转,否则继续执行 - 0x0000000000401132 <+62>: mov %r12d,%ebx 将 %r12 复制到 %ebx - - 0x0000000000401135 <+65>: movslq %ebx,%rax 将 %ebx 符号位扩展复制到 %rax - 0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax 将第 %ebx 输入数复制到 %eax - 0x000000000040113b <+71>: cmp %eax,0x0(%rbp) 比较 %r13 指向的输入数和 第 %ebx 输入数 是否相等 - 0x000000000040113e <+74>: jne 0x401145 如果相等,则调用 explode_bomb - 0x0000000000401140 <+76>: callq 0x40143a - 0x0000000000401145 <+81>: add $0x1,%ebx 将 %ebx 加1 - 0x0000000000401148 <+84>: cmp $0x5,%ebx 判断 %ebx 是否小于等于5 - 0x000000000040114b <+87>: jle 0x401135 若小于等于,跳转,否则继续执行;该循环判断 %r13 指向的数据和其后输入数不相等 - - 0x000000000040114d <+89>: add $0x4,%r13 将 %r13 指向下一个输入数,该循环判断所有的输入数全部不相等 - 0x0000000000401151 <+93>: jmp 0x401114 -========================================================================================================================================================= - 0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi 将 %rsi 指向栈中跳过读入数据位置作为结束标记,并且 %r14 仍和 %rsp 指向同一个位置 - 0x0000000000401158 <+100>: mov %r14,%rax 将 %r14 复制到 %rax - 0x000000000040115b <+103>: mov $0x7,%ecx - 0x0000000000401160 <+108>: mov %ecx,%edx 将立即数0x7复制到 %edx - 0x0000000000401162 <+110>: sub (%rax),%edx 立即数7减去 %r14 指向的数据 - 0x0000000000401164 <+112>: mov %edx,(%rax) 将7减的结果存回 %r14 执行的内存单元 - 0x0000000000401166 <+114>: add $0x4,%rax %rax 指向下一个输入数 - 0x000000000040116a <+118>: cmp %rsi,%rax 比较是否达到输入数组的末尾, - 0x000000000040116d <+121>: jne 0x401160 该循环使用立即数7减去每个输入数据 -========================================================================================================================================================== - 0x000000000040116f <+123>: mov $0x0,%esi 将 %rsi 置0 - 0x0000000000401174 <+128>: jmp 0x401197 - - 0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx 将 0x8(%rdx) 指向内存单元的内容复制到 %rdx, 指向链表下一个元素 - 0x000000000040117a <+134>: add $0x1,%eax 将 %eax 加1 - 0x000000000040117d <+137>: cmp %ecx,%eax 比较 %ecx 和 %eax 是否相等 - 0x000000000040117f <+139>: jne 0x401176 不相等,继续遍历链表,最终 %rdx 指向链表的第 %ecx 个节点 - 0x0000000000401181 <+141>: jmp 0x401188 - 0x0000000000401183 <+143>: mov $0x6032d0,%edx 重置链表首地址 - 0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) - 0x000000000040118d <+153>: add $0x4,%rsi - 0x0000000000401191 <+157>: cmp $0x18,%rsi - 0x0000000000401195 <+161>: je 0x4011ab - - 0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx 将 (%rsp + %rsi) 指向的数据复制到 %ecx - 0x000000000040119a <+166>: cmp $0x1,%ecx 比较 %ecx 是否小于等于1 - 0x000000000040119d <+169>: jle 0x401183 若小于等于,跳转,否则继续执行, 等于1, %edx 直接指向链表首地址 - 0x000000000040119f <+171>: mov $0x1,%eax 将 %eax 置1 - 0x00000000004011a4 <+176>: mov $0x6032d0,%edx 将 %rdx 指向内存单元 0x6032d0 - 0x00000000004011a9 <+181>: jmp 0x401176 跳转; 该循环根据输入数将链表中对应的第输入数个节点的地址复制到 0x20(%rsp) 开始的栈中 - ========================================================================================================================================================== - 0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx 将0x20(%rsp)的链表节点地址复制到 %rbx - 0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax 将 %rax 指向栈中下一个链表节点的地址 - 0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi 将 %rsi 指向保存的链表节点地址的末尾 - 0x00000000004011ba <+198>: mov %rbx,%rcx - - 0x00000000004011bd <+201>: mov (%rax),%rdx - 0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) 将栈中指向的后一个节点的地址复制到前一个节点的地址位置 - 0x00000000004011c4 <+208>: add $0x8,%rax 移动到下一个节点 - 0x00000000004011c8 <+212>: cmp %rsi,%rax 判断6个节点是否遍历完毕 - 0x00000000004011cb <+215>: je 0x4011d2 - 0x00000000004011cd <+217>: mov %rdx,%rcx - 0x00000000004011d0 <+220>: jmp 0x4011bd - 0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) 该循环按照7减去输入数据的索引重新调整链表 -========================================================================================================================================================== - 0x00000000004011da <+230>: mov $0x5,%ebp - 0x00000000004011df <+235>: mov 0x8(%rbx),%rax 将 %rax 指向 %rbx 下一个链表节点 - 0x00000000004011e3 <+239>: mov (%rax),%eax - 0x00000000004011e5 <+241>: cmp %eax,(%rbx) 比较链表节点中第一个字段值的大小,如果前一个节点值大于后一个节点值,跳转 - 0x00000000004011e7 <+243>: jge 0x4011ee - 0x00000000004011e9 <+245>: callq 0x40143a - 0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx 将 %rbx 向后移动,指向栈中下一个链表节点的地址 - 0x00000000004011f2 <+254>: sub $0x1,%ebp 判断循环是否结束,该循环判断栈中重新调整后的链表节点是否按照降序排列 - 0x00000000004011f5 <+257>: jne 0x4011df - 0x00000000004011f7 <+259>: add $0x50,%rsp - 0x00000000004011fb <+263>: pop %rbx - 0x00000000004011fc <+264>: pop %rbp - 0x00000000004011fd <+265>: pop %r12 - 0x00000000004011ff <+267>: pop %r13 - 0x0000000000401201 <+269>: pop %r14 - 0x0000000000401203 <+271>: retq -End of assembler dump. - -(gdb) disas read_six_numbers -%rsi存储调用者phase_2栈帧的局部变量开始地址 -%rdx = %rsi + 0 -%rcx = %rsi + 4 -%r8 = %rsi + 8 -%r9 = %rsi + 12 -(%rsp) = %rsi + 16 -8(%rsp) = %rsi + 20 -Dump of assembler code for function read_six_numbers: - 0x000000000040145c <+0>: sub $0x18,%rsp - 0x0000000000401460 <+4>: mov %rsi,%rdx - 0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx - 0x0000000000401467 <+11>: lea 0x14(%rsi),%rax - 0x000000000040146b <+15>: mov %rax,0x8(%rsp) - 0x0000000000401470 <+20>: lea 0x10(%rsi),%rax - 0x0000000000401474 <+24>: mov %rax,(%rsp) - 0x0000000000401478 <+28>: lea 0xc(%rsi),%r9 - 0x000000000040147c <+32>: lea 0x8(%rsi),%r8 - 0x0000000000401480 <+36>: mov $0x4025c3,%esi - 0x0000000000401485 <+41>: mov $0x0,%eax - 0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt> - 0x000000000040148f <+51>: cmp $0x5,%eax - 0x0000000000401492 <+54>: jg 0x401499 - 0x0000000000401494 <+56>: callq 0x40143a - 0x0000000000401499 <+61>: add $0x18,%rsp - 0x000000000040149d <+65>: retq -``` -%rbp %rbx %r12~%15 被调用者保存寄存器 -%r10 %r11 调用者保存寄存器 -%rdi %rsi %rdx %rcx %r8 %r9 依次保存输入数1~6 - -假设输入数据为4 3 2 1 6 5 - -猜测0x6032d8为链表首地址,链表中每个节点占用12个Byte,前8字节保存两个4字Byte的整型数,剩余的4Byte存放下个节点地址 - -GDB查看使用7减去对应的输入后的数据 -(gdb) p /x $rsp -$1 = 0x7fffffffe270 -(gdb) x/6dw 0x7fffffffe270 -0x7fffffffe270: 3 4 5 6 -0x7fffffffe280: 1 2 - -重新调整链表前的链表的结构 -(gdb) x/24xw 0x006032d0 -0x6032d0 : 0x0000014c 0x00000001 0x006032e0 0x00000000 -0x6032e0 : 0x000000a8 0x00000002 0x006032f0 0x00000000 -0x6032f0 : 0x0000039c 0x00000003 0x00603300 0x00000000 -0x603300 : 0x000002b3 0x00000004 0x00603310 0x00000000 -0x603310 : 0x000001dd 0x00000005 0x00603320 0x00000000 -0x603320 : 0x000001bb 0x00000006 0x00000000 0x00000000 - -保存在栈中链表节点信息 -(gdb) x/6xg 0x7fffffffe290 -0x7fffffffe290: 0x00000000006032f0 0x0000000000603300 -0x7fffffffe2a0: 0x0000000000603310 0x0000000000603320 -0x7fffffffe2b0: 0x00000000006032d0 0x00000000006032e0 - -按照7减去对应的输入后重新调整链表后的链表结构,索引顺序为 3 4 5 6 1 2 -(gdb) x/24xw 0x006032d0 -0x6032d0 : 0x0000014c 0x00000001 0x006032e0 0x00000000 -0x6032e0 : 0x000000a8 0x00000002 0x00000000 0x00000000 -0x6032f0 : 0x0000039c 0x00000003 0x00603300 0x00000000 -0x603300 : 0x000002b3 0x00000004 0x00603310 0x00000000 -0x603310 : 0x000001dd 0x00000005 0x00603320 0x00000000 -0x603320 : 0x000001bb 0x00000006 0x006032d0 0x00000000 - -破解思路: -将链表中每个节点按照前4字节降序排序 -3 4 5 6 1 2 -因为在前面使用7减去对应的值,所以破解密码 -4 3 2 1 6 5 - - -## final - -![](src/defued.png) - -啊,终于拆除💣了, -╰(*°▽°*)╯ -等等, 还漏了什么`? 在asm中,可以看到还有secret_phase这个函数,可是这个函数的调用是有技巧的,追踪发现是在phase_defused中调用的,同样, 查看字符串, 发现比较了"DrEvil", 以及一个格式串"%d %d %s",可能是phase3,phase4的数字加上DrEvil输入.可是最后我试了很多篇都没有是出来. -最后在gdb中设置断点, 然后`jump secret_phase` 即可进入 -## 总结 -通过这个lab,学到了gdb,objdump等工具的使用, 对汇编语言更熟悉, 对函数调用中栈帧的变化, 动态变量的理解更加深刻 -不得不佩服作者,以及这个lab的有趣与实用 \ No newline at end of file