这是小星学DSA系列的第三篇,我会记录我学习的过程与理解,希望能够帮到你。
本篇文章的思维导图如下,在文章的末尾,我会给出更加详细的思维导图。
上篇文章中我们介绍了二叉树的基础知识,这篇我们来介绍常用的二叉树进阶篇。
二叉搜索树
二叉搜索树的定义
虽然我们已经在前面两篇文章中都涉及到了二叉搜索树,这里还是再次给二叉搜索树正式的定义。
所有节点的值唯一,且左子树的值都小于该节点的值,右子树的值都大于该节点的值。
二叉搜索树的操作
在小星学DSA丨一文学完红黑树篇中,我们其实已经介绍过二叉搜索树的操作,这里我们将这些操作再次介绍一遍。
查找
二叉搜索树是有序的,因此二叉搜索树的查找不需要遍历整个树,只需要根据需要查找的值与当前节点的值比较即可,可以大大降低查找的时间复杂度。
插入
二叉搜索树的插入不同于二叉树的插入,一个节点插入的位置是固定的。
我们用搜索的方式去遍历二叉搜索树,找到小于插入值的最大叶子节点,或大于插入值的最小叶子节点,这个节点的下方就是我们要插入的位置。
删除
二叉搜索树的删除首先仍是通过查找的方式,找到需要删除的节点。
由于在删除后仍需要保持二叉搜索树的性质,因此对于非叶子节点,需要找到合适的值替代,具体替代方式我在红黑树的删除也讲过一次。
主要分为以下三种情况
性能分析
平衡情况(接近完全二叉树) | 非平衡情况(退化成单链表) | |
---|---|---|
搜索 | O(logn) | O(n) |
插入 | O(logn) | O(n) |
删除 | O(logn) | O(n) |
二叉平衡树
定义
二叉平衡树是左子树和右子树高度差至多为1的二叉搜索树。
平衡因子
为了更好的描述二叉平衡树,我们引入了平衡因子的概念,它的计算公式为
平衡因子 = 左子树高度 − 右子树高度 平衡因子=左子树高度-右子树高度 平衡因子=左子树高度−右子树高度
有了平衡因子之后,我们就能重新定义二叉平衡树,即平衡因子为-1,0或1的二叉搜索树。
二叉平衡树的操作
左旋与右旋
在二叉平衡树中,为了维持平衡,我们也需要左旋与右旋这一操作。
💫小星说丨还记得我们在红黑树一节说的,左旋即:被左旋的节点成为左节点;右旋:被右旋的节点成为右节点
左旋与右旋都分两步:1. 把准父节点中,多余的子节点接到原父节点上 2. 准父节点与原父节点的父子关系调整,如下图所示
搜索
由于二叉平衡树也是二叉搜索树,因此二叉平衡树的搜索与二叉搜索树的搜索完全相同
插入
二叉平衡树,由于需要维持平衡,因此其插入过程分两步:
- 按照二叉搜索树的方式插入
- 平衡修正
当我们向二叉平衡树中插入节点后,若导致不平衡,则这一不平衡肯定最早出现在插入节点的祖父节点上,因此我们要对插入节点、父节点与祖父节点的关系进行平衡修正。
主要有以下几种情况:
- 祖父节点平衡因子>1,则插入节点和插入节点的父节点必然出现在祖父节点的左边,此时
- 若插入节点>父节点,父子方向不一致,则需要先将插入节点的父节点左旋使二者朝向一致,此时来到情况b
- 若插入节点与父节点方向一致,直接右旋祖父节点即可
- 祖父节点平衡因子< -1,则说明右边的链路更长,此时的处理与上面对称
- 若插入节点<父节点,父子方向不一致,则需要先将插入节点的父节点右旋
- 若父子方向一致,直接左旋插入节点的祖父节点
> 💫小星说丨插入只影响到了父节点和祖父节点,因此只需要根据这三个节点的形状进行适当的左右旋,使之变为“**^”**这样的形状就好了
>
删除
二叉平衡树的删除同样分为两步:
- 按照二叉搜索树的方式删除
- 平衡修正
二叉树平衡树中最终删除的必定是叶子节点的位置,因此这一不平衡肯定最早出现在被删除节点的父结点处,因此我们要对这一节点进行平衡修正。
- 该节点平衡因子>1,则左链路更长,考虑左链路
- 该节点左子节点的平衡因子<0,即对左子节点而言,其右链路更长,因此我们需要先对左子节点进行左旋,以补偿左子节点 的左链路,来到情况b
- 该节点左子节点的平衡因子≥0, 则我们对该节点进行右旋,以补偿该节点的右链路
- 该节点平衡因子< -1,则右链路更长
- 该节点右子节点平衡因子>0,对右子节点来说,其左链路更长,因此需要先补偿右子节点的左链路
- 该节点右子节点平衡因子≤0,直接左旋补偿左链路
其实到这里我们发现,删除的修正与插入的修正都是一样的,即对平衡因子绝对值大于1的节点通过左旋或右旋补偿较短的那条链路,如果子节点需要补偿的方向与该节点一致则可直接补偿,否则,需要先补偿子节点,再补偿该节点。
性能分析
二叉平衡树由于能够保证树左右高度差不超过1,因此其操作的时间复杂度能够保持在最优
平衡情况(接近完全二叉树) | |
---|---|
搜索 | O(logn) |
插入 | O(logn) |
删除 | O(logn) |
手撕二叉进阶树
手撕二叉搜索树
节点定义
二叉搜索树的节点定义与二叉树相同,不需要额外的值
struct Node
{
int data;
Node *left;
Node *right;
};
搜索
用递归的方式实现搜索
NodePtr searchTreeHelper(NodePtr node, int key)
{
if (node == nullptr || key == node->data)
{
return node;
}
if (key < node->data)
{
return searchTreeHelper(node->left, key);
}
return searchTreeHelper(node->right, key);
}
NodePtr searchTree(int k)
{
return searchTreeHelper(this->root, k);
}
插入
同样用递归的方式插入, 找到插入点
NodePtr insertNode(NodePtr node, int data)
{
if (node == nullptr)
return newNode(data);
if (data < node->data)
{
node->left = insertNode(node->left, data);
}
else
{
node->right = insertNode(node->right, data);
}
return node;
}
删除
我们也可以使用递归的方式实现删除,如下
其中,后继节点为右子树的最小节点,由findMin函数实现
void deleteNode(NodePtr &root, int value)
{
if (!root)
{
return;
}
if (value < root->data)
{
deleteNode(root->left, value);
}
else if (value > root->data)
{
deleteNode(root->right, value);
}
else
{
// 情况1&情况2的一部分:为叶子节点或只有右节点
if (!root->left)
{
NodePtr temp = root->right;
delete root;
root = temp;
}
// 只有左节点
else if (!root->right)
{
NodePtr temp = root->left;
delete root;
root = temp;
}
// 作于节点都没有,使用后继节点代替,并在右子树中删除后继节点
else
{
NodePtr temp = findMin(root->right);
root->data = temp->data;
deleteNode(root->right, temp->data);
}
}
}
NodePtr findMin(NodePtr node) {
while (node->left) {
node = node->left;
}
return node;
}
手撕二叉平衡树
节点定义
由于我们需要计算节点的平衡因子,因此在定义节点时,需要额外定义节点的高度,以方便平衡因子的计算。
struct Node
{
int data;
Node *left;
Node *right;
int height;
};
相应的,我们需要实现高度的计算与更新、平衡因子的计算等函数
int getHeight(NodePtr node)
{
if (!node)
{
return 0;
}
return node->height;
}
int getBalanceFactor(NodePtr node)
{
if (!node)
{
return 0;
}
return getHeight(node->left) - getHeight(node->right);
}
void updateHeight(NodePtr node)
{
node->height = max(getHeight(node->left), getHeight(node->right)) + 1;
}
左旋与右旋
二叉平衡树的左旋与右旋除了需要实现定义好的行为之外,还需要对节点高度进行重新计算:
NodePtr leftRotate(NodePtr node)
{
// rotate
NodePtr right = node->right;
NodePtr rleft = right->left;
right->left = node;
node->right = rleft;
// update height
// only child-changed node need to update
updateHeight(node);
updateHeight(right);
return right;
}
NodePtr rightRotate(NodePtr node)
{
// rotate
NodePtr left = node->left;
NodePtr lright = left->right;
left->right = node;
node->left = lright;
// update height
// only child-changed node need to update
updateHeight(node);
updateHeight(left);
return left;
}
查找
查找的实现与二叉搜索树相同
插入
我们使用递归的方式实现插入,在前序部分插入,后序部分处理平衡逻辑,这样就实现了自下至上的平衡。
NodePtr insertHelper(NodePtr node, int data)
{
// 1. 二叉搜索树式插入
// 叶子节点插入位置
if (!node)
{
return createNewNode(data);
}
if (data < node->data)
{
node->left = insertHelper(node->left, data);
}
else if (data > node->data)
{
node->right = insertHelper(node->right, data);
}
else
{
return node;
}
// 后序方式,更新当前节点平衡因子
// 实际上在插入节点的祖父节点处才会进入以下的计算
updateHeight(node);
int balanceFactor = getBalanceFactor(node);
// 情況1a
if (balanceFactor > 1 && data > node->left->data)
{
node->left = leftRotate(node->left);
return rightRotate(node);
}
// 情況1b
if (balanceFactor > 1 && data < node->left->data)
{
return rightRotate(node);
}
// 情況2a
if (balanceFactor < -1 && data < node->right->data)
{
node->right = rightRotate(node->right);
return leftRotate(node);
}
// 情況2b
if (balanceFactor < -1 && data > node->right->data)
{
return leftRotate(node);
}
return node;
}
NodePtr insertNode(int data)
{
insertHelper(this->root, data);
}
删除
NodePtr deleteHelper(NodePtr node, int data)
{
// 1. 二叉搜索树方式删除节点
if (!node)
{
return node;
}
if (data < node->data)
{
node->left = deleteHelper(node->left, data);
}
else if (data > node->data)
{
node->right = deleteHelper(node->right, data);
}
else
{
if (!root->left)
{
NodePtr temp = root->right;
delete root;
root = temp;
}
else if (!root->right)
{
NodePtr temp = root->left;
delete root;
root = temp;
}
else
{
NodePtr temp = findMin(root->right);
root->data = temp->data;
deleteHelper(root->right, temp->data);
}
}
// 2.后序方式,更新当前节点平衡因子并进行平衡修正
updateHeight(node);
int balanceFactor = getBalanceFactor(node);
// 情況1a
if (balanceFactor > 1 && getBalanceFactor(node->left) < 0)
{
node->left = leftRotate(node->left);
return rightRotate(node);
}
// 情況1b
if (balanceFactor > 1 && getBalanceFactor(node->left) >= 0)
{
return rightRotate(node);
}
// 情況2a
if (balanceFactor < -1 && getBalanceFactor(node->right) > 0)
{
node->right = rightRotate(node->right);
return leftRotate(node);
}
// 情況2b
if (balanceFactor < -1 && getBalanceFactor(node->right) <= 0)
{
return leftRotate(node);
}
return node;
}
public:
NodePtr insertNode(int data)
{
insertHelper(this->root, data);
}
NodePtr deleteNode(int data)
{
deleteHelper(this->root, data);
}
总结
最后我们再用一张思维导图总结这篇博客的内容
源代码
本文代码已在github上开源,包含c++,python(待补充), golang(待补充)的红黑树代码
https://github.com/Yuxin1999/star-code
参考内容
- https://www.programiz.com/dsa/avl-tree
- 萨尼 S, 王立柱, 刘志红 数据结构、算法与应用 C++语言描述[M]. 北京: 机械工业出版社, 2015.