From 7e3583dbffeb115067b3cb1db2726682a8bdfe76 Mon Sep 17 00:00:00 2001 From: mbinary Date: Thu, 6 Sep 2018 18:50:05 +0800 Subject: [PATCH] Add fib-heap and graph algorithm notes --- notes/b-tree.md | 1204 +++++++++++++++++++++++---------------------- notes/fib-heap.md | 151 ++++++ notes/graph.md | 464 +++++++++++++++++ 3 files changed, 1223 insertions(+), 596 deletions(-) create mode 100644 notes/fib-heap.md create mode 100644 notes/graph.md diff --git a/notes/b-tree.md b/notes/b-tree.md index 588004e..9a75293 100644 --- a/notes/b-tree.md +++ b/notes/b-tree.md @@ -1,596 +1,608 @@ - - -- [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-参考资料) - - - -# 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树, 等价于[红黑树](https://mbinary.coding.me/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/data-structure-and-algorithm) - - -## 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: +--- + + + +- [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-参考资料) + + + + +{% note info %}从此心里有了B数(●'◡'●){% endnote %} + +# 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树, 等价于[红黑树](https://mbinary.coding.me/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/data-structure-and-algorithm) + + +## 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/notes/fib-heap.md b/notes/fib-heap.md new file mode 100644 index 0000000..57e0029 --- /dev/null +++ b/notes/fib-heap.md @@ -0,0 +1,151 @@ + +![](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. 势函数 +$\Phi(H) = t(H) + 2m(h)$ +t 是根链表中树的数目,m(H) 表示被标记的结点数 + +最初没有结点 + +# 3. 最大度数 +$D(n)\leqslant \lfloor lgn \rfloor$ +![](https://upload-images.jianshu.io/upload_images/7130568-c9e0cd3be4e98c4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + + +# 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. 删除结点 +decrease(H,nd, MIN) + + +- [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-删除结点) + + \ No newline at end of file diff --git a/notes/graph.md b/notes/graph.md new file mode 100644 index 0000000..da38f93 --- /dev/null +++ b/notes/graph.md @@ -0,0 +1,464 @@ + + +- [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. Bellman-Ford 算法](#45-bellman-ford-算法) + - [4.6. Dijkstra 算法](#46-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$, 详细见[树](https://mbinary.coding.me/tree.html), + + +# 2. 搜索 + +求图的生成树[^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. 最小生成树 +利用了贪心算法, + +## 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 +## 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)$ +如果使用的是 [斐波那契堆](https://mbinary.coding.me/fib-heap.html), 则可改进到 $O(E+VlgV)$ + + +# 4. 单源最短路 +求一个结点到其他结点的最短路径, 可以用 Bellman-ford算法, 或者 Dijkstra算法. +定义两个结点u,v间的最短路 +$$ +\delta(u,v) = \begin{cases} +min(w(path)),\quad u\xrightarrow{path} v\\ +MAX, \quad u\nrightarrow v +\end{cases} +$$ +问题的变体 +* 单目的地最短路问题: 可以将所有边反向转换成求单源最短路问题 +* 单结点对的最短路径 +* 所有结点对最短路路径 + + +## 4.1. 负权重的边 +Dijkstra 算法不能处理, 只能用 Bellman-Ford 算法, +而且如果有负值圈, 则没有最短路, bellman-ford算法也可以检测出来 + +## 4.2. 初始化 +```python +def initialaize(G,s): + for v in G.V: + v.pre = None + v.distance = MAX + s.distance = 0 +``` + +## 4.3. 松弛操作 +```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.4. 有向无环图的单源最短路问题 +```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.5. Bellman-Ford 算法 +```python +def bellman-ford(G,s): + initialize(G,s) + for ct in range(|V|-1): # v-1times + 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.6. Dijkstra 算法 +```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})) +$$ +由于是简单路径, 则包含的边最多为 |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 +n = L.rows +L' = new matrix(nxn) +for i in range(n): + for j in range(n): + l'[i][j] = MAX + for k in range(n): + l'[i][j] = min(l'[i][j], l[i][k]+w[k][j]) +return L' +``` +可以看出该算法与矩阵乘法的关系 +$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}=MAX\\ +i, \quad i\neq j and \ w_{ij} d[i][k]+d[k][j]: + d2[i][j] = min(d[i][j], d[i][k]+d[k][j]) + pre2[i][j] = pre[k][j] + pre = pre2 + d = d2 +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]: 图论, 王树禾 \ No newline at end of file