『数据结构』B树(B-Tree)及其变体 B+树,B*树

2023-11-06

原文地址

1. 背景

当有大量数据储存在磁盘时, 如数据库的查找, 插入, 删除等操作的实现, 如果要读取或者写入, 磁盘的寻道, 旋转时间很长, 远大于在 内存中的读取, 写入时间.

平时用的二叉排序树搜索元素的时间复杂度虽然是 O(log2n) O ( l o g 2 n ) 的, 但是底数还是太小, 树高太高.

所以就出现了 B 树 (英文为 B-Tree, 不是 B 减树), 可以理解为多叉排序树. 一个结点可以有多个孩子, 于是增大了底数, 减小了高度, 虽然比较的次数多 (关键字数多), 但是由于是在内存中比较, 相较于磁盘的读取还是很快的.

2. 定义

度为 d(degree) 的 B 树 (阶 (order) 为 2d) 定义如下,
0. 每个结点中包含有 n 个关键字信息: (n,P0,K1,P1,K2,,Kn,Pn) ( n , P 0 , K 1 , P 1 , K 2 , … , K n , P n ) 。其中:
a) Ki K i 为关键字, 且关键字按顺序升序排序 Ki1<Ki K i − 1 < K i
b) Pi P i 为指向子树根的接点, Ki1<P(i1)<Ki K i − 1 < P ( i − 1 ) < K i
c) 关键字的数 n 满足 (由此也确定了孩子结点的个数): d1n2d1 d − 1 ⩽ n ⩽ 2 d − 1 (根节点可以少于 d-1)

  1. 树中每个结点最多含有 2d 个孩子(d>=2);
  2. 除根结点和叶子结点外, 其它每个结点至少有 d 个孩子;
  3. 若根结点不是叶子结点, 则至少有 2 个孩子(特殊情况:没有孩子的根结点, 即根结点为叶子结点, 整棵树只有一个根节点);
  4. 所有叶子结点都出现在同一层, 叶子节点没有孩子和指向孩子的指针

性质:
hlogd(n+12). h ≤ ⌊ log d ⁡ ( n + 1 2 ) ⌋ .

如下是 度为 2 的 B 树, 每个结点可能有 2,3 或 4 个孩子, 所以也叫 2,3,4 树, 等价于红黑树

3. 查找操作

可以看成二叉排序树的扩展, 二叉排序树是二路查找, B - 树是多路查找。
节点内进行查找的时候除了顺序查找之外, 还可以用二分查找来提高效率。

下面是顺序查找的 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<len(nd) and nd[i]==key:
                if withpath:return nd,i,fathers
                else:return nd,i
            if nd.isLeafNode():
                if withpath:return None,None,None
                else:return None,None
            nd = nd.getChd(i)

我实现时让 fathers 记录查找的路径, 方便在实现 delete 操作时使用 (虽然有种 delete 方法可以不需要, 直接 from up to down with no pass by),

4. 插入操作

自顶向下地进行插入操作, 最终插入在叶子结点,
考虑到叶子结点如果有 2t-1 (k1,k2,,k2t1) ( k 1 , k 2 , … , k 2 t − 1 ) 个 关键字, 则需要进行分裂,

一个有 2t-1 (k1,k2,,k2t1) ( k 1 , k 2 , … , k 2 t − 1 ) 个关键字 结点分裂是这样进行的: 此结点分裂为 两个关键字为 t-1 个的结点, 分别为 (k1,k2,,kt1) ( k 1 , k 2 , … , k t − 1 ) , (kt+1,kt+2,,k2t1) ( k t + 1 , k t + 2 , … , k 2 t − 1 ) , 然后再插入一个关键字 kt k t 到父亲结点.

注意同时要将孩子指针移动正确.

所以自顶向下地查找到叶子结点, 中间遇到 2t-1 个关键字的结点就进行分裂, 这样如果其子结点进行分裂, 上升来的一个关键字可以插入到父结点而不会超过 2t-1

