1. 二叉树的层高和结点的关系
在二叉树中,层高和节点之间存在一种线性关系。层数和节点个数之间是一个线性关系,随着层数的增加,节点个数也会增加。在完全二叉树中,除了最后一层的节点个数可能不满,其他层的节点个数都是满的。最后一层的节点个数不满时,也是从左到右连续排列的。可以通过公式表示节点个数和层数的关系:n = 2^h - 1 + m,其中n为节点个数,h为完全二叉树的层数,m为最后一层的节点个数。因此,节点个数随着层数的增加而增加,而且增长的速度是指数级的。
2. 树的层高和查找的关系
树的层高和查找之间存在一定的关系。在二叉树中,层数越多,意味着需要比较的次数越多。这是因为在进行查找操作时,需要从根节点开始,沿着树的结构逐层向下遍历,直到找到目标节点或达到叶子节点为止。因此,树的层数越多,需要遍历的路径越长,比较的次数也就越多。这也就意味着查找时间会随着树层数的增加而增加。因此,在设计和实现二叉查找树时,需要根据实际应用场景和数据规模来权衡树的层数和节点之间的关系,以获得最佳的查找性能。
红黑树存放在内存中,数据的比较效率很高,可以忽略不计。如果在内存中没有命中,就得去磁盘中找。每一次寻找下一个结点是需要磁盘寻址的。磁盘寻址是个很麻烦的事情,是个很耗时的操作。所以出现了一些降层高的数据结构。
每一个结点都存储在磁盘,每次对比后找下一个结点是一次磁盘寻址。
3. B树
3.1 一个M阶B树T,满足的条件
- 每个结点至多拥有M棵子树
- 根节点至少拥有两棵子树
- 除了根节点以外,其余每个分支结点至少拥有M/2棵子树
- 所有的叶节点都在同一层上
- 有K棵子树的分支结点则存在K-1个关键字,关键字按照递增顺序进行排序
- 关键字数量满足ceil(M/2)-1 <= n <= M-1
B+树常用于索引,特别是在磁盘上。B树所有结点存储数据,B+树是叶子结点存储数据,内结点【有子结点的结点】用于索引。B+树更加适合于磁盘索引。
3.2 B树的定义
#define SUB_M 3
typedef struct _btree_node {
int *keys; //5
struct _btree_node** childrens; //6
}btree_node;
typedef struct _btree
{
struct _btree_node* root;
}btree;
3.3 B树的添加(M=6)
难点1:根节点的分叉 由1个结点变成3个结点,所要添加的F比C大,添加到右子树。
插入I时,I比C大,插入到C的右子树,但C的右子树存储的结点数量已经等于5,把F结点提到上面,下面分成两个结点:DE和GH。先分裂,再添加。
B树每次添加都是添加到叶子结点上,通过分裂增加层高。
3.4 B树的删除
删除结点A,如何找到A?A比C小,找第一个子树。
将A与I进行比较,A小于I,所以找到I的第一棵子树CF,CF数量为ceil(M/2)-1=2,所以想要借位I,I又向LORU借位,然后变成L、CFI、ORU。
然后比较A与C,A比C小,故找到C的第一棵子树:AB,AB的数量=ceil(M/2)-1,故想往CFI借位,CFI又往DE借位,但DE的数量=ceil(M/2)-1,故无法借位,只能合并。
如果直接删除A,就不满足B树的条件6:每棵子树的关键字数量在2~5之间了(M=3)。将AB子树与DE子树进行合并,同时把C推下来。因为子树AB比C小,合并的是C两侧的子树。
删除B:B比L小借位删除(满足顺序进行借位)B与L对比,比L小,走L的左边子树,但是子树FI的数量=ceil(M/2)-1。为了避免后面资源不足的现象需要借位。
删除C:C与O对比,比O小,往第一棵子树走,比F这个结点小,找到F的第一棵子树。
删除D:有一个合并。
删除E,E的第一棵子树IL=ceil(M/2)-1=2,需要借位,借不到,就合并。
删除H:直接删除。
删除I:直接删除
删除J:合并再删除【因为借位不行:子树MN的数量=ceil(M/2)-1】
在删除的时候只有两种情况,一种是借位,一种是合并。
B树删除的核心:B树先通过借位或者合并的方式将自身转化为可以删除的状态,再进行删除。
B树在查找子树结点的时候,当某个子树的结点树大于ceil(M/2)-1的时候,才能寻找下一级。
借位和合并的优先级:通过if else判断,哪个优先级高取决于哪个条件写在前面。
以上删除的都是叶子结点。若删除内结点:假设要删除结点O,找个替身当根节点:把L换到根节点上,O移到RU结点处变为ORU,把O放到叶子结点再进行删除。或者是通过合并,把O转移到叶子结点上删除O。
分裂只有在添加的时候才会有,借位或合并只有在删除的时候才会有。
根节点的分裂:一分为三。如果只有一个结点时。创建一个空结点,指向ABCDE结点(原来的根节点),然后把C放到新创建的结点上,然后将原来的根节点分成结点AB、DE。和原来的分裂不同的是:需要创建一个空结点。相当于父亲结点为新创建的结点,子树为第0棵子树。
要求:理解B树的添加、删除的两个过程,可以自己解释。需要清楚代码的逻辑和流程,知道是怎么运行的。
4. 代码
#include <iostream>
#include <assert.h>
using namespace std;
#define SUB_M 3
typedef struct _btree_node {
int *keys; //5
struct _btree_node** childrens; //6
int num;
int leaf;
}btree_node;
typedef struct _btree
{
struct _btree_node* root;
}btree;
//calloc和malloc的区别:calloc会自动清零
btree_node* btree_create_node(int leaf)
{
btree_node* node = (btree_node*)calloc(1, sizeof(btree_node));
if (node == NULL) return NULL;
node->leaf = leaf;
node->keys = (int*)calloc(2 * SUB_M - 1, sizeof(int));
node->childrens = (btree_node**)calloc(2 * SUB_M, sizeof(btree_node*));
node->num = 0;
return node;
}
void btree_destory_node(btree_node* node)
{
free(node->childrens);
free(node->keys);
free(node);
}
void btree_split_child(btree* T, btree_node* x, int idx) //T代表这棵树,x代表要分裂结点的父结点,i代表是这个父结点的第几棵子树(从0开始)
{
btree_node *y = x->childrens[idx]; //找到要分裂的子树
btree_node* z = btree_create_node(y->leaf);
//对z的操作
z->num = SUB_M - 1; //复制2个过去
int i = 0;
for (i = 0; i < SUB_M - 1; i++)
{
z->keys[i] = y->keys[SUB_M + i]; //复制y的结点3、4到z的结点1、2
}
//如果y不是叶子结点,需要把子树也copy过去(如果y是内结点)
if (y->leaf == 0)
{
//将y结点的子树:总共有2*SUB_M棵,假设SUB_M=3,复制3、4、5棵子树
for (i = 0; i < SUB_M; i++);//i=0,1,2
{ //z是新建的结点
z->childrens[i] = y->childrens[SUB_M + i]; //将y的子树3、4、5复制到新建子树z的0、1、2上
}
}
y->num = SUB_M - 1; //y->num = 3
//对x进行修改
//发现:第几棵子树提上来的结点就放在第几个位置
//x->num是key的个数:3 i>=4不满足条件,不用循环
//x->num = 3, idx=2,2+1=3(第2棵子树),4=3, i=2不满足>=3,跳出循环 y是第2棵,处理完后只有2个结点 y的下一个结点是3,赋值为z
for (i = x->num; i >= idx + 1; i--) //i = 5(新的空位),到第几棵子树+1。y是第几棵子树,就占据x的第几个位置。将占据的位置后面的后移
{ //5 = 4
//4孩子=3孩子 到
x->childrens[i+1] = x->childrens[i];//i=3
}
x->childrens[i + 1] = z;//爸爸的子树多一个新建的子树
//把值处理一下 i = x->num-1=3-1=2 [从CFI变成CFIL]; i>=3(第3个子树要分裂)
for (i = x->num - 1; i >= idx; i--)
{
//3 = 2
x->keys[i + 1] = x->keys[i];
}
//3(新的) = 新的
x->keys[i] = y->keys[SUB_M - 1];
x->num += 1;
}
void btree_insert(btree* T, int key)
{
btree_node* r = T->root;
if (r->num == 2 * SUB_M - 1) //key等于5个的时候
{
btree_node *node = btree_create_node(0); //0代表非叶子结点
node->childrens[0] = r; //新创建的结点的第一棵子树为原来的根节点
btree_split_child(T, node, 0);
}
}
//合并 x代表当前的结点:O结点 哪两棵子树需要合并?idx=0、1
void btree_merge(btree* T, btree_node* x, int idx)
{
btree_node *left = x->childrens[idx];
btree_node *right = x->childrens[idx + 1];
//如何合并?把左合到右还是右合到左边?都可以 反正有一个会被消灭掉 后面放到前面会更好一点,因为不用移位,直接往前面的后面放即可
//对于x = 结点CFI left=AB(CFI.childrens[0]) right=DE(CFI.childrens[1])
//num=2 0是A,1是B,2是空的 idx=0
left->keys[left->num] = x->keys[idx]; //意思是把C拷贝到AB后面,结点AB变为ABC
int i = 0;
//这里意味着把结点ABC变成结点ABCDE,right->num=2,i=0、1,把right的0、1位置:分别为D、E拷贝到ABC结点上,从ABC结点上的第3个位置开始,到第4个位置结束
for (i = 0; i < right->num; i++)
{
left->keys[SUB_M + i] = right->keys[i];
}
//判断是否是叶子结点 如果不是叶子结点:有子树,需要把子树全部copy过来
if (!left->leaf)
{
//i=0、1、2
for (i = 0; i < SUB_M; i++)
{ //2个keys有3叉子树 left->childrens从3开始,、4、5,copyright的0、1、2子树
left->childrens[SUB_M + i] = right->childrens[i];
}
}
//left->num即为left结点:多了3(CDE)一个来自于父亲结点,两个来自于右边结点
left->num += SUB_M;
//消灭right结点
btree_destory_node(right);
//需要更改x结点对于结点CFI,C移下去了,FI要前移。分为两步:1. 把key前移,2.把num前移
//从idx的后一位开始移动 x->num=3i从1开始,取1,2
for (i = idx + 1; i < x->num; i++)
{ //x->keys[0] = x->keys[1], x->keys[1] = x->keys[2]
x->keys[i - 1] = x->keys[i]; //将后一位移到前一位,即:CFI变为FI,F是不是没有消掉呀?
//x->children[1] = x->children[2], x->childrens[2] = x->childrens[3]
x->childrens[i] = x->childrens[i + 1]; //同时x的第2、3棵子树也要移到1、2棵子树的位置[注意AB、C、DE、F的位置关系]
}
}