1.平衡二叉树定义
平衡二叉树的定义由两部分组成:
- 首先它是二叉搜索树
- 其次任何一个节点的左右子树的高度差小于等于1
因此,平衡二叉树在数据的查询方面体现出了相当高的效率。
下面我将使用C语言实现平衡二叉树的构造、增加节点、删除节点等操作。
2.平衡二叉树节点结构
平衡二叉树的节点结构信息如下:
// 二叉平衡树节点结构体
struct Tree {
int value; // 当前节点的权值
int left_height; // 当前节点左子树的高度
int right_height; // 当前节点右子树的高度
struct Tree* left; // 左孩子根节点指针
struct Tree* right; // 右孩子根节点指针
};
3.构造节点
既然平衡二叉树节点结构已经明了,那么接下来应该要编写的就是一个生成节点的函数。要生成一个二叉树节点,我们只需要知道这个节点的权值,并且这个函数的返回值应该是生成的二叉树节点的指针。
据此编写出来的生成树节点函数如下:
// 生成二叉树节点,返回值为生成的节点指针
struct Tree* root_init(int value) {
// 参数value代表生成的树节点的权值
// 动态分配空间
struct Tree* root = (struct Tree*)malloc(sizeof(struct Tree));
if (root == NULL) {
printf("动态分配空间失败!");
exit(1);
}
// 初始化树节点信息
root->value = value;
root->left_height = 0;
root->right_height = 0;
root->left = NULL;
root->right = NULL;
return root;
}
4.查找节点
查找是平衡二叉树的一个重要功能,通常是查找平衡二叉树中权值为目标值的节点。
由平衡二叉树的性质可知,要查找一个权值为目标值的节点,我们只需要将目标权值与当前树的根节点权值进行比较。若相等,则该根节点就是我们要找的节点,此时我们可以直接返回该根节点的指针;若目标权值小于当前树根节点权值,则权值为目标值的节点若存在,必然存在于当前节点的左子树中,故我们可以继续对当前根节点的左子树进行查找操作;若目标权值大于当前树根节点权值,则同理,我们可以继续对当前根节点的右子树进行查找操作。
这种不断在新的树中进行查找的操作是不是很熟悉?没错,在这里我们可以使用递归解决这个问题!至于返回值,显然就是查找到的节点的指针了!调用这个函数的时候,我们首先判断平衡二叉树是否为空,然后对权值进行比较,并根据比较结果,或直接返回当前根节点指针,或继续向左或向右继续递归查找下去。并且若平衡二叉树中不存在要查找的节点,这个函数最终一定会递归到一棵空树,即返回的是一个空指针,我们便可以据此判断该节点是否存在于平衡二叉树中。
据此我们可以写出下面的代码:
// 在平衡二叉树中根据权值查询节点,返回值为查找到的节点的指针(若没查找到则返回空指针)
struct Tree* search(struct Tree* root, int value) {
// 参数root为平衡二叉树根节点指针,value为要查询的权值
// 若平衡二叉树为空,直接返回空指针
if (root == NULL) {
return NULL;
}
if (root->value == value) {
// 当前根节点权值等于目标权值,查找成功,返回匹配的节点的指针
return root;
}
else if (root->value > value) {
// 向左子树进行查找
return search(root->left, value);
}
else {
// 向右子树进行查找
return search(root->right, value);
}
}
5.增加节点
知道如何生成平衡二叉树的一个节点以及根据权值查找节点之后,我们就可以尝试将一个新的节点插入到一棵已有的平衡二叉树中了。
5.1 将新节点插入到二叉树合适的位置
增加节点这一操作其实和查找节点这一操作差不多,都是利用二叉搜索树的性质,通过不断地将新节点权值与根节点权值进行比较。若新节点权值较小,则其必定会添加到根节点的左子树;若新节点权值较大,则其必定会添加到根节点的右子树;若新节点权值与根节点权值相等,那说明添加节点重复,此时应给出提示,并且不再进行添加操作。
在这种不断更新重复的操作中,递归就又派上用场了。我们可以定义添加节点的函数函数名为add,参数则有两个,分别是要进行添加的平衡二叉树根节点指针root和被添加的新节点指针new_node。在add函数中,我们只需要将root和new_node指向的节点的权值进行比较,然后根据比较的结果或直接返回root,或向左、右子树进行递归即可。当然,也不能忘了添加完节点后马上更新根节点的左右子树高度(添加到哪边就更新哪边)。
需要注意的是,在进行比较之前,必须先对root进行判空,这也可以看作是递归的终止条件。因为新节点会被放到叶子节点的位置,所以当root为空时,表示找到放置新节点的位置了。下面便是实现把新节点放到二叉树上的代码(真就只是放上去,只保证满足二叉搜索树性质,还没有说到放上去后如何调整二叉树使其平衡,马上这个函数还会补充)
// 往平衡二叉树增加节点,返回值为新二叉树根节点指针
struct Tree* add(struct Tree* root, struct Tree* new_node) {
// 参数root为平衡二叉树根节点指针,new_node为要添加的节点指针
// 平衡二叉树为空,直接返回添加节点的指针
if (root == NULL) {
return new_node;
}
if (new_node->value < root->value) {
// 将新节点添加到左子树
root->left = add(root->left, new_node);
// 更新左子树高度
root->left_height = get_height(root->left);
}
else if (new_node->value > root->value) {
// 将新节点添加到右子树
root->right = add(root->right, new_node);
// 更新右子树高度
root->right_height = get_height(root->right);
}
else {
// 找到重复节点
printf("不能添加重复节点!\n");
}
// 返回添加了新节点后的二叉树根节点指针
return root;
}
其中,获取二叉树高度的函数get_height如下:
#define max(a, b) (a > b ? a : b)
// 获取平衡二叉树高度,返回值即为获取到的高度值
int get_height(struct Tree* root) {
// 参数root为求高度的平衡二叉树根节点指针
// 判空处理
if (root == NULL) {
return 0;
}
// 取root节点左右子树高度最大值加一
else {
return max(root->left_height, root->right_height) + 1;
}
}
可以看到add函数是有返回值的,并且返回值代表的是增加节点后的平衡二叉树根节点指针(单看现在返回的不一定是平衡二叉树,后面代码完善后就一定是了)。那为什么会设置这么一个返回值呢?
我们可以看一下这个递归函数的终止条件,需要到传入的根节点指针为空。如此一来,如果不设置返回值,我们就不能在最后一层递归中将新节点指针赋值给某个叶子节点的孩子指针变量。因为传入的root存储的是某个叶子节点的某个孩子指针,即NULL,并且是形参变量,所以令root=new_node是没有意义的。为此我们还得在每次进入这个add函数前判断传入的root是否为空(包括主函数中),这样一来问题就来了——这个代码不够优雅啊!咳咳,至少我是这么认为的。而将加入新节点后的平衡二叉树根节点指针返回的话,我们就可以在上一层函数递归中,将接收到的新节点指针(或者添加了节点的平衡二叉树根节点指针)返回值赋值给合适的孩子指针变量。
后面的函数大多也是设置了类似的返回值,原因也大多和上面一样。
5.2 调整二叉树结构使其重新成为平衡二叉树
言归正传,现在新节点已经加入到原平衡二叉树中了,并且已知放置的位置是满足得到的二叉树是二叉搜索树的。接下来我们要考虑的就是,这棵二叉树是否还是一棵平衡二叉树。
答案是不一定,原因我们可以看图1。
图1
new_node指向的是新节点,我们可以看到原本这是一棵平衡二叉树,执行完上面的代码后新节点必定会添加到图示位置,显然这棵新的二叉树并不是平衡二叉树。因此,我们需要在上面的代码基础上补充调整二叉树结构的代码。
在这里,我们把这个负责调整二叉搜索树结构使其成为平衡二叉树的函数取名为adjust,并且这个函数包含一个参数root,代表需要调整的二叉搜索树的根节点指针。至于函数的返回值,同上面类似,是调整后得到的平衡二叉树的根节点指针。
5.2.1 调整时机和条件
接下来需要思考的问题,就是这个函数需要在什么时候被调用。我们看回图1,添加了新节点的平衡二叉树可能会在什么时候变得不再平衡的呢?显然是在调用完add函数之后。因此我们只需要在每次调用完add函数后,检查得到的二叉树,然后即可决定是否对其进行调整。
那么我们该如何检查呢?很简单,如果新节点被添加到左子树,那么该次调用完add函数后,我们就会更新左子树的高度。对于添加节点这个操作,左子树高度只可能增或不变,不可能减,而右子树高度不变。因此,如果添加完新节点后,若二叉树不再是平衡二叉树,那必定是因为左子树高度比右子树高度大了2。若新节点被添加到右子树也同理。
所以我们可以完善add函数代码如下:
// 往平衡二叉树增加节点,返回值为新平衡二叉树根节点指针
struct Tree* add(struct Tree* root, struct Tree* new_node) {
// 参数root为平衡二叉树根节点指针,new_node为要添加的节点指针
// 平衡二叉树为空,直接返回添加节点的指针
if (root == NULL) {
return new_node;
}
if (new_node->value < root->value) {
// 将新节点添加到左子树
root->left = add(root->left, new_node);
// 更新左子树高度
root->left_height = get_height(root->left);
if (root->left_height - root->right_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else if (new_node->value > root->value) {
// 将新节点添加到右子树
root->right = add(root->right, new_node);
// 更新右子树高度
root->right_height = get_height(root->right);
if (root->right_height - root->left_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else {
// 找到重复节点
printf("不能添加重复节点!\n");
}
// 返回添加了新节点后的平衡二叉树根节点指针
return root;
}
至此,add函数才算是编写完成,下一步我们就要开始编写adjust函数了。
5.2.2 调整分类
在adjust函数里,我们也需要对接收到的二叉树进行分类,对不同的类别采用不同的调整方法。进行分类的依据,我们首先应该想到的是左右子树的高度,左子树高度更高的分为一类,右子树高度更高的则分为另一类。
但是,仅仅是这个程度的分类还不够,我们还需要进行更加细致的分类。对于左子树高度更高的那一类,我们假设这棵需要调整的二叉树右子树高度为n(n0,以下所有图示同),那么左子树的高度就是n+2。所以我们得知左子树的高度至少为2,即左子树一定不会是一棵空树,且左子树根节点的左右子树之中至少会有一棵高度为n+1。
进一步假设左子树根节点的左子树高度为n+1,那能不能就此确定左子树根节点的右子树高度呢?这是可以的。图2是一棵刚添加了新节点的二叉树的图,规定圈着小写字母的圆圈仅代表一个节点,圆圈旁标着的表达式则代表以这个节点为根节点的二叉树的高度;圈着大写字母的圆圈则代表一棵二叉树(高度大于等于0,即可以是没有任何节点的空树),圆圈旁标着的表达式则代表这棵二叉树的高度。这种画图方式下面也会沿用,届时将不再重复说明。
图2
在图2中,新节点被添加到了E上,这个操作导致了以b为根节点的二叉树的高度变成了n+2,从而导致以a为根节点的二叉树不再平衡。因此,F的高度不可能是n+1或者更高,这是因为若F的高度是n+1或者更高,那么在添加新节点之前,以b为根节点的二叉树高度就已经是不小于n+2了,这意味着以a为根节点的二叉树在添加新节点之前不是平衡二叉树,显然与我们是把新节点添加到一棵平衡二叉树上的前提矛盾。
同时,F的高度也不可能小于n。在证明这个结论之前,我们还需要知道一个前提结论:对于这棵进行调整的二叉树,除了它本身之外,其所有的子树都是平衡二叉树。以图2为例,以b为根节点的二叉树是平衡二叉树,以及D、E、F都是平衡二叉树。
为什么这样说?我们看回代码,add函数是一个递归函数,而adjust函数是在add函数里面被调用的,并且是在add函数调用自身之后。再想想递归函数有什么特点?有去有回,而且是原路返回!所以我们添加节点的时候是从根节点一层层往下一直找到叶子节点,而检查二叉树是否平衡以及进行调整就是从叶子节点一层层往上进行检查了。因此对于需要进行调整的二叉树,它的所有子树(不包括自身)肯定都已经是平衡二叉树了,我们把这棵二叉树成为最小不平衡二叉树。
知道了这个前提结论后,我们不妨假设F的高度小于n,这样的话E与F的高度差将会大于等于2,这意味着以b为根节点的二叉树就已经不是一棵平衡二叉树了,显然与前提结论矛盾。综上,F的高度只能是n。而这里若是把E和F对调也是一样的,也就是说最小不平衡二叉树左子树比右子树高的情况下,其左子树的左右子树高度一定不一样,且一定相差1。
不过需要注意,这种结论只在增加节点中成立,后面要说到的删除节点中这个结论就不再成立了,具体细节我们会在删除节点中讲到。
所以,根据上述情况,我们可以制定进一步的分类准则:左子树比右子树高的大分类下,左子树的左子树更高的再细分为一类,我们称之为左左型;左子树的右子树更高的则细分为另一类,我们称之为左右型。
类似的,右子树比左子树高的大分类下,右子树的左子树更高的再细分为一类,我们称之为右左型;右子树的右子树更高的则细分为另一类,我们称之为右右型。
针对上面的四种小分类,我们分别定义四个函数进行处理:left_left、left_right、right_left、right_right。它们传入的参数都是要进行调整的二叉树的根节点指针root,返回值则都是调整完成后得到的平衡二叉树根节点的指针。
5.2.3 左左型
好了,铺垫了这么多,现在终于要开始讲如何调整了。我们从左左型开始,先配上要调整的二叉树示例图:
图3
我们可以将图3这棵二叉树调整成为图4这样的平衡二叉树(注意,上面讲到过,添加完节点调整之前,这棵二叉树已经是一棵二叉搜索树了,只是不一定是平衡二叉树,所以我们可以借助二叉搜索树的性质进行调整):
图4
对于图4这棵调整后的二叉树, 首先E和以a为根节点的二叉树高度差不大于1,F和D的高度差也不大于1,所以这棵二叉树的子树高度差满足平衡二叉树条件。再看权值,由调整前的结构可知E上所有节点的权值都小于b节点的权值,a节点的权值大于b节点的权值,F上所有节点的权值都大于b节点的权值的同时还都小于a节点的权值,D上所有节点的权值都大于a节点的权值。因此,调整后的这棵二叉树也是一棵二叉搜索树。综上,图4这棵二叉树是一棵平衡二叉树,这种调整方式是正确的。
我们用图5展示左左型调整的具体过程(为方便查看,我们用单线箭头表示节点之间的父子关系,由父节点指向孩子节点或子树,并且不标示高度):
图5
最后,我们还要修改a节点的左子树高度以及b节点的右子树高度,通过图3和图4的比较,我们只需要将a节点的左子树高度自减2,b节点的右子树高度自增1即可。
最后left_left函数代码如下:
// 左左型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* left_left(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点左孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->left;
root->left = tmp->right;
tmp->right = root;
// 更新子树高度
(root->left_height) -= 2;
(tmp->right_height) += 1;
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
5.2.4 左右型
接下来我们看左右型。还是先配上要调整的二叉树示例图:
图6
注意,图6中n的取值范围仍然是大于等于0,但我们在此特别规定若n=0,H的高度看作0而不是-1,也就是当n=0或n=1时,我们都认为H是空树,即f节点的右孩子指针为NULL。参照上面左左型的推理过程,当n>0时,G和H两棵二叉树的高度必定有一个为n,另一个为n-1。我们可以将图6这棵二叉树调整成为图7这样的平衡二叉树:
图7
对于图7是否是平衡二叉树的证明过程和上面左左型的类似,这里不再赘述。不过需要注意的是,即使图6中G和H的高度对调,经过调整成为图7后,其仍然是平衡二叉树,因此对于左右型的调整,所有的情况均已被考虑。
我们用图8展示左右型调整的具体过程(为方便查看,我们用单线箭头表示节点之间的父子关系,由父节点指向孩子节点或子树,并且不标示高度):
图8
最后,我们还要修改a节点的左子树高度、b节点的右子树高度以及f节点的左右子树高度。注意这里不能再使用左左型的方法直接比较图6和图7直接对原高度进行加减操作,这是因为在这里G和H的高度可以互换,并且当n=0时n-1和n都看作0。所以这次我们需要自下向上更新需要的节点的左右子树高度,即先更新b节点和a节点,最后更新f节点。
最后left_right函数代码如下:
// 左右型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* left_right(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点的左孩子节点的右孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->left->right;
root->left->right = tmp->left;
tmp->left = root->left;
root->left = tmp->right;
tmp->right = root;
// 更新子树高度
tmp->left->right_height = get_height(tmp->left->right);
root->left_height = get_height(root->left);
tmp->left_height = get_height(tmp->left);
tmp->right_height = get_height(tmp->right);
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
至此,根节点左子树高度比右子树高度大2的左左型和左右型已经讨论完毕,至于接下来的右左型和右右型,其调整方法和上面的左左型和左右型是很类似的,其中右左型和左右型对称,右右型和左左型对称,故只需要把代码里的左右对换即可。因此对于右左型和右右型的调整过程这里不再详细说明,而是直接给出代码。
5.2.5 右左型
right_left函数代码如下:
// 右左型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* right_left(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点的右孩子节点的左孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->right->left;
root->right->left = tmp->right;
tmp->right = root->right;
root->right = tmp->left;
tmp->left = root;
// 更新子树高度
tmp->right->left_height = get_height(tmp->right->left);
root->right_height = get_height(root->right);
tmp->left_height = get_height(tmp->left);
tmp->right_height = get_height(tmp->right);
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
5.2.6 右右型
right_right函数代码如下:
// 右右型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* right_right(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点右孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->right;
root->right = tmp->left;
tmp->left = root;
// 更新子树高度
(root->right_height) -= 2;
(tmp->left_height) += 1;
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
5.2.7 总调整函数
四种类型的代码都写好了,但adjust函数传入的仅仅是需要调整的二叉树的根节点指针,因此判断这棵需要调整的二叉树类别的功能我们可以在adjust函数中实现,代码如下:
// 调整还不是平衡二叉树的二叉树一些节点的位置,使其成为平衡二叉树,返回值为调整后得到的平衡二叉树根节点的指针
struct Tree* adjust(struct Tree* root) {
// 参数root为待调整的二叉树的根节点指针
// 判空处理
if (root == NULL) {
return NULL;
}
if (root->left_height - root->right_height == 2) {
// 进一步判断使用左左型还是左右型调整方法
if (root->left->left_height > root->left->right_height) {
// 进行左左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_left(root);
}
else {
// 进行左右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_right(root);
}
}
else {
// 进一步判断使用右左型还是右右型调整方法
if (root->right->left_height > root->right->right_height) {
// 进行右左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_left(root);
}
else {
// 进行右右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_right(root);
}
}
}
5.3 补充
前面我们说过,每次进行调整的二叉树必定是最小不平衡二叉树,即对所有左右子树高度差大于1的节点调用adjust函数,一定是自下向上地从叶子节点开始的。那问题来了,若插入新节点后需要对二叉树结构进行调整,我们能不能知道adjust函数调用的次数?
答案是可以的。其实若adjust函数被调用,那么它只会被调用一次,也就是说在把最开始的最小不平衡二叉树调整为平衡二叉树后,整棵二叉树就已经是平衡二叉树了。
我们以左左型为例,首先看图3,从图3我们可以得知在插入新节点之前,以a为根节点的二叉树高度是n+2,而在插入新节点之前,整棵二叉树是平衡二叉树,也就是说,只要放在这个位置上的二叉树是平衡二叉树,且高度为n+2,那整棵二叉树就一定是平衡二叉树。再看图4,调整之后得到的平衡二叉树的高度也是n+2!因此把调整后的以b节点为根节点的平衡二叉树代替插入新节点前二叉树的位置,整棵二叉树就必定会是平衡二叉树,而不用再进行其他的调整了。
其他几个类型也是类似的,读者可以把二叉树画出来,比较一下,就能得到一样的结论,这里就不再一一赘述了。
6.删除节点
讲完了如何增加节点,接着我们来看删除节点。我们把删除节点的函数命名为delete_node,传入的参数为进行删除操作的平衡二叉树根节点指针root以及目标删除节点的权值value,返回值则是删除结束后的平衡二叉树根节点指针。删除节点的操作同样比较复杂,在正式讲解如何删除之前,我们先讲解一下前提的理论知识。
6.1 左右子树高度差为2的最小不平衡二叉树调整
在增加节点这部分,我们把可能出现的最小不平衡二叉树结构分为四类:左左型、左右型、右左型和右右型。这四种类型都是左右子树高度差为2的,但它们并不能涵盖左右子树高度差为2的全部情况,因为我们已经知道,在这四种类型中,左右子树高度更高的那一边,其两棵子树的高度差只能是1。所以,我们接下来将会讨论当左右子树高度差为2,且高度更高的那一边的两棵子树高度相等时,如何对二叉树进行调整使其成为平衡二叉树。
我们这里讨论的都是最小不平衡二叉树,删除函数我们也将会使用递归的方式实现,故在此我们也是以最小不平衡二叉树举例,如图9:
图9
需要注意(除特别说明,下同),此处G、H、J、M下面的n/n-1表示这棵子树的高度可以是n或n-1,当n为0时只能是n,且G和H两者之间至少一棵高度是n,J和M两者之间也至少一棵高度是n,如此所有可能出现的情况就都被包括在了图9中。
我们尝试使用left_left函数进行调整,结果将会如图10所示:
图10
图10中的二叉树显然是一棵平衡二叉树,而若使用left_right函数进行调整,结果则将会如图11所示:
图11
因为J的高度可以是n或n-1,而若J的高度是n-1,则以b节点为根节点的二叉树显然不是一棵平衡二叉树,所以当左子树高度比右子树高度大2,且左子树根节点的两棵子树高度相等时,我们可以使用left_left函数进行调整。
右子树高度比左子树高度大2,且右子树根节点的两棵子树高度相等的情况与上述类似,我们这里不再赘述,读者可以自行论证。这里直接给出结论,即可以使用right_right函数进行调整。
据此,我们只需在之前的基础上对adjust函数进行一些修改,就可以将所有左子树与右子树高度差为2的最小不平衡二叉树调整为平衡二叉树,如下:
// 调整还不是平衡二叉树的二叉树一些节点的位置,使其成为平衡二叉树,返回值为调整后得到的平衡二叉树根节点的指针
struct Tree* adjust(struct Tree* root) {
// 参数root为待调整的二叉树的根节点指针
// 判空处理
if (root == NULL) {
return NULL;
}
if (root->left_height - root->right_height == 2) {
// 进一步判断使用左左型还是左右型调整方法
if (root->left->left_height < root->left->right_height) {
// 进行左右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_right(root);
}
else {
// 进行左左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_left(root);
}
}
else {
// 进一步判断使用右左型还是右右型调整方法
if (root->right->left_height > root->right->right_height) {
// 进行右左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_left(root);
}
else {
// 进行右右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_right(root);
}
}
}
又因为我们删除一个节点后,该节点所在的子树高度只会不变或者减一,所以删除一个节点后,任何节点左右子树的高度差不可能大于2,故我们在删除节点函数中对二叉树结构的调整可以复用修改后的adjust函数。
6.2 删除节点函数基本框架
下面是delete_node函数的基本框架
// 删除节点,返回值为删除完毕后得到的平衡二叉树根节点的指针
struct Tree* delete_node(struct Tree* root, int value) {
// 参数root为要删除节点的平衡二叉树根节点指针,value为目标删除节点的权值
// 判空处理
if (root == NULL) {
printf("没有这个节点!\n");
return NULL;
}
if (value < root->value) {
// 目标删除节点在当前二叉树的左子树
root->left = delete_node(root->left, value);
// 更新左子树高度
root->left_height = get_height(root->left);
if (root->right_height - root->left_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else if (value > root->value) {
// 目标删除节点在当前二叉树的右子树
root->right = delete_node(root->right, value);
// 更新右子树高度
root->right_height = get_height(root->right);
if (root->left_height - root->right_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else {
// 删除的节点为叶子节点
if (root->left == NULL && root->right == NULL) {
// 此处编写删除叶子节点的代码
}
// 删除的节点仅有右子树
else if (root->left == NULL) {
// 此处编写删除仅有右子树的节点的代码
}
// 删除的节点仅有左子树
else if (root->right == NULL) {
// 此处编写删除仅有左子树的节点的代码
}
// 删除的节点即有左子树也有右子树
else {
// 此处编写删除即有左子树又有右子树的节点的代码
}
}
// 返回删除目标节点后的平衡二叉树根节点指针
return root;
}
6.3 删除叶子节点
我们假设图9是删除了叶子节点之后得出的一棵最小不平衡二叉树,并令删除之前D子树的高度为n+1,删除后D子树高度变为n,我们以a节点指针作为参数调用adjust函数即可解决问题。并且从这个例子我们可以看到,删除一个节点后,左右子树中高度更高的子树,其左右子树的高度也可以是一致的,这就解释了前面我们在增加节点中提到的“左右子树高度差为2时,更高的那棵子树的左右子树高度差也必定为1只能适用于增加节点,而不适用于删除节点”。
因此,我们在上述删除函数框架中删除叶子节点部分需要完善的功能就是释放该节点的空间,然后返回NULL指针,这个被返回的NULL指针将会被赋给原平衡二叉树存储这个叶子节点指针值的指针变量,表示这个叶子节点被删除。至于可能需要进行的结构调整,并不会在删除叶子节点这一层函数递归调用里面实现,且在上述删除函数框架中关于调整结构这一部分的代码已经是完善的了,其实现原理和add函数类似。
我们完善删除叶子节点部分的代码如下:
// 删除的节点为叶子节点
if (root->left == NULL && root->right == NULL) {
// 释放被删除节点的空间
free(root);
root = NULL;
}
6.4 删除仅有一棵子树的节点
如果被删除的节点仅有一棵子树,那么这棵仅有的子树高度只能为1。这是因为删除节点这个操作是在平衡二叉树上进行的,若目标删除节点仅有的子树高度大于1,那么一开始目标删除节点的左右子树高度差就大于1了,显然是矛盾的。
这里我们以下面的图12为例:
图12
假设我们要删除d节点,那么我们要做的就是把a节点的右孩子节点换成e节点,并且释放原本d节点的空间,最后调整二叉树结构使其成为平衡二叉树即可。
我们完善删除仅有一棵子树的节点部分的代码如下(整个delete_node函数最后会return root,所以这里令root指向那棵仅有的子树,并且释放删除节点的空间就行):
// 删除的节点仅有右子树
else if (root->left == NULL) {
struct Tree* tmp = root;
root = root->right;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
// 删除的节点仅有左子树
else if (root->right == NULL) {
struct Tree* tmp = root;
root = root->left;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
6.5 删除同时拥有两棵子树的节点
我们以图13为例:
图13
假设我们要删除a节点,那么我们要做的就是把B子树中权值最大的节点,或者D子树中权值最小的节点,移动到a节点的位置顶替a节点,然后释放a节点的空间,最后调整二叉树结构使其成为平衡二叉树,这样我们就可以在不破坏二叉树平衡性质的前提下删除目标节点。
我们把这些步骤一一细分开进行说明。首先我们要决定是移动左子树中权值最大的节点,还是右子树中权值最小的节点。因为我们要维持二叉树的平衡性质,所以很自然地我们就可以想到,若一棵子树的高度大于等于它的兄弟子树,那我们从这棵子树中移走一个节点,并不会导致两棵子树的高度差大于1(这里需要注意,我们从一棵平衡二叉树中删除一个节点,并使用adjust函数进行结构调整后,得到的新的平衡二叉树的高度最多在原平衡二叉树高度基础上减一,详情会在稍后的补充环节讲到)。
因此,我们可以根据左右子树的高度做出决定:若左子树高度大于右子树的高度,那我们就选左子树中权值最大的节点;若左子树高度小于右子树的高度,我们就选右子树中权值最小的节点;若左子树高度与右子树的高度相等,那我们选哪一个都可以,这里我们默认为选左子树中权值最大的节点。
选好移动策略后,我们就该进行移动操作了。这里我们可以把移动操作分为下面几步:1.查找到要移动的节点的权值,记为value_move;2.删除这个权值为value_move的节点;3.以权值value_move生成一个新节点,记为node_move;4.将root节点中除权值外其他的必要属性赋值给node_move节点(如左右孩子指针等);5.释放原root节点的空间。
我们可以编写查找要移动的节点权值的函数如下:
// 获取平衡二叉树中的最大权值,返回值为获取到的最大权值
int get_max(struct Tree* root) {
// 参数root为要进行查找的平衡二叉树根节点指针
// 向根节点的右子树逐层查找
while (root->right) {
root = root->right;
}
// 返回最大权值
return root->value;
}
// 获取平衡二叉树中的最小权值,返回值为获取到的最小权值
int get_min(struct Tree* root) {
// 参数root为要进行查找的平衡二叉树根节点指针
// 向根节点的左子树逐层查找
while (root->left) {
root = root->left;
}
// 返回最小权值
return root->value;
}
观察上面的get_max和get_min函数,被移动的节点至少有一棵子树是空树,即删除这个被移动的节点肯定属于上面三种分类中的前面两种。
至此,我们编写删除同时拥有两棵子树的节点代码如下:
// 删除的节点既有左子树,也有右子树
else {
struct Tree* tmp = root;
int value_move = 0;
// 获取移动节点权值,并删除移动节点
if (root->left_height >= root->right_height) {
value_move = get_max(tmp->left);
tmp->left = delete_node(tmp->left, value_move);
}
else {
value_move = get_min(tmp->right);
tmp->right = delete_node(tmp->right, value_move);
}
// 生成新节点顶替删除节点
root = root_init(value_move);
root->left = tmp->left;
root->right = tmp->right;
root->left_height = tmp->left_height;
root->right_height = tmp->right_height;
// 释放删除节点的空间
free(tmp);
tmp = NULL;
}
最后,整合整个delete_node函数:
// 删除节点,返回值为删除完毕后得到的平衡二叉树根节点的指针
struct Tree* delete_node(struct Tree* root, int value) {
// 参数root为要删除节点的平衡二叉树根节点指针,value为目标删除节点的权值
// 判空处理
if (root == NULL) {
printf("没有这个节点!\n");
return NULL;
}
if (value < root->value) {
// 目标删除节点在当前二叉树的左子树
root->left = delete_node(root->left, value);
// 更新左子树高度
root->left_height = get_height(root->left);
if (root->right_height - root->left_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else if (value > root->value) {
// 目标删除节点在当前二叉树的右子树
root->right = delete_node(root->right, value);
// 更新右子树高度
root->right_height = get_height(root->right);
if (root->left_height - root->right_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else {
// 删除的节点为叶子节点
if (root->left == NULL && root->right == NULL) {
// 释放被删除节点的空间
free(root);
root = NULL;
}
// 删除的节点仅有右子树
else if (root->left == NULL) {
struct Tree* tmp = root;
root = root->right;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
// 删除的节点仅有左子树
else if (root->right == NULL) {
struct Tree* tmp = root;
root = root->left;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
// 删除的节点既有左子树,也有右子树
else {
struct Tree* tmp = root;
int value_move = 0;
// 获取移动节点权值,并删除移动节点
if (root->left_height >= root->right_height) {
value_move = get_max(tmp->left);
tmp->left = delete_node(tmp->left, value_move);
}
else {
value_move = get_min(tmp->right);
tmp->right = delete_node(tmp->right, value_move);
}
// 生成新节点顶替删除节点
root = root_init(value_move);
root->left = tmp->left;
root->right = tmp->right;
root->left_height = tmp->left_height;
root->right_height = tmp->right_height;
// 释放删除节点的空间
free(tmp);
tmp = NULL;
}
}
// 返回删除目标节点后的平衡二叉树根节点指针
return root;
}
6.6 补充
又到了补充环节了,在增加节点中我们就曾经说过,adjust函数调用时调整的二叉树必定是最小不平衡二叉树,这在删除节点中也是一样的。我们观察一下就可以知道,在删除节点过程中传入adjust函数的节点指针,也是从叶子节点开始自下向上的。
但是,在删除节点中,adjust函数一旦被调用,那被调用的次数就可能不止一次了。在add函数中,adjust函数之所以一旦被调用则只会有一次,是因为调用结束后调整得到的新平衡二叉树高度必定和新增节点前的平衡二叉树高度一致,但在删除节点的过程中,这一结论并不一定成立。
我们以图3为例,即左左型(其他类型这里不再一一赘述,道理是类似的)。如果是增加节点,那么图3这棵二叉树原本的高度就会是n+2,调整完后得到的平衡二叉树高度也是n+2,这没问题。但如果是删除节点,出现图3这种情况只可能是D子树原本的高度是n+1,被删除了一个节点后D子树高度变成了n,那么原本这棵平衡二叉树的高度就不是n+2,而是n+3,也就是说调整完图3这棵二叉树后,新得到的平衡二叉树的高度相比于原本是减少了1的。
在这种情况下,若图3这棵二叉树同时也是另一棵更大的二叉树中的D子树呢?那就会再次出现上面的情况,adjust函数就会被再次调用。但无论adjust函数被调用多少次,每次得到的平衡二叉树的高度相比于原来的高度最多只会减少1,又因为原本整棵二叉树就是平衡二叉树,左右子树高度差最大为1,所以在adjust函数调整完后出现的最极端的情况,无非就是被调整的二叉子树高度原本比其兄弟子树的高度少1,调整后该二叉子树高度减1,从而其与兄弟子树的高度差变成了2。而我们编写的adjust函数对于左右子树高度差为2的任何一种最小不平衡二叉树都是可以应对的,所以删除节点函数delete_node理论上是可以达到我们想要的效果的。
7.修改节点
如果你完全弄懂了增加节点和删除节点这两部分,那么修改节点对你来说肯定就是小意思了。因为我们并不打算编写修改节点的函数,而是直接复用增加节点和删除节点的代码,以此达到修改节点的目的。
我们不指望写一个专门的函数,去查找到目标节点,然后直接把它的权值修改为目标值。如果这样做的话,若要满足二叉搜索树性质,修改权值后的节点很有可能就不能在它原本的位置上了。至于要换到哪里才能重新满足二叉搜索树性质,要讨论的情况可就太多了。
因此,既然有现成的代码可以用,我们为什么还要去再绞尽脑汁想其他的呢?当然,这也是因为想出来的方法很可能并不会比已有方法高效。言归正传,为了实现修改节点权值效果,我们可以先把目标节点删除,然后增加以修改后的权值生成的节点。这样一来,修改节点权值的目的就可以通过这两步得以达成了。不过在进行上述操作之前,我们还需要先确保修改节点一开始就是存在的,且修改后的节点不与任何其他节点重复。
8.补充
你没看错,还是补充。不过放心,这是本文最后一次补充了,而这次我们要补充的是关于上面这些操作时间复杂度和空间复杂度的。
我们这里讨论的时间复杂度看的不是平均执行时间,而是最坏情况下的执行时间,空间复杂度也同理。观察上面的查找节点、增加节点以及删除节点,我们发现这些函数递归的最大深度就是整棵平衡二叉树的高度(或加一减一),而每一层递归中进行的一些基本操作或者调用其他函数都仅是常数级别的时间复杂度(删除节点函数中的get_min、get_max除外,这两个函数时间复杂度也与平衡二叉树的高度相关,但因为这两个函数仅会被调用一次,故可以看作重复调用了一次删除节点函数,而调用一次还是两次删除节点函数在时间复杂度上并没有区别),同时每一层递归中需要的辅助空间也是常数级别的,因此上述这三个操作的时间复杂度和空间复杂度仅和平衡二叉树的高度相关。
因此,求时间复杂度和空间复杂度的问题可以转换为已知平衡二叉树节点数目,求该平衡二叉树的最大高度。但若要我们直接去求解这个问题,难度还是不小的,所以我们可以将问题再进行一次转换,变为已知平衡二叉树的高度,求至少需要多少个节点才能组成这棵平衡二叉树。
要求解这个问题,我们需要用到动态规划的思想。现在我们令函数f(n)表示组成高度为n的平衡二叉树最少需要的节点数目,那么我们不难知道,f(1)=1,f(2)=2。
而在已知f(1)和f(2)的情况下,我们就可以推导出f(3)=4。推导过程如下:
要得到高度为3的平衡二叉树,那这棵平衡二叉树至少得有一棵子树高度为2。又因为我们需要组成平衡二叉树的节点数目最少,那么另一棵子树就需要有多小就取多小。而平衡二叉树性质规定任何左右子树高度差不得大于1,所以在必须有一棵子树高度为2的情况下,另一棵子树的高度最小为1。与此同时,这两棵子树也要满足组成它们的节点数目也是最小,因此整棵高度为3的平衡二叉树的节点数最小为两棵子树的节点数加起来后再加上根节点,即f(3)=f(1)+f(2)+1。
以此类推,我们有f(4)=f(2)+f(3)+1=7,f(5)=f(3)+f(4)+1=12,......
最后,整理一下规律,我们便可以得到通式:f(n)=f(n-2)+f(n-1)+1,且n3,f(1)=1,f(2)=2。
然后再把问题转换回去,我们就可以知道,当平衡二叉树的节点数大于等于f(n)且小于f(n+1)时,其高度最大为n,即查找节点、增加节点以及删除节点的时间复杂度为O(n)。
借助计算工具,我们可以计算出f(100)=927372692193078999175,2的一百次方等于1267650600228229401496703205376,1.5的一百次方取整等于406561177535215232,因此当平衡二叉树节点规模为n时,查找节点、增加节点以及删除节点的时间复杂度和空间复杂度应大于O(log2 n),小于O(log1.5 n),通常也可写作O(logn)。
9.总结
通过上文时间复杂度和空间复杂度的分析,与有序数组对比起来,在平衡二叉树上进行数据的查找需要的时间其实并不比在有序数组上进行二分查找要短,甚至需要耗费的空间反而要更多。但是,如果我们不仅需要频繁地查找数据,还需要不断对数据进行增加删除等操作,那么平衡二叉树的优势就体现出来了。因为在有序数组上进行数据的插入或删除,时间复杂度是O(n),这显然不如平衡二叉树的O(logn)。
好了,以上就是笔者关于实现平衡二叉树的见解了,可能有点啰嗦,如果有不足或错误的地方,欢迎各位读者在评论区提出来,我会进行改正,谢谢!
10.完整源代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define max(a, b) (a > b ? a : b)
struct Tree* root_init(int value);
struct Tree* search(struct Tree* root, int value);
struct Tree* add(struct Tree* root, struct Tree* new_node);
struct Tree* adjust(struct Tree* root);
struct Tree* left_left(struct Tree* root);
struct Tree* left_right(struct Tree* root);
struct Tree* right_left(struct Tree* root);
struct Tree* right_right(struct Tree* root);
struct Tree* delete_node(struct Tree* root, int value);
struct Tree* free_tree(struct Tree* root);
int get_height(struct Tree* root);
int get_max(struct Tree* root);
int get_min(struct Tree* root);
//void check_balance(struct Tree* root, int* pre);
// 平衡二叉树节点结构体
struct Tree {
int value; // 当前节点的权值
int left_height; // 当前节点左子树的高度
int right_height; // 当前节点右子树的高度
struct Tree* left; // 左孩子根节点指针
struct Tree* right; // 右孩子根节点指针
};
// 生成二叉树节点,返回值为生成的节点的地址
struct Tree* root_init(int value) {
// 参数value代表生成的树节点存储的值
// 动态分配空间
struct Tree* root = (struct Tree*)malloc(sizeof(struct Tree));
if (root == NULL) {
printf("动态分配空间失败!");
exit(1);
}
// 初始化树节点信息
root->value = value;
root->left_height = 0;
root->right_height = 0;
root->left = NULL;
root->right = NULL;
return root;
}
// 在平衡二叉树中根据权值查询节点,返回值为查找到的节点的指针(若没查找到则返回空指针)
struct Tree* search(struct Tree* root, int value) {
// 参数root为平衡二叉树根节点指针,value为要查询的权值
// 若平衡二叉树为空,直接返回空指针
if (root == NULL) {
return NULL;
}
if (root->value == value) {
// 当前根节点权值等于目标权值,查找成功,返回匹配的节点的指针
return root;
}
else if (root->value > value) {
// 向左子树进行查找
return search(root->left, value);
}
else {
// 向右子树进行查找
return search(root->right, value);
}
}
// 往平衡二叉树增加节点,返回值为新平衡二叉树根节点指针
struct Tree* add(struct Tree* root, struct Tree* new_node) {
// 参数root为平衡二叉树根节点指针,new_node为要添加的节点指针
// 平衡二叉树为空,直接返回添加节点的指针
if (root == NULL) {
return new_node;
}
if (new_node->value < root->value) {
// 将新节点添加到左子树
root->left = add(root->left, new_node);
// 更新左子树高度
root->left_height = get_height(root->left);
if (root->left_height - root->right_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else if (new_node->value > root->value) {
// 将新节点添加到右子树
root->right = add(root->right, new_node);
// 更新右子树高度
root->right_height = get_height(root->right);
if (root->right_height - root->left_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else {
// 找到重复节点
printf("不能添加重复节点!\n");
}
// 返回添加了新节点后的平衡二叉树根节点指针
return root;
}
// 获取平衡二叉树高度,返回值即为获取到的高度值
int get_height(struct Tree* root) {
// 参数root为求高度的平衡二叉树根节点指针
// 判空处理
if (root == NULL) {
return 0;
}
// 取root节点左右子树高度最大值加一
else {
return max(root->left_height, root->right_height) + 1;
}
}
// 调整还不是平衡二叉树的二叉树一些节点的位置,使其成为平衡二叉树,返回值为调整后得到的平衡二叉树根节点的指针
struct Tree* adjust(struct Tree* root) {
// 参数root为待调整的二叉树的根节点指针
// 判空处理
if (root == NULL) {
return NULL;
}
if (root->left_height - root->right_height == 2) {
// 进一步判断使用左左型还是左右型调整方法
if (root->left->left_height < root->left->right_height) {
// 进行左右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_right(root);
}
else {
// 进行左左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return left_left(root);
}
}
else {
// 进一步判断使用右左型还是右右型调整方法
if (root->right->left_height > root->right->right_height) {
// 进行右左型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_left(root);
}
else {
// 进行右右型调节,并将调节后得到的平衡二叉树根节点指针直接返回
return right_right(root);
}
}
}
// 左左型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* left_left(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点左孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->left;
root->left = tmp->right;
tmp->right = root;
// 更新子树高度
(root->left_height) -= 2;
(tmp->right_height) += 1;
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
// 左右型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* left_right(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点的左孩子节点的右孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->left->right;
root->left->right = tmp->left;
tmp->left = root->left;
root->left = tmp->right;
tmp->right = root;
// 更新子树高度
tmp->left->right_height = get_height(tmp->left->right);
root->left_height = get_height(root->left);
tmp->left_height = get_height(tmp->left);
tmp->right_height = get_height(tmp->right);
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
// 右左型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* right_left(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点的右孩子节点的左孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->right->left;
root->right->left = tmp->right;
tmp->right = root->right;
root->right = tmp->left;
tmp->left = root;
// 更新子树高度
tmp->right->left_height = get_height(tmp->right->left);
root->right_height = get_height(root->right);
tmp->left_height = get_height(tmp->left);
tmp->right_height = get_height(tmp->right);
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
// 右右型二叉树调节为平衡二叉树,返回值为调节完成后得到的平衡二叉树根节点的指针
struct Tree* right_right(struct Tree* root) {
// 参数root为需要调节的二叉树根节点的指针
// 临时指针变量存储原根节点右孩子指针变量的值,然后对结构进行调整
struct Tree* tmp = root->right;
root->right = tmp->left;
tmp->left = root;
// 更新子树高度
(root->right_height) -= 2;
(tmp->left_height) += 1;
// 返回调整后得到的平衡二叉树的根节点指针
return tmp;
}
// 删除节点,返回值为删除完毕后得到的平衡二叉树根节点的指针
struct Tree* delete_node(struct Tree* root, int value) {
// 参数root为要删除节点的平衡二叉树根节点指针,value为目标删除节点的权值
// 判空处理
if (root == NULL) {
printf("没有这个节点!\n");
return NULL;
}
if (value < root->value) {
// 目标删除节点在当前二叉树的左子树
root->left = delete_node(root->left, value);
// 更新左子树高度
root->left_height = get_height(root->left);
if (root->right_height - root->left_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else if (value > root->value) {
// 目标删除节点在当前二叉树的右子树
root->right = delete_node(root->right, value);
// 更新右子树高度
root->right_height = get_height(root->right);
if (root->left_height - root->right_height == 2) {
// 调整二叉树使其成为平衡二叉树
root = adjust(root);
}
}
else {
// 删除的节点为叶子节点
if (root->left == NULL && root->right == NULL) {
// 释放被删除节点的空间
free(root);
root = NULL;
}
// 删除的节点仅有右子树
else if (root->left == NULL) {
struct Tree* tmp = root;
root = root->right;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
// 删除的节点仅有左子树
else if (root->right == NULL) {
struct Tree* tmp = root;
root = root->left;
// 释放被删除节点的空间
free(tmp);
tmp = NULL;
}
// 删除的节点既有左子树,也有右子树
else {
struct Tree* tmp = root;
int value_move = 0;
// 获取移动节点权值,并删除移动节点
if (root->left_height >= root->right_height) {
value_move = get_max(tmp->left);
tmp->left = delete_node(tmp->left, value_move);
}
else {
value_move = get_min(tmp->right);
tmp->right = delete_node(tmp->right, value_move);
}
// 生成新节点顶替删除节点
root = root_init(value_move);
root->left = tmp->left;
root->right = tmp->right;
root->left_height = tmp->left_height;
root->right_height = tmp->right_height;
// 释放删除节点的空间
free(tmp);
tmp = NULL;
}
}
// 返回删除目标节点后的平衡二叉树根节点指针
return root;
}
// 获取平衡二叉树中的最大权值,返回值为获取到的最大权值
int get_max(struct Tree* root) {
// 参数root为要进行查找的平衡二叉树根节点指针
// 向根节点的右子树逐层查找
while (root->right) {
root = root->right;
}
// 返回最大权值
return root->value;
}
// 获取平衡二叉树中的最小权值,返回值为获取到的最小权值
int get_min(struct Tree* root) {
// 参数root为要进行查找的平衡二叉树根节点指针
// 向根节点的左子树逐层查找
while (root->left) {
root = root->left;
}
// 返回最小权值
return root->value;
}
检查平衡二叉树平衡性质
//void check_balance(struct Tree* root, int* pre) {
// // 参数root为检查的平衡二叉树根节点指针,pre为中序遍历时存储前一个节点权值的变量的指针
//
// // 判空处理
// if (root == NULL) {
// return;
// }
// // 中序遍历当前节点左子树
// check_balance(root->left, pre);
// // 检查中序遍历时前一个元素是否小于当前元素
// if (*pre >= root->value) {
// printf("\n\n不满足二叉搜索树性质!\n\n");
// exit(1);
// }
// // 更新记录前一个节点权值的变量
// *pre = root->value;
// // 检查当前节点左右子树高度差
// int height_difference = root->left_height - root->right_height;
// if (height_difference < -1 || height_difference > 1) {
// printf("\n\n左右子树高度差大于1!\n\n");
// exit(1);
// }
// // 打印当前节点信息
// printf("-----------------\n");
// printf("%d %d %d\n", root->value, root->left_height, root->right_height);
// printf("-----------------\n");
// // 中序遍历当前节点右子树
// check_balance(root->right, pre);
//}
// 释放平衡二叉树所有节点空间,返回值为NULL
struct Tree* free_tree(struct Tree* root) {
// 参数root为要释放空间的平衡二叉树根节点指针、
// 后序地释放节点空间
if (root) {
root->left = free_tree(root->left);
root->right = free_tree(root->right);
free(root);
root = NULL;
}
// 返回释放空间后的二叉树根节点指针,其实必定是NULL
return root;
}
// 主函数
int main() {
// 平衡二叉树根节点指针变量
struct Tree* root = NULL;
// 记录选择选项的变量
int choice = 0;
// 记录操作节点权值的变量
int value = 0;
// 主循环
while (choice != 4) {
printf("1.查找节点\n");
printf("2.增加节点\n");
printf("3.删除节点\n");
printf("4.退出\n");
printf("请输入你的选择:");
scanf_s("%d", &choice);
if (choice == 1) {
// 根据权值查找节点,并获取到目标节点的指针值
printf("请输入你要查找的节点的权值:");
scanf_s("%d", &value);
struct Tree* tmp = search(root, value);
// 此处编写读者想要利用查到的节点指针值进行的操作
}
else if (choice == 2) {
// 根据权值构造新节点并增加到平衡二叉树中
printf("请输入你要增加的节点的权值:");
scanf_s("%d", &value);
root = add(root, root_init(value));
}
else if (choice == 3) {
// 根据权值删除节点
printf("请输入你要删除的节点的权值:");
scanf_s("%d", &value);
root = delete_node(root, value);
}
else if (choice == 4) {
break;
}
else {
printf("没有这个选项!\n");
}
检查平衡二叉树平衡性质,仅作为测试代码时用,测试完后可以注释掉
//if (root) {
// int pre = get_min(root) - 1;
// check_balance(root, &pre);
//}
}
// 释放平衡二叉树空间
root = free_tree(root);
return 0;
}
大家在测试的时候可以自行编写自动生成随机数的方法进行测试。