解密树的平衡:二分搜索树 → AVL自平衡树 → 左倾红黑树
Posted Debroon
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解密树的平衡:二分搜索树 → AVL自平衡树 → 左倾红黑树相关的知识,希望对你有一定的参考价值。
从二叉树框架开始,过渡到二分搜索树,通过自平衡机制,过渡到AVL树,通过绝对平衡,过渡到红黑树,一步接一步,把技术学到位。
二叉树框架
二叉树或者应该称为,二叉链表树。
- 每个节点都最多有两个叉的树
- 实现树的底层结构是链表
所以,二叉树和链表会有很多相同之处。
链表遍历框架:
class ListNode
int val;
ListNode next;
void traverse( ListNode head )
for( ListNode p = head; p != NULL; p = p.next ) // 迭代遍历
do ···
void traverse( ListNode head ) // 前序遍历
print(head.val);
traverse(head.next);
void traverse( ListNode head ) // 后序遍历
traverse(head.next);
print(head.val);
二叉树遍历框架:
class TreeNode
int val;
TreeNode left, right;
void traverse( TreeNode root ) // 前序遍历
print(root.val);
traverse(root.left);
traverse(root.right);
void traverse( TreeNode root ) // 中序遍历
traverse(root.left);
print(root.val);
traverse(root.right);
void traverse( TreeNode root ) // 后序遍历
traverse(root.left);
traverse(root.right);
print(root.val);
二叉树遍历框架拓展为多叉树遍历框架:
class TreeNode
int val;
TreeNode[] children;
void traverse( TreeNode root )
for( TreeNode child: root.children )
traverse(child);
我们把这二叉树框架映入脑海里,下文的二分搜索树就是二叉树的延伸。
二分搜索树
数组的查找快,插入、删除慢,链表的插入、删除快,查找慢。
高效率的动态修改和高效率的静态查找,究竟能否同时兼顾?
通过对二分查找策略的抽象与推广,定义并实现二分搜索树结构,兼顾了:
- 链表插入的灵活性(直接插入即可,不需要挪动其他元素)
- 数组查找的高效性(二分查找)
插入、删除、查询操作均为 O ( l o g n ) O(log ~n) O(log n)。
实现二分搜索树,满足以下俩个条件:
- 若它的左子树不为空,左子树上所有节点的值都小于它的根节点。
- 若它的右子树不为空,右子树上所有的节点的值都大于它的根节点。
struct TreeNode
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr)
TreeNode(int x) : val(x), left(nullptr), right(nullptr)
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right)
;
验证
请打开题目:
- 验证二分搜索树的合法性:98. 验证二叉搜索树
按照二叉树框架:
void traverse( TreeNode root )
/* 明确一个节点(root) 要做的事情 */
// 其他部分,交给递归
traverse(root.left);
traverse(root.right);
按照二分搜索树的定义,一个节点自己要做的事情不就是比左孩子大,比右孩子小吗!
class Solution
public:
bool isValidBST(TreeNode* root)
/* 明确一个节点(root) 要做的事情 */
if( !root ) // 定义规定:空也是一颗二分搜索树
return true;
if( root->left && root->val <= root->left->val ) // 定义规定:根节点 > 左孩子
return false;
if( root->right && root->val >= root->right->val ) // 定义规定:根节点 < 右孩子
return false;
// 其他部分,交给递归
return isValidBST(root.left) && isValidBST(root.right);
;
提交上去,发现居然不对。
二分搜索树的定义,不只是:
- 根节点 > 左孩子
- 根节点 < 右孩子
还有:
- 左子树上所有节点的值都小于它的根节点
- 右子树上所有的节点的值都大于它的根节点
比如:
10
/ \\
5 15
/ \\
6 20
P.S. 6 处于根节点的右子树,按照定义,凡右子树任何的子节点都要大于根节点 10
那么一个节点要做的事,不仅是和俩个孩子节点比较,还得和整个左子树、右子树的所有节点比较。
但其实我们也没必要和子树里面所有节点都比较,只需要知道一个子树的上界、下界,如果比较节点的值与 Min&Max,在范围外则 return false。
这个方法最关键的是理解对于每个节点来说,什么是他的 Min&Max。
以上图的树为例进行分析:
- 从 10 开始,因为是 root,所以可以为任何值。也就是在【Integer.MIN_VALUE, Integer.MAX_VALUE】范围内。
- 递归到 10 的右子树 15,15 的范围是多少呢?首先需要比 parent 要大,也就是 >10。上边界呢?没有改变,可以非常大,只要 >10 就行了。所以范围是【10+1,Integer.MAX_VALUE】。
- 递归到 15 的右子树 20,20 的范围是多少呢?首先需要比 parent 要大,也就是>15。上边界呢?没有改变,可以非常大,只要 >16 就行了。所以范围是【15+1,Integer.MAX_VALUE】。
- 递归到 15 的左子树 11,11 的范围是多少呢?首先需要比 parent 要小,也就是 <15。下边界呢?没有改变。所以范围是【11,15-1】。
class Solution
public:
bool isValidBST(TreeNode* root)
return isValidBST(root, LONG_MIN, LONG_MAX);
// 使用 INT_MAX 会溢出,改用 LONG_MAX
// 因为函数参数列表要增加参数,需要使用辅助函数
bool isValidBST(TreeNode* root, long long min, long long max)
if( !root ) // 节点为空,则说明一直到此的节点都在边界范围内,树没有问题
return true;
if( root->val <= min || root->val >= max )
return false;
return isValidBST(root->left, min, root->val) && isValidBST(root->right, root->val, max);
// 节点不是null,那么继续开始递归到左右子树(更新Min&Max)
// 并且只有他的左右子树都能通过测试时,这个节点才能通过测试。所以用 &&
;
查找
按照二叉树框架:
void traverse( TreeNode root )
/* 明确一个节点(root) 要做的事情 */
// 其他部分,交给递归
traverse(root.left);
traverse(root.right);
在二分搜索树中查找一个数是否查找:
int target;
bool isInBST( TreeNode root )
/* root 该做的事 */
if( !root )
return false;
if( root->val == target )
return true;
// 递归框架
return isInBST(root->left) || isInBST(root->right);
大框架是如此,此外 BST 还有 “左小右大” 的特点。
bool isInBST( TreeNode root, int target )
/* root 该做的事 */
if( !root )
return false;
if( root->val == target )
return true;
// 二分递归:一次排除一半
if( root->val < target )
return isInBST(root->right, target);
if( root->val > target )
return isInBST(root->left, target);
请打开题目:
class Solution
public:
TreeNode* searchBST(TreeNode* root, int val)
if (root == NULL || root->val == val) return root;
return root->val > val ? searchBST(root->left, val) : searchBST(root->right, val);
;
添加
在二分搜索树中插入一个数。
// 涉及到改,函数应该返回 TreeNode 类型,通过函数返回值接收
TreeNode insertIntoBST(TreeNode root, int val)
if( !root ) // 找到空位置
return new TreeNode(val); // 插入新节点
if( root->val == val ) // 已存在
return root; // 不重复,直接返回
if( root->val < val ) // val 大
root->right = insertIntoBST( root->right, val ); // 则插到右子树
if( root->val > val ) // val 小
root->left = insertIntoBST( root->left, val ); // 则插到左子树
请打开题目:
class Solution
public:
TreeNode* insertIntoBST(TreeNode* root, int val) // 俩个条件表达式嵌套
return !root ? new TreeNodeval :
(val < root->val ? root->left = insertIntoBST( root->left, val) :
root->right = insertIntoBST(root->right, val) , root);
;
删除
先写出删除框架:
TreeNode deleteNode(TreeNode root, int key)
if( root->val == key )
// 找到了,删除
else if( root->val > key )
root->left = deleteNode(root->left, key); // 去左子树寻找 key
else if( root->val < key )
root->right = deleteNode(root->right, key); // 去右子树寻找 key
return root;
删除有三种情况:
// 第一种:删除的是末端节点,没有孩子节点
if( !root->left && !root->right )
return NULL;
// 第二种:待删除节点有一个子节点
if( !root->left )
return root->right;
if( !root->right )
return root->left;
// 第三种,待删除节点有俩个子节点,必须找到左子树最大节点,或者右子树最小节点,这里找右子树最小节点
if( root->left && root->right )
TreeNode minNode = getMin(root->right); // 找到右子树的最小节点
root->val = minNode->val; // 最小节点上移到 root
root->right = deleteNode(root->right, minNode->val); // 删除最小节点
完整代码:
TreeNode deleteNode(TreeNode root, int key)
if( !root )
return NULL;
if( root->val == key )
// 第二种:待删除节点有一个子节点,包含了第一种情况
if( !root->left ) return root->right;
if( !root->right ) return root->left;
// 左右孩子都存在,第三种情况
TreeNode minNode = getMin(root->right); // 找到右子树的最小节点
root->val = minNode->val; // 最小节点上移到 root
root->right = deleteNode(root->right, minNode->val); // 删除最小节点
else if( root->val > key )
root->left = deleteNode(root->left, key); // 去左子树寻找 key
else if( root->val < key )
root->right = deleteNode(root->right, key); // 去右子树寻找 key
return root;
TreeNode getMin(TreeNode node)
while( !node->left ) // 最左边的就是最小的
node = node->left;
return node;
无论是增、删、改、查:
- 二叉树的总体设计思路,把当前节点要做的事做好,其他的抛给递归框架,不用操心
- 如果当前节点会对下面的子节点有整体影响,可通过辅助函数增长参数列表,借助参数传递信息
请打开题目:
class Solution
public:
TreeNode* deleteNode(TreeNode* root, int key)
if (root == nullptr)
return nullptr;
if (key > root->val)
root->right = deleteNode(root->right, key); // 去右子树删除
else if (key < root->val)
root->left = deleteNode(root->left, key); // 去左子树删除
else // 当前节点就是要删除的节点
if (! root->left)
return root->right; // 情况1,欲删除节点无左子
if (! root->right)
return root->left; // 情况2,欲删除节点无右子
TreeNode* node = root->right; // 情况3,欲删除节点左右子都有
while (node->left) // 寻找欲删除节点右子树的最左节点
node = node->left;
node->left = root->left; // 将欲删除节点的左子树成为其右子树的最左节点的左子树
root = root->right; // 欲删除节点的右子顶替其位置,节点被删除
return root;
;
AVL平衡树
平衡树:对于任意一个节点,左子树与右子树的高度差不能超过 1。
所以,平衡树的成员还需要添加一个高度 height
以及计算出每个节点的平衡因子(左右子树高度差)是多少。
// 计算每个节点的平衡因子
int getBalanceFactor(TreeNode *root)
if( !root )
return 0;
return getBalanceFactor(root.left) - getBalanceFactor(root.right);
插入
在插入时,平衡树可能就不平衡了,失去树的优势,从树退化成链表。
如果该节点变得不平衡,则有 4 种情况:
- LL,插入在左侧的左侧
- RR,插入在右侧的右侧
- LR,插入在左侧的右侧
- RL,插入在右侧的左侧
- 插入在左侧的左侧,导致高度差 > 1,需要右旋转
最简单的情况:
现在我们来讨论一下,通常的情况:
按照二分搜索树的特性:
T
1
<
z
<
T
2
<
x
<
T
3
<
y
<
T
4
T1<z<T2<x<T3<y<T4
T1<z<T2<x<T3<y<T4,我们如果要改变树,也必须按照这个特性来平衡。
整个右旋转过程如下(共 4 步):
这个过程相对于,节点 y 顺时针的旋转到了节点 x 的右子树位置。
// 右旋转
Node *rightRotate(Node *y)
Node *x = y->left;
Node *T2 = x->right;
// 向右旋转过程
x->right = y;
y->left = T2;
// 更新高度
y->height = max(height(y->left), height(y->right)) + 1;
x->height = max(height(x->left), height(x->right)) + 1;
return x;
2. 插入在右侧的右侧,导致高度差 > 1,需要左旋转
过程推导如右旋转。
// 左旋转
Node *leftRotate(Node *x)
Node *y = x->right;
Node *T2 = y->left;
// 向左旋转过程
y->left = x;
x->right = T2;
// 更新高度
x->height = max(height(x->left), height(x->right)) + 1;
y->height = max(height(y->left), height(y->right)) + 1;
return y;
3. 插入在左侧的右侧,导致高度差 > 1,需要旋转
旋转后,就变成了左侧的左侧:
右旋转即可。
4. 插入在右侧的左侧,导致高度差 > 1,需要旋转
解密树的平衡:二分搜索树 → AVL自平衡树 → 红黑树