基本概念

  • 父亲(parent node):对于除根以外的每个结点,定义为从该结点到根路径上的第二个结点。 根结点没有父结点。
  • 祖先(ancestor):一个结点到根结点的路径上,除了它本身外的结点。 根结点的祖先集合为空。
  • 子结点(child node):如果\(u\)\(v\)的父亲,那么\(v\)\(u\)的子结点。 子结点的顺序一般不加以区分,二叉树是一个例外。
  • 结点的深度(depth):到根结点的路径上的边数。
  • 树的高度(height):所有结点的深度的最大值。
  • 兄弟(sibling):同一个父亲的多个子结点互为兄弟。
  • 后代(descendant):子结点和子结点的后代。 或者理解成:如果\(u\)\(v\)的祖先,那么\(v\)\(u\)的后代。
  • 子树(subtree):删掉与父亲相连的边后,该结点所在的子图。
  • Depth of a node N :从N节点到到根的路径长度。
  • Height of node N :从N出发,到叶子的最长路径的长度。
  • Depth of tree:最深节点的深度。
  • Height of tree:根的高度。

性质

  • 树中的两个节点之间至多有一条路径。
  • 树没有环。

一般实现

使用指针:每个节点有三个元素:一个本身元素值,一个指针:指向第一个儿子,另一个指针:指向它的兄弟。

//Node declarations for trees
Struct TreeNode{
Object element;
TreeNode *firstChild;
TreeNode *nextSibling;
};

二叉树

树中的每个节点最多只有两个儿子的树。

  • 满二叉树:树中的每一个节点:要么有两个儿子,要么是叶子(没有儿子)。
  • 完全二叉树:深度为\(d\)的树中,除了\(d-1\)层外,其余层的节点数都是满的。
  • 完美平衡:除了右下角部分外,整棵树都是满的。

给定 N 个节点,二叉树的最小深度是多少?

在深度为d处,有\(N=2^d\)\(N=2^{d+1}-1\)个节点。取对数可以得到: \[ 2^d \leq N \leq 2^{d+1}-1 \]

\[ log_2N-1 < d \leq log_2N \]

(对应的”\(N=2^d\)\(N=2^{d+1}-1\)“个节点推导)。

最大深度?

退化为链,深度是\(N-1\),为一个链表。

遍历二叉树

利用递归定义遍历:根->左子树->右子树。

三种遍历方法:前序遍历,中序遍历,后序遍历。

前序遍历

tree-basic-preorder

按照 根,左,右 的顺序遍历二叉树。

void preorder(BiTree* root) {
if (root) {
cout << root->key << " ";
preorder(root->left);
preorder(root->right);
}
}

中序遍历

tree-basic-inorder

按照 左,根,右 的顺序遍历二叉树。

void inorder(BiTree* root) {
if (root) {
inorder(root->left);
cout << root->key << " ";
inorder(root->right);
}
}

后序遍历

tree-basic-postorder

按照 左,右,根 的顺序遍历二叉树。

void postorder(BiTree* root) {
if (root) {
postorder(root->left);
postorder(root->right);
cout << root->key << " ";
}
}

反推

根据中序遍历和前/后序遍历,可以构造出二叉树(或者得到第三个序列)。

tree-basic-reverse
  1. 前序的第一个是 root,后序的最后一个是 root
  2. 先确定根节点,然后根据中序遍历,在根左边的为左子树,根右边的为右子树。
  3. 对于每一个子树可以看成一个全新的树,仍然遵循上面的规律。

二叉搜索树

特征

对于树上的每一个节点,都有:

  • 左子树中的所有值都比小于该节点。
  • 右子树中的所有值都大于该节点。

操作

  • 查找
  • 查找最小值
  • 查找最大值
  • 插入
  • 删除

查找

使用递归进行查找:

/**
* test if an item is in a subtree.
* x is item to search for.
* t is the node that roots the subtree.
*/
bool contains( const Comparable & x, BinaryNode *t ) const{
if( t == nullptr ) return false; //找不到
else if( x < t->element ) return contains( x, t->left ); //查找值小于当前节点,往左子树找
else if( t->element < x ) return contains( x, t->right ); //查找值大于当前节点,往右子树找
else return true; // Match 找到
}

查找最小值

使用递归进行查找:

/**
* find the smallest item in a subtree t.
* Return node containing the smallest item.
**/
BinaryNode * findMin( BinaryNode *t ) const{
if( t == nullptr ) return nullptr; // root is empty 加这一行判断主要是t可能不是整棵树的根,而是某颗子树的根
//比方说:只有一个节点的树:如果我们是从根的做儿子开始找,那么就需要这一行代码进行判断
if( t->left == nullptr ) return t; //left child is empty
return findMin( t->left );
}

插入

讨论情况:

  • 如果找到,不执行任何操作。
  • 如果找不到,插入新节点。

使用递归进行插入:

/**
* Internal method to insert into a subtree.
* x is the item to insert.
* t is the node that roots the subtree.
* Set the new root of the subtree.
*/
void insert( const Comparable & x, BinaryNode * & t ){ //注意这里t的传递方式是传递引用,不然这棵树就断了
if( t == nullptr )
t = new BinaryNode{ x, nullptr, nullptr }; //不存在,插入新节点
else if( x < t->element )
insert( x, t->left ); //插入值比当前节点小,往左子树插入
else if( t->element < x )
insert( x, t->right ); //插入值比当前节点大,往右子树插入
else
; // Duplicate; do nothing 已经存在这个节点,不执行操作
}

二叉搜索树的形状取决于元素插入的顺序。

删除

讨论情况:

  • 如果该节点没有儿子,直接删除即可

  • 如果有一个儿子,则由替换为儿子。

  • 如果有两个儿子,则由右子树中的最小元素进行替换。

/**
* remove from a subtree. Textbook page 140
* x is the item to remove. t is the node that roots the subtree.
* Set the new root of the subtree.
*/
void remove( const Comparable & x, BinaryNode * & t ){
if( t == nullptr ) return; // Item not found; do nothing
if( x < t->element ) remove( x, t->left );
else if( t->element < x ) remove( x, t->right );
else if( t->left != nullptr && t->right != nullptr ){// Two children
t->element = findMin( t->right )->element;
remove( t->element, t->right );
}
else{
BinaryNode *oldNode = t;
t = ( t->left != nullptr ) ? t->left : t->right;
delete oldNode;
}
}

我们希望BST尽可能保持平衡。

二叉搜索树的所有操作评价情况下都是\(O(d),d \ is \ depth\)

  • Balanced BST in average case: Θ(log n)
  • Unbalanced BST in worst case: Θ(n)

为了让二叉搜索树尽可能保持平衡,引入了很多方法。

AVL树

AVL是高度平衡的二叉搜索树。

  • 平衡因子(Balance factor of a node):左子树的高度-右子树的高度。
  • AVL树中的每个节点都有平衡因子。
  • 对于每个节点,左子树和右子树的高度差不超过1。

计算平衡因子有一些注意事项:

  • 对于一个节点:如果它的子树是空的,那么对应子树的高度为-1。

高度推导

定义\(S(h)\)高度为 h 的 AVL 树中的最小节点数

对应的有:\(S(0)=1,S(1)=2\)。根据AVL树的定义,可以得到: \[ S(h) = S(h-1) + S(h-2) + 1 \] 转化为: \[ S(h)+1=S(h-1)+1+S(h-2)+1,F(h)=S(h)+1 \] 由斐波那契数列得到:'' \[ S(h) \geq k^{h},k=1.62 \]

又因为\(n \geq S(h)\),故可以计算得到\(log_kn \geq h\),即\(h \leq 1.44log_2h\),确实为\(O(logn)\)

旋转

旋转操作是为了解决插入后,某个节点的平衡因子大于1或者小于-1的情况,说白了,就是AVL树不再平衡。

分为两种操作:单旋转,双旋转(执行两次单旋转)。

对应的情况:假设当前节点为now(不平衡)

  • 左儿子的左子树插入:单旋转
  • 右儿子的右子树插入:单旋转
  • 左儿子的右子树插入:双旋转
  • 右儿子的左子树插入:双旋转

单旋转

左旋操作:右旋拎左右挂左。

初始:

拎左:左儿子作为根

右挂左:对应的左儿子给now作为左儿子

右旋操作:右旋拎右左挂右。这里不再演示。

也可以看看下面的PPT演示。

双旋转

对应的是后两种情况:执行两次单旋转。

这里通过PPT图片进行展示,比较好理解。

代码实现

我们需要关注每个节点的平衡因子,一旦执行插入操作,需要及时修改平衡因子。