代码如下

    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<len(nd) and nd[idx] == key:return
            if nd.isLeafNode():
                nd.insert(idx,key)
                self.keyNum+=1
                return
            else:
                chd = nd.getChd(idx)
                if len(chd)== self.degree*2-1: #ensure its keys won't excess when its chd split and u
                    nd = chd.split(nd,self.degree)
                    self.nodeNum +=1
                else:
                    nd = chd

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 的兄弟结点中借关键字: (这里看了网上的很多说法, 都是在介绍关键字的操作, 而没有提到孩子结点. 我实现的时候想了很久才想出来: 借关键字时, 比如从右兄弟借一个关键字 (第一个 k1 k 1 ), 此时即为左旋, 将父亲结点对应关键字移到当前结点, 再将右兄弟的移动父亲结点 (因为要满足排序性质, 类似二叉树的选择) 然后进行孩子操作, 将右兄弟的 p0 p 0 插入到 当前结点的孩子指针末尾) 左兄弟类似, 而且要注意到边界条件, 比如当前结点是第 0 个 / 最后一个孩子, 则没有 左兄弟 / 右兄弟)

    b. **合并**: 如果其左右兄弟结点中不存在关键字个数大于 t-1 的结点,进行结点合并:将其父结点中的关键字拿到下一层,与该节点的左右兄弟结点的所有关键字合并

同样要注意到边界条件, 比如当前结点是第 0 个 / 最后一个孩子, 则没有 左兄弟 / 右兄弟

3. 自底向上地检查来到这个叶子结点的路径上的结点是否满足关键字数目的要求, 只要关键字少于d-1,则进行旋转(2a)或者合并(2b)操作

* 删除结点在非叶子结点上
1. 查到到该结点, 然后转化成 上述 叶子结点中情况
2. 转化过程:
a. 找到相邻关键字:即需删除关键字的左子树中的最大关键字或右子树中的最小关键字
b. 用相邻关键字来覆盖需删除的非叶子节点关键字, 再删除原相邻关键字 (在; 叶子上, 这即为上述情况)。

python 代码如下, delete函数中, 查找到结点, 用 fathers::[(父节点, 关键字指针, 孩子指针)] 记录路径, 如果不是叶子结点, 就再进行查找, 并记录结点, 转换关键字.

rebalance 就是从叶子结点自底向上到根结点, 只要遇到关键字数少于 2d-1 的, 就进行平衡操作 (旋转, 合并)

实现时要很仔细, 考虑边界条件, 还有当是左孩子的时候操作的是父结点的 chdIdx 的前一个, 是右孩子的时候是 chdIdx 的关键字. 具体实现完整代码见文末.

    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-1: # rebalance tree from down to up
            prt,keyIdx,chdIdx = fathers[-1]
            lbro = [] if chdIdx==0 else prt.getChd(chdIdx-1)
            rbro = [] if chdIdx==len(prt) else prt.getChd(chdIdx+1)
            if len(lbro)<self.degree and len(rbro)<self.degree:  # merge two deficient nodes
                beforeNode,afterNode = None,None
                if lbro ==[]:
                    keyIdx = chdIdx
                    beforeNode,afterNode = nd,rbro
                else:
                    beforeNode,afterNode = lbro,nd
                    keyIdx = chdIdx-1      # important, when choosing
                keys = beforeNode[:]+[prt[keyIdx]]+afterNode[:]
                children = beforeNode.getChildren() + afterNode.getChildren()
                isLeaf = beforeNode.isLeafNode()
                prt.delChd(keyIdx+1)
                del prt[keyIdx]
                nd.update(keys,isLeaf,children)
                prt.children[keyIdx]=nd
                self.nodeNum -=1
            elif len(lbro)>=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] 上的

例如

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. 非叶子结点的子树指针  pi p i 指向关键字值属于  [ki,ki+1) [ k i , k i + 1 )  的子树(B- 树是开区间);
3. 为所有叶子结点增加一个链指针;
4. 所有关键字都在叶子结点出现

 B+ 的搜索与 B- 树也基本相同, 区别是 B+ 树只有达到叶子结点才命中(B- 树可以在非叶子结点命中), 其性能也等价于在关键字全集做一次二分查找;
下面摘自 wiki[^4]
>

查找

