解密树的平衡:二分搜索树 → 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) 
;

 


验证

请打开题目:

按照二叉树框架:

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 的右子树 1515 的范围是多少呢?首先需要比 parent 要大,也就是 >10。上边界呢?没有改变,可以非常大,只要 >10 就行了。所以范围是【10+1,Integer.MAX_VALUE】。
  • 递归到 15 的右子树 2020 的范围是多少呢?首先需要比 parent 要大,也就是>15。上边界呢?没有改变,可以非常大,只要 >16 就行了。所以范围是【15+1,Integer.MAX_VALUE】。
  • 递归到 15 的左子树 1111 的范围是多少呢?首先需要比 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. 插入在左侧的左侧,导致高度差 > 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自平衡树 → 红黑树

13-自平衡二分搜索树 AVLTree

数据结构AVL

数据结构AVL

十三彻底搞懂平衡二叉树AVL

动画 | 什么是平衡二分搜索树(AVL)?