//Node declaration for AVL trees
struct AvlNode{
Comparable element;
AvlNode *left;
AvlNode *right;
int height; //keep the height. You can keep balance factor
AvlNode(const Comparable & ele, AvlNode *lt, AvlNode *rt, int h = 0)
: element{ ele }, left{ lt }, right{ rt }, height{ h } { }
};

/**
* Return the height of node t or -1 if nullptr.
*/
int height( AvlNode *t ) const{
return t == nullptr ? -1 : t->height;
}
static const int IMBALANCE = 1;

注意:只有从插入点到根节点的路径上的节点的高度可能发生变化。因此在插入之后,逐个节点返回到根节点,更新高度。如果存在新的平衡因子为 2 或 – 2、通过围绕节点旋转来调整树。

/**
* Internal method to insert into a subtree.
* x is the item to insert.
* t is the node that roots the subtree.
* Set the new root of the subtree.
*/
void insert( const Comparable & x, AvlNode * & t ){
if( t == nullptr )
t = new AvlNode{ x, nullptr, nullptr };
else if( x < t->element )
insert( x, t->left );
else if( t->element < x )
insert( x, t->right );
balance( t );
}

// Assume t is balanced or within one of being balanced
void balance( AvlNode * & t ){
if( t == nullptr ) return;
if( height(t->left) - height(t->right) > IMBALANCE )
if( height( t->left->left ) >= height( t->left->right ) )
rotateWithLeftChild( t );
else
doubleWithLeftChild( t );
else
if( height( t->right ) - height( t->left ) > IMBALANCE )
if( height( t->right->right ) >= height( t->right->left ) )
rotateWithRightChild( t );
else
doubleWithRightChild( t );
t->height = max( height( t->left ), height( t->right ) ) + 1; //注意需要更新高度
}
/**
* Rotate binary tree node with left child.
* For AVL trees, this is a single rotation for case 1.
* Update heights, then set new root.
*/
void rotateWithLeftChild( AvlNode * & k2 ){ //注意传递的是引用
AvlNode *k1 = k2->left; //获取左儿子
k2->left = k1->right; //右挂左
k1->right = k2; //拎左
k2->height = max( height( k2->left ), height( k2->right ) ) + 1; //更新高度
k1->height = max( height( k1->left ), k2->height ) + 1; //更新高度
k2 = k1; //更换当前的节点,非常重要,这样才真正完成了“拎左”
}
/**
* Double rotate binary tree node: first left child
* with its right child; then node k3 with new left child.
* For AVL trees, this is a double rotation for case 3.
* Update heights, then set new root.
*/
void doubleWithLeftChild( AvlNode * & k3 ) {
rotateWithRightChild( k3->left );
rotateWithLeftChild( k3 );
}
void rotateWithRightChild( AvlNode * & k2 ){	//注意传递的是引用
AvlNode *k1 = k2->right; //获取右儿子
k2->right = k1->left; //左挂右
k1->left = k2; //拎右
k2->height = max( height( k2->left ), height( k2->right ) ) + 1; //更新高度
k1->height = max( height( k1->left ), k2->height ) + 1; //更新高度
k2 = k1; //更换当前的节点,非常重要,这样才真正完成了“拎右”
}
void doubleWithRightChild( AvlNode * & k3 ) {
rotateWithLeftChild( k3->right );
rotateWithRightChild( k3 );
}

Splay树

Splay树具有以下特征:

  • 并非始终保持完美平衡
  • 最近访问的数据位于根附近,满足“80-20”原则。
  • 当节点X被访问后,进行Splaying操作,将X带到树的根。

Splay 操作即对\(x\)做一系列的 splay 步骤。每次对做一次 splay 步骤,\(x\)到根节点的距离都会更近。定义\(x\)\(x\)的父节点。Splay 步骤有三种,具体分为六种情况:

  • zig:类似AVL的单次旋转,分为左旋和右旋。

右旋:

splay-rotate1

左旋:

splay-rotate2
  • 双旋转:又称为Splay。分为zig-zigzig-zag。(为了解决单旋转比较低效的问题)

对应的前置条件:设节点X为一个非根节点,且至少拥有两个祖先。记P为其父亲节点,G为祖父母节点。对应四种情况。

下面详细介绍这些操作的具体实现步骤。

单次旋转(XP(根)但没有 G):ZigFromLeft、ZigFromRight