查找以典型的方式进行, 类似于二叉查找树。起始于根节点, 自顶向下遍历树, 选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来确定这个位置。

插入

节点要处于违规状态, 它必须包含在可接受范围之外数目的元素。

  1. 首先, 查找要插入其中的节点的位置。接着把值插入这个节点中。
  2. 如果没有节点处于违规状态则处理结束。
  3. 如果某个节点有过多元素, 则把它分裂为两个节点, 每个都有最小数目的元素。在树上递归向上继续这个处理直到到达根节点, 如果根节点被分裂, 则创建一个新根节点。为了使它工作, 元素的最小和最大数目典型的必须选择为使最小数不小于最大数的一半。

删除 

  1. 首先, 查找要删除的值。接着从包含它的节点中删除这个值。
  2. 如果没有节点处于违规状态则处理结束。
  3. 如果节点处于违规状态则有两种可能情况:
    1. 它的兄弟节点, 就是同一个父节点的子节点, 可以把一个或多个它的子节点转移到当前节点, 而把它返回为合法状态。如果是这样, 在更改父节点和两个兄弟节点的分离值之后处理结束。
    2. 它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中, 而且我们递归到父节点上, 因为它被删除了一个子节点。持续这个处理直到当前节点是合法状态或者到达根节点, 在其上根节点的子节点被合并而且合并后的节点成为新的根节点。

由于叶子结点间有指向下一个叶子的指针, 便于遍历, 以及区间查找, 所以数据库的以及操作系统文件系统的实现常用 B + 树,

7. B * 树

B*-tree [^5] 是 B+-tree 的变体, 在 B+ 树的基础上 (所有的叶子结点中包含了全部关键字的信息, 及指向含有这些关键字记录的指针),B * 树中非根和非叶子结点再增加指向兄弟的指针;B* 树定义了非叶子结点关键字个数至少为 (2/3)*M, 即块的最低使用率为 2/3(代替 B+ 树的 1/2)

8. 代码实现与测试

github 地址

8.1. 测试



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

8.2. 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 i<len(nd) and nd[i]==key:
                if withpath:return nd,i,fathers
                else:return nd,i
            if nd.isLeafNode():
                if withpath:return None,None,None
                else:return None,None
            nd = nd.getChd(i)
    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<len(nd) and nd[idx] == key:return
            if nd.isLeafNode():
                nd.insert(idx,key)
                self.keyNum+=1
                return
            else:
                chd = nd.getChd(idx)
                if len(chd)== self.degree*2-1: #ensure its keys won't excess when its chd split and u
                    nd = chd.split(nd,self.degree)
                    self.nodeNum +=1
                else:
                    nd = chd
    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-1: # rebalance tree from down to up
            prt,keyIdx,chdIdx = fathers[-1]
            lbro = [] if chdIdx==0 else prt.getChd(chdIdx-1)
            rbro = [] if chdIdx==len(prt) else prt.getChd(chdIdx+1)
            if len(lbro)<self.degree and len(rbro)<self.degree:  # merge two deficient nodes
                beforeNode,afterNode = None,None
                if lbro ==[]:
                    keyIdx = chdIdx
                    beforeNode,afterNode = nd,rbro
                else:
                    beforeNode,afterNode = lbro,nd
                    keyIdx = chdIdx-1      # important, when choosing
                keys = beforeNode[:]+[prt[keyIdx]]+afterNode[:]
                children = beforeNode.getChildren() + afterNode.getChildren()
                isLeaf = beforeNode.isLeafNode()
                prt.delChd(keyIdx+1)
                del prt[keyIdx]
                nd.update(keys,isLeaf,children)
                prt.children[keyIdx]=nd
                self.nodeNum -=1
            elif len(lbro)>=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. 参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