双旋转(X 同时具有 PG):ZigZigFromLeft、ZigZigFromRight ZigZagFromLeft、ZigZagFromRight

这里解释一下这些名字的含义:

  • 单旋转:直接就是从左儿子/右儿子进行旋转。
  • 双旋转:这个命名就有些复杂了:这里记住LeftRight最近的动词是谁:如ZigZagFromLeft中距离Left最近的是Zag。那么就是对应上面的情况3。

​ 也就是说这个Left指的是没有修改前的根。

单旋转

双旋转

Zig-Zag

注意:执行顺序是自下而上的:也就是\(R->Q,R->P\)

Zig-Zig

注意:执行顺序是自下而上的:也就是\(Q->P,R->Q\)

操作

插入

正常插入X,如何执行Splay操作,展开到root

删除

  • X 展开到 root 并将其删除。 (注意:该节点不必像 BST 删除那样是叶节点或单个子节点。)剩下两棵树,右子树和左子树。

  • 将左子树中的最大值展开到根。

  • 将右子树附加到新根。

B树

在计算机科学中,B 树(B-tree)是一种自平衡的搜索树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B 树的每个节点可以拥有两个以上的子节点,因此 B 树是一种多路搜索树。

在 B 树中,有两种节点:

  • 内部节点(internal node):存储了引导叶子的变量,叫做\(key\)

  • 叶子节点(leaf node):与内部节点不同的是,叶子节点只存储数据,并没有子节点。

首先介绍一下一棵\(m\)阶的 B 树的特性。表示这个树的每一个节点最多可以拥有的子节点个数。一棵阶的 B 树满足的性质如下:

  1. 每个节点最多有个\(m\)子节点。
  2. 每一个非叶子节点(除根节点)最少有\(\lceil \frac{m}{2} \rceil\)个子节点。
  3. 如果根节点不是叶子节点,那么它至少有两个子节点。
  4. \(k\)个子节点的非叶子节点拥有\(k-1\)(最多是\(m-1\))个键,且升序排列\(k_i < k_{i+1}\),满足。
  5. 所有的叶子节点都在同一层(也就是深度相同)。
  6. 存储N个节点的B树,深度为\(O(log_{\lceil \frac{m}{2} \rceil}N)\)

属性

每个内部节点的子节点位于该节点中的“中间”。

假设\(T_i\)是该节点的第\(i\)个子节点:那么\(T_i\)中的所有\(keys\),都满足:\(k_{i-1} \leq T_i <k_i\)。且第一个子树\(T_1 <k_1\),最后一个子树\(T_m \geq k_{m-1}\)

注意\(K_i\)是键值,\(P_i\)是指向子树的指针。

拓展:B+树

如果叶节点连接为链表,则 B 树称为 B+ 树,可以轻松访问。

如果是B+树,这里的存储不同

这里的\(K_i\)还是键值(唯一的)。\(R_i\)是指针,指向这些键值的记录。\(Next\)是指向键值顺序中的下一个叶子节点。(即叶子节点是通过指针串联起来)

操作

搜索

Find(ElementType K, Btree T){
B = T;
while (B is not a leaf){
find the Pi in node B that points to the proper subtree that K will be in;
B = Pi;
}
/* Now we’re at a leaf */
if key K is the jth key in leaf B,
use the jth record pointer to find the associated record;
else /* K is not in leaf B */ report failure;
}

B 树中的节点包含有多个键。假设需要查找的是\(k\),那么从根节点开始,从上到下递归的遍历树。在每一层上,搜索的范围被减小到包含了搜索值的子树中。子树值的范围被它的父节点的键确定。因为是从根节点开始的二分法查找,所以查找一个键的代码如下:

BTreeNode *BTreeNode::search(int k) {
// 找到第一个大于等于待查找键 k 的键
int i = 0;
while (i < n && k > keys[i]) i++;

// 如果找到的第一个键等于 k , 返回节点指针
if (keys[i] == k) return this;

// 如果没有找到键 k 且当前节点为叶子节点则返回NULL
if (leaf == true) return NULL;

// 递归
return C[i]->search(k);
}

遍历

B 树的中序遍历与二叉搜索树的中序遍历也很相似,从最左边的孩子节点开始,递归地打印最左边的孩子节点,然后对剩余的孩子和键重复相同的过程。最后,递归打印最右边的孩子节点。

遍历的代码如下:

void BTreeNode::traverse() {
// 有 n 个键和 n+1 个孩子
// 遍历 n 个键和前 n 个孩子
int i;
for (i = 0; i < n; i++) {
// 如果当前节点不是叶子节点, 在打印 key[i] 之前,
// 先遍历以 C[i] 为根的子树.
if (leaf == false) C[i]->traverse();
cout << " " << keys[i];
}

// 打印以最后一个孩子为根的子树
if (leaf == false) C[i]->traverse();
}

先贴一份GPT的实现代码

#include <iostream>
using namespace std;

const int T = 3; // B树的最小度

// B树节点定义
class BTreeNode {
public:
int keys[2 * T - 1]; // 节点中的关键字
BTreeNode* children[2 * T]; // 子节点
int n; // 当前存储的关键字数
bool leaf; // 是否为叶子节点

BTreeNode(bool _leaf);

void traverse(); // 遍历节点

BTreeNode* search(int k); // 搜索关键字

int findKey(int k); // 查找关键字在节点中的索引

void insertNonFull(int k); // 在非满节点中插入

void splitChild(int i, BTreeNode* y); // 分裂子节点

void remove(int k); // 从子树中删除关键字

void removeFromLeaf(int idx); // 从叶子节点中删除

void removeFromNonLeaf(int idx); // 从非叶子节点中删除

int getPred(int idx); // 获取前驱关键字

int getSucc(int idx); // 获取后继关键字

void fill(int idx); // 填补缺少的子节点

void borrowFromPrev(int idx); // 从前一个子节点借一个关键字

void borrowFromNext(int idx); // 从后一个子节点借一个关键字

void merge(int idx); // 合并两个子节点

friend class BTree;
};

class BTree {
public:
BTreeNode* root; // 根节点

BTree() {
root = nullptr;
}

void traverse() {
if (root != nullptr)
root->traverse();
}

BTreeNode* search(int k) {
return (root == nullptr) ? nullptr : root->search(k);
}

void insert(int k);

void remove(int k);
};

BTreeNode::BTreeNode(bool _leaf) {
leaf = _leaf;
n = 0;
}

void BTreeNode::traverse() {
int i;
for (i = 0; i < n; i++) {
if (!leaf)
children[i]->traverse();
cout << " " << keys[i];
}

if (!leaf)
children[i]->traverse();
}

BTreeNode* BTreeNode::search(int k) {
int i = 0;
while (i < n && k > keys[i])
i++;

if (keys[i] == k)
return this;

if (leaf)
return nullptr;

return children[i]->search(k);
}

int BTreeNode::findKey(int k) {
int idx = 0;
while (idx < n && keys[idx] < k)
idx++;
return idx;
}

void BTreeNode::insertNonFull(int k) {
int i = n - 1;

if (leaf) {
while (i >= 0 && keys[i] > k) {
keys[i + 1] = keys[i];
i--;
}
keys[i + 1] = k;
n = n + 1;
} else {
while (i >= 0 && keys[i] > k)
i--;

if (children[i + 1]->n == 2 * T - 1) {
splitChild(i + 1, children[i + 1]);

if (keys[i + 1] < k)
i++;
}
children[i + 1]->insertNonFull(k);
}
}

void BTreeNode::splitChild(int i, BTreeNode* y) {
BTreeNode* z = new BTreeNode(y->leaf);
z->n = T - 1;

for (int j = 0; j < T - 1; j++)
z->keys[j] = y->keys[j + T];

if (!y->leaf) {
for (int j = 0; j < T; j++)
z->children[j] = y->children[j + T];
}

y->n = T - 1;

for (int j = n; j >= i + 1; j--)
children[j + 1] = children[j];

children[i + 1] = z;

for (int j = n - 1; j >= i; j--)
keys[j + 1] = keys[j];

keys[i] = y->keys[T - 1];

n = n + 1;
}

void BTree::insert(int k) {
if (root == nullptr) {
root = new BTreeNode(true);
root->keys[0] = k;
root->n = 1;
} else {
if (root->n == 2 * T - 1) {
BTreeNode* s = new BTreeNode(false);
s->children[0] = root;
s->splitChild(0, root);

int i = 0;
if (s->keys[0] < k)
i++;
s->children[i]->insertNonFull(k);
root = s;
} else
root->insertNonFull(k);
}
}