『数据结构』B树(B-Tree)及其变体 B+树,B*树 的相关文章

  • 如何在CentOS安装SQL Server数据库并通过内网穿透工具实现公网访问

    文章目录 前言 1 安装sql server 2 局域网测试连接 3 安装cpolar内网穿透 4 将sqlserver映射到公网 5 公网远程连接 6 固定连接公网地址 7 使用固定公网地址连接 前言 简单几步实现在Linux cento
  • 天猫数据分析工具推荐(天猫第三方数据平台)

    在电商迅速发展的大背景下 做好天猫数据分析能够在多方面帮助品牌商家更好地运营店铺 塑造品牌 如通过数据分析了解消费者的需求 购买偏好 这有利于品牌商家及时调整商品结构 产品推广 商品宣传等等 灵活制定品牌的销售策略 那么 天猫平台行业 品牌
  • SQL 解析与执行流程

    一 前言 在先前的技术博客中 我们已经详细介绍过数据库的 parser 模块与执行流程 用户输入的 SQL 语句通过词法解析器生成 token 再通过语法分析器生成抽象语法树 AST 经过 AST 生成对应的 planNode 最后执行 p
  • 内网穿透的应用-使用Net2FTP轻松部署本地Web网站并公网访问管理内网资源

    文章目录 1 前言 2 Net2FTP网站搭建 2 1 Net2FTP下载和安装 2 2 Net2FTP网页测试 3 cpolar内网穿透 3 1 Cpolar云端设置 3 2 Cpolar本地设置
  • AntDB内存管理之内存上下文之内存上下文机制是怎么实现的

    4 内存上下文机制是怎么实现的 下文将针对内存上下文机制进行代码说明 本次以AntDB的代码为例 来解析内存上下文的实现方式 4 1 最基础的数据结构 MemoryContextData和MemoryContextMethods是内存上下文
  • 【Mysql】InnoDB 引擎中的页目录

    一 页目录和槽 现在知道记录在页中按照主键大小顺序串成了单链表 那么我使用主键查询的时候 最顺其自然的办法肯定是从第一条记录 也就是 Infrimum 记录开始 一直向后找 只要存在总会找到 这种在数据量少的时候还好说 一旦数据多了 遍历耗
  • 【计算机毕业设计】实验室预约管理

    身处网络时代 随着网络系统体系发展的不断成熟和完善 人们的生活也随之发生了很大的变化 人们在追求较高物质生活的同时 也在想着如何使自身的精神内涵得到提升 而读书就是人们获得精神享受非常重要的途径 为了满足人们随时随地只要有网络就可以看书的要
  • 【计算机毕业设计】线上招聘问答系统

    计算机网络发展到现在已经好几十年了 在理论上面已经有了很丰富的基础 并且在现实生活中也到处都在使用 可以说 经过几十年的发展 互联网技术已经把地域信息的隔阂给消除了 让整个世界都可以即时通话和联系 极大的方便了人们的生活 所以说 线上招聘问
  • 38条Web测试经验分享

    1 页面链接检查 每一个链接是否都有对应的页面 并且页面之间切换正确 可以使用一些工具 如LinkBotPro File AIDCS HTML Link Validater Xenu等工具 LinkBotPro不支持中文 中文字符显示为乱码
  • 排序:计数排序

    一 概念 计数排序是非比较排序 是对哈希直接定址法的变形应用 二 思想 利用数组统计相同数据出现的次数 例如整型数据m出现n次 就在数组m位置记录数据为n 最后从头遍历数组打印数据即可 通俗来讲就是 数组下标即为数据 下标所指位置的值即为数
  • python超详细基础文件操作【建议收藏】

    文章目录 前言 发现宝藏 1 文件操作 1 1 文件打开与关闭 1 1 1 打开文件 1 1 2 关闭文件 1 2 访问模式及说明 2 文件读写 2 1 写数据 write 2 2 读数据 read 2 3 读数据 readlines 2
  • 206.翻转链表

    翻转链表 力扣 LeetCode 官网 全球极客挚爱的技术成长平台 备战技术面试 力扣提供海量技术面试资源 帮助你高效提升编程技能 轻松拿下世界 IT 名企 Dream Offer https leetcode cn problems re
  • 软件测试|SQLAlchemy环境安装与基础使用

    简介 SQLAlchemy 是一个强大的 Python 库 用于与关系型数据库进行交互 它提供了高度抽象的对象关系映射 ORM 工具 允许使用 Python 对象来操作数据库 而不必编写原生SQL查询 本文将介绍如何安装 SQLAlchem
  • 【计算机毕业设计】宝鸡文理学院学生成绩动态追踪系统

    研究开发宝鸡文理学院学生成绩动态追踪系统的目的是让使用者可以更方便的将人 设备和场景更立体的连接在一起 能让用户以更科幻的方式使用产品 体验高科技时代带给人们的方便 同时也能让用户体会到与以往常规产品不同的体验风格 与安卓 iOS相比较起来
  • 【计算机毕业设计】springbootstone音乐播放器的设计与实现

    随着我国经济的高速发展与人们生活水平的日益提高 人们对生活质量的追求也多种多样 尤其在人们生活节奏不断加快的当下 人们更趋向于足不出户解决生活上的问题 stone音乐播放器展现了其蓬勃生命力和广阔的前景 与此同时 为解决用户需求 stone
  • 数据结构——排序

    前言 哈喽小伙伴们好久不见 也是顺利的考完试迎来了寒假 众所周知 不怕同学是学霸 就怕学霸放寒假 假期身为弯道超车的最佳时间 我们定然是不能懒散的度过 今天我们就一起来学习数据结构初阶的终章 七大排序 本文所有的排序演示都为升序排序 目录
  • 顺序表和链表基础

    定义动态的顺序表 typedef int SLDataType typedef struct Seqlist SLDataType array size t size size t capacity Seqlist 在顺序表中插入数据 bo
  • 做测试不会 SQL?超详细的 SQL 查询语法教程来啦!

    前言 作为一名测试工程师 工作中在对测试结果进行数据比对的时候 或多或少要和数据库打交道的 要和数据库打交道 那么一些常用的sql查询语法必须要掌握 最近有部分做测试小伙伴表示sql查询不太会 问我有没有sql查询语法这一块的文档可以学习
  • 每日变更的最佳实践

    在优维公司内部 我们采用发布单的方式进行每天的应用变更管理 这里给各位介绍优维的最佳实践 变更是需要多角色合作的 而且他是整体研发流程的一部分 在优维内部 我们坚持每日变更 打通开发环节到最终发布上线的全过程 在保证质量的前提下 尽可能提升
  • btree 实现中的分段错误

    任何人都可以帮助消除这个分段错误 我已经在这个代码上工作了一个星期仍然无法调试它 这段代码是Btree的实现 插入部分工作正常 但删除部分出现分段错误 我无法调试它 有人可以帮忙吗 我已经根据此链接给出了输入 已将字母值转换为 ASCII

随机推荐

  • C语言指针转换为intptr_t类型

    随笔 155 文章 2 评论 342 C语言指针转换为intptr t类型 1 前言 今天在看代码时 发现将之一个指针赋值给一个intptr t类型的变量 由于之前没有见过intptr t这样数据类型 凭感觉认为intptr t是int类型
  • 一个五位数判断他是否为回文数。

    一个五位数判断他是否是回文数 代码 num int input munber n flag True while True if 10000 lt num lt 100000 print input number num break els
  • 腾讯三面被问到有没有参加过CTF?我反手就是一套军体拳打得面试官哑口无言!

    目录 前言 正文 什么是CTF 什么是PWN 为什么要学CTF CTF竞赛模式 CTF各大题型简介 学之前的思考 分析赛题情况 常规做法 CTF比赛需要的知识储备 CTF比赛的神器 恶补基础知识 信息安全专业知识推荐图书 前言 这是一场紧张
  • Typora字体颜色修改

    Typora字体颜色修改 typora中不能直接修改字体颜色 但有三种方法可以实现修改Typora中颜色 第一种 安装AutoHotKey 较简单 安装AutoHotKey windows系统快捷键设置软件 从而通过设置快捷键来达到修改字体
  • macbook brew install 经常遇见 No such file or directory @ rb_sysopen

    安装php brew install php 在执行过程中经常报错 比如以下 gt Installing php dependency openldap gt Pouring openldap 2 5 8 arm64 monterey bo
  • Docker介绍

    目录 docker定义 docker解决了什么问题 docker技术边界 docker给我们带来了哪些改变 docker和虚拟机的区别 docker基本架构 基本架构图 RootFs Linux Namespace 进程命名空间 查看元祖进
  • 动态规划快速入门

    更多内容 欢迎关注微信公众号 全菜工程师小辉 公众号回复关键词 领取免费学习资料 动态规划算法一直是面试手撕算法中比较有挑战的一种类型 很多的分配问题或者调度问题实际上都可能用动态规划进行解决 当然 如果问题的规模较大 有时候会抽象模型使用
  • 【CV】第 14 章:用最少的数据点训练

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • 情人节-动态爱心背景(html5+css+js)

    一 效果图 二 源代码
  • GDAL对TIF创建内建金字塔一个问题

    gdalwarp输出tif图像的时候 默认如果没有使用BIGTIFF YES选项 则会根据输出影像的大小进行判断 低于4G则不适用bigtiff格式 对于非bigtiff图像 如果这时候使用gdaladdo创建金字塔 内建模式 如果出现了文
  • SQL——游标

    非原创 东拼西凑来的 游标 cursor 是一个存储在MySQL服务器上的数据库查询 它不是一条SELECT语句 而是被该语句检索出来的结果集 在存储了游标之后 应用程序可以根据需要滚动或浏览其中的数据 游标主要用于交互式应用 其中用户需要
  • 业务安全及实战案例

    业务安全 关于漏洞 注入 业务逻辑 信息泄露 A04 2021 Insecure Design 在线靶场PortSwigger 1 概述 1 1 业务安全现状 1 1 1 业务逻辑漏洞 近年来 随着信息化技术的迅速发展和全球一体化进程的不断
  • php读写excel文件

    1 引入包 有不少提供读写excel文件的包 这里选择比较常用的一个 加到自己的项目里就好了 phpoffice phpspreadsheet 1 8 2 2 读取文件
  • Android中的USB中的UsbAccessory和UsbDevice的区别

    转载自 http www crifan com android usb usbaccessory vs usbdevice utm source tuicool utm medium referral UsbAccessory和UsbDev
  • MySQL更新表的记录详解

    目录 前言 前言 一 更新数据记录 1 特定数据记录 2 所有数据记录 总结 前言 更新数据记录是数据操作中常见的操作 可以更新表中已经存在数据记录中的值 在MySQL中可以通过UPDATE语句来实现更新数据记录 该SQL语句可以通过如下几
  • 5个炫酷登录页面,拿去就能用(附源码)

    5个炫酷登录页面 拿去就能用 附源码 登录页面 觉得显示效果很好 借鉴其他博主的 喜欢的可以收藏关注 不商用 只为学习传播 目录 1 炫酷星空登录 2 动态云层登录 3 深海灯光水母登录 4 炫酷蛛网登录 5 彩色气泡登录 1 炫酷星空登录
  • 响应式网页设计(Responsive Web Design)的核心原理

    聚沙成塔 每天进步一点点 专栏简介 响应式网页设计的核心原理 优点和缺点 优点 缺点 写在最后 专栏简介 前端入门之旅 探索Web开发的奇妙世界 欢迎来到前端入门之旅 感兴趣的可以订阅本专栏哦 这个专栏是为那些对Web开发感兴趣 刚刚踏入前
  • CVE-2022-26134 Confluence OGNL RCE 复现

    一 漏洞概述 Atlassian Confluence 是一款各企业广泛使用的 wiki 系统 在Atlassian Confluence Server and Data Center上存在OGNL 注入漏洞 远程攻击者在未经身份验证的情况
  • Servlet之间传递数据

    转自 http jallay iteye com blog 256004 1 如何让用户的请求数据从一个Servlet传递给另一个Servlet 第一种方式 通过超链接传递数据 第二种方式 通过表传递取参数 第三种方式 通过setAttri
  • 『数据结构』B树(B-Tree)及其变体 B+树,B*树

    原文地址 1 背景 当有大量数据储存在磁盘时 如数据库的查找 插入 删除等操作的实现 如果要读取或者写入 磁盘的寻道 旋转时间很长 远大于在 内存中的读取 写入时间 平时用的二叉排序树搜索元素的时间复杂度虽然是 O log2n O l o