void BTree::remove(int k) {
if (!root) {
cout << "树为空\n";
return;
}

root->remove(k);

if (root->n == 0) {
BTreeNode* tmp = root;
if (root->leaf)
root = nullptr;
else
root = root->children[0];
delete tmp;
}
}

void BTreeNode::remove(int k) {
int idx = findKey(k);

if (idx < n && keys[idx] == k) {
if (leaf)
removeFromLeaf(idx);
else
removeFromNonLeaf(idx);
} else {
if (leaf) {
cout << "关键字 " << k << " 不存在于树中\n";
return;
}

bool flag = (idx == n);

if (children[idx]->n < T)
fill(idx);

if (flag && idx > n)
children[idx - 1]->remove(k);
else
children[idx]->remove(k);
}
}

void BTreeNode::removeFromLeaf(int idx) {
for (int i = idx + 1; i < n; i++)
keys[i - 1] = keys[i];
n--;
}

void BTreeNode::removeFromNonLeaf(int idx) {
int k = keys[idx];

if (children[idx]->n >= T) {
int pred = getPred(idx);
keys[idx] = pred;
children[idx]->remove(pred);
} else if (children[idx + 1]->n >= T) {
int succ = getSucc(idx);
keys[idx] = succ;
children[idx + 1]->remove(succ);
} else {
merge(idx);
children[idx]->remove(k);
}
}

int BTreeNode::getPred(int idx) {
BTreeNode* cur = children[idx];
while (!cur->leaf)
cur = cur->children[cur->n];
return cur->keys[cur->n - 1];
}

int BTreeNode::getSucc(int idx) {
BTreeNode* cur = children[idx + 1];
while (!cur->leaf)
cur = cur->children[0];
return cur->keys[0];
}

void BTreeNode::fill(int idx) {
if (idx != 0 && children[idx - 1]->n >= T)
borrowFromPrev(idx);
else if (idx != n && children[idx + 1]->n >= T)
borrowFromNext(idx);
else {
if (idx != n)
merge(idx);
else
merge(idx - 1);
}
}

void BTreeNode::borrowFromPrev(int idx) {
BTreeNode* child = children[idx];
BTreeNode* sibling = children[idx - 1];

for (int i = child->n - 1; i >= 0; i--)
child->keys[i + 1] = child->keys[i];

if (!child->leaf) {
for (int i = child->n; i >= 0; i--)
child->children[i + 1] = child->children[i];
}

child->keys[0] = keys[idx - 1];

if (!child->leaf)
child->children[0] = sibling->children[sibling->n];

keys[idx - 1] = sibling->keys[sibling->n - 1];

child->n += 1;
sibling->n -= 1;
}

void BTreeNode::borrowFromNext(int idx) {
BTreeNode* child = children[idx];
BTreeNode* sibling = children[idx + 1];

child->keys[child->n] = keys[idx];

if (!child->leaf)
child->children[child->n + 1] = sibling->children[0];

keys[idx] = sibling->keys[0];

for (int i = 1; i < sibling->n; i++)
sibling->keys[i - 1] = sibling->keys[i];

if (!sibling->leaf) {
for (int i = 1; i <= sibling->n; i++)
sibling->children[i - 1] = sibling->children[i];
}

child->n += 1;
sibling->n -= 1;
}

void BTreeNode::merge(int idx) {
BTreeNode* child = children[idx];
BTreeNode* sibling = children[idx + 1];

child->keys[T - 1] = keys[idx];

for (int i = 0; i < sibling->n; i++)
child->keys[i + T] = sibling->keys[i];

if (!child->leaf) {
for (int i = 0; i <= sibling->n; i++)
child->children[i + T] = sibling->children[i];
}

for (int i = idx + 1; i < n; i++)
keys[i - 1] = keys[i];

for (int i = idx + 2; i <= n; i++)
children[i - 1] = children[i];

child->n += sibling->n + 1;
n--;

delete sibling;
}

int main() {
BTree t;

t.insert(10);
t.insert(20);
t.insert(5);
t.insert(6);
t.insert(12);
t.insert(30);
t.insert(7);
t.insert(17);

cout << "遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(6);
cout << "删除 6 后遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(13);
cout << "删除 13 后遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(7);
cout << "删除 7 后遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(4);
cout << "删除 4 后遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(2);
cout << "删除 2 后遍历 B 树:\n";
t.traverse();
cout << endl;

t.remove(16);
cout << "删除 16 后遍历 B 树:\n";
t.traverse();
cout << endl;

return 0;
}