AVL树、红黑树

AVL树、红黑树

AVL树和红黑树的出现,是为了解决二叉搜索树的节点不均匀而导致的效率降低的问题,将二叉树的子树高度控制平衡。他们的底层解决方法都是通过旋转来调整节点(与其说是旋转,不如说是按压节点),他们的不同就是依赖逻辑不同。

一、AVL树

特性

要平衡左右子树的高度的话,就得记录左右子树的高度差:因此AVL树引入了平衡因子(balance factor)的概念。

平衡因子:右子树的高度减去左子树的高度。

那么我们想要让左右子树高度平衡,就是要控制平衡因子的绝对值尽量的小。我们能控制的最小范围是多少呢?

能不能就一直是0,让左右子树高度一直相等?当然是不行!

最简单的就比如当在根节点下插入一个新节点,根节点的平衡因子必然不是0。

所以要控制的范围是[ -1, 1],也就是正常的平衡因子只能是-1、0、1。

因此将增删查改的效率控制在了O(logn)

结构性质

  1. 增加了平衡因子的属性;
  2. 节点的key、value包装在了pair容器中;
  3. 节点的key大小规则都满足二叉搜索树的规则。
  4. 增加了父节点的维护。
c++ 复制代码
template<class K, class V>
struct AVLTreeNode {
	pair<K, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;	// 平衡因子(balance factor)

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};


template<class K, class V>
class AVLTree {
	typedef AVLTreeNode<K, V> Node;
public:
    // ...成员函数
private:
	Node* _root = nullptr;
};

插入节点

步骤:

  1. 寻找插入位置;
  2. 找到位置之后接入新节点;
  3. 向上调整节点的平衡因子,因为增加节点之后大概率也会影响其祖先节点,遇到不正常的平衡因子要进行旋转,最坏情况:调整到根节点。
步骤1、2

操作和二叉搜索树没有区别,先判空树,再对比key值,大了往右走,小了往左走,直到遇到空节点,将新节点接入,连接父子节点。

c++ 复制代码
bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur) {
        if ((cur->_kv).first < kv.first) {
            parent = cur;
            cur = cur->_right;
        }
        else if ((cur->_kv).first > kv.first) {
            parent = cur;
            cur = cur->_left;
        }
        else {
            return false;
        }
    }
    cur = new Node(kv);
    if ((parent->_kv).first > kv.first) {
        parent->_left = cur;
    }
    else {
        parent->_right = cur;
    }
    cur->_parent = parent;
    // *****调整平衡*****
    // .....
    // *****************
    return true;
}
步骤3 调整平衡

当插入新节点cur时,cur的_bf必然为0;

需要修改parent->_bf

parent的_bf的变化规则:

方法:

c++ 复制代码
while (parent) {	// 因为需要向上调整祖先节点的_bf
    // 改变平衡因子
    if (cur == parent->_left) {
        --(parent->_bf);
    }
    else {
        ++(parent->_bf);
    }
    // ...后续分析_bf
}

那么parent->_bf变化之后的结果有哪些呢?

分析结果

分三种大类情况:

1.最棒情况:插入后parent->_bf == 0

说明插入前parent->_bf等于-1或1,也就是本来就有一个节点

那么此时就不需要再向上调整祖先节点的_bf了,因为此时插入的节点并没有改变parent这棵树的高度。所以可以break跳出向上调整的循环了。

c++ 复制代码
while (parent) {	// 因为需要向上调整祖先节点的_bf
    // 改变平衡因子
    if (cur == parent->_left) {
        --(parent->_bf);
    }
    else {
        ++(parent->_bf);
    }
    // ...后续分析_bf
    if (parent->_bf == 0) {
        break;
    }
    // else if (....) 
}
2.比较好的情况:插入后parent->_bf == 1或者-1

说明插入前一定parent->_bf == 0(也就是parent本来没有节点),如果是-2或2,那这棵树本来就是一颗坏树了

此时就是改变了parent这棵树的高度,会影响祖先节点的_bf,所以要向上调整,(也就是cur、parent向上一层,再次进入循环)

c++ 复制代码
while (parent) {	// 因为需要向上调整祖先节点的_bf
    // 改变平衡因子
    if (cur == parent->_left) {
        --(parent->_bf);
    }
    else {
        ++(parent->_bf);
    }
    // 分析_bf
    if (parent->_bf == 0) {
        break;
    }
    else if (parent->_bf == 1 || parent->_bf == -1) {
        // 继续向上更新
        cur = parent;
        parent = parent->_parent;
    }
    // else if(...)
}
3.麻烦的情况:插入后parent->_bf == 2或者-2

(此时就超出我们能接受的平衡范围-1、0、1了,此时要进行旋转处理)。

说明插入前parent->_bf等于-1或1(高度差已经是极限了),且cur插入到已经高出一截的那个parent的子节点的下面了。

c++ 复制代码
while (parent) {	// 因为需要向上调整祖先节点的_bf
    // 改变平衡因子
    if (cur == parent->_left) {
        --(parent->_bf);
    }
    else {
        ++(parent->_bf);
    }
    // 分析_bf
    if (parent->_bf == 0) {
        break;
    }
    else if (parent->_bf == 1 || parent->_bf == -1) {
        // 继续向上更新
        cur = parent;
        parent = parent->_parent;
    }
    else if (parent->_bf == 2 || parent->_bf == -2) {
        // 旋转处理
        // if (...) 
        // ......
    }
    else {
        // 插入之前就没处理好
        assert(false);
    }
}

不对呀?那cur和parent不就是爷孙关系了吗?当然没这么简单!此时的情况其实是从情况2向上调整到这的,parent最终停留在了6节点初:

当然,插入的情况并没有这么简单,从插入前就的不平衡分析,情况三又可以分为四种小情况:

(1)极端左[右单旋];(2)极端右[左单旋];(3)左多中高[左右双旋];(4)右多中高[右左双旋]。

h是代表这棵局部子树的高度,可以是任何值。

(1)极端左(parent->_____bf == -2 && cur->_bf == -1):

例如:(只是实例说明一下h可以等于任何值,后几种小情况不会再举这么多例子)

当h=0时,也就是没节点,

当h=1时

当h=2时

(所以当h变多时,只是情况2的循环次数多,最后遇到了这四种(极端左;极端右;左多中高;右多中高)小情况。)

话说回来极端左,该怎么右单旋转成平衡呢?

1.先赋值一些用得到的变量

2.parent与subLR整体下压(改变parent、subL、subLR的父子关系),再改变subL和parent的_bf

3.不要忘了,若parent原本还有他的父节点和其他兄弟的话,还要将parentParent与subL相关联

例如:

4.最后修改parent和subL的平衡因子

c++ 复制代码
else if (parent->_bf == 2 || parent->_bf == -2) {
    // 旋转处理
    if (parent->_bf == -2 && cur->_bf == -1) {
        // 右单旋
        RotateR(parent);
    }
    // else if (...)
    break;    
}

void RotateR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    // 修改局部
    parent->_left = subLR;
    if (subLR) {
        subLR->_parent = parent;
    }
    Node* parentParent = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;

    // 连接整体
    if (parentParent == nullptr) {
        _root = subL;
        subL->_parent = nullptr;
    }
    else {
        if (parent == parentParent->_left) {
            parentParent->_left = subL;
        }
        else {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }
    parent->_bf = subL->_bf = 0;
}
(2)极端右(parent->_____bf == 2 && cur->_bf == 1):

与极端左大致相同,不用多余谈论

c++ 复制代码
else if (parent->_bf == 2 || parent->_bf == -2) {
    // 旋转处理
    if (parent->_bf == -2 && cur->_bf == -1) {
        // 右单旋
        RotateR(parent);
    }
    else if (parent->_bf == 2 && cur->_bf == 1) {
        // 左单旋
        RotateL(parent);
    }
    // else if (...)
    break;
}

void RotateL(Node* parent) {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if (subRL) {
        subRL->_parent = parent;
    }
    Node* parentParent = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (parentParent == nullptr) {
        _root = subR;
        subR->_parent = nullptr;
    }
    else {
        if (parent == parentParent->_left) {
            parentParent->_left = subR;
        }
        else {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
    parent->_bf = subR->_bf = 0;
}
(3)左多中高(parent->_____bf == -2 && cur->_bf == 1):

都是需要进行左右双旋,先左旋后右旋(先左压,后右压)

复用的左单旋,右单旋,所以如果parent之上还有祖先需要连接,复用的单旋函数就帮我们解决了

根据旋转后,调整parent、subL和subLR的平衡因子的情况不同,又分三种子情况:

(左旋右旋,都可以看成是,将插入节点所在的那棵树的左右子树各分一边)

<1>当h=0时(subLR->_bf==0)

此时调整subL->_____bf = subLR->_____bf = parent->_bf = 0

<2>当h不等于0,且新插入的在子树的左边(subLR->_bf==-1)

(h的变化已经分析过,也就是新插入的节点cur通过一层层调整平衡因子,经过情况2的循环遇到了这种需要旋转的情况3)

此时调整subL->_____bf = 0;parent->_____bf = 1;subLR->_____bf = 0;

<3>当h不等于0,且新插入的在子树的右边(subLR->_bf==1)

(旋转都是一样的操作,不再赘述)

旋转后最后调整subL->_____bf = -1;parent->_____bf = 0;subLR->_____bf = 0;

c++ 复制代码
else if (parent->_bf == 2 || parent->_bf == -2) {
    // 旋转处理
    if (parent->_bf == -2 && cur->_bf == -1) {
        // 右单旋
        RotateR(parent);
    }
    else if (parent->_bf == 2 && cur->_bf == 1) {
        // 左单旋
        RotateL(parent);
    }
    else if (parent->_bf == -2 && cur->_bf == 1) {
        // 左右双旋
        RotateLR(parent);
    }
    // else if(...)
    break;
}


void RotateLR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    // 获取subLR的平衡因子,后续判断双旋后的结果
    int bf = subLR->_bf;

    // 可以复用单旋成员函数
    RotateL(parent->_left);
    RotateR(parent);

    // 通过之前获取到的bf,可以推断双旋后的状态,从调整其节点的平衡因子
    if (bf == 0) {
        // <1>
        subL->_bf = subLR->_bf = parent->_bf = 0;
    }
    else if (bf == -1) {
        // <2>
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1) {
        // <3>
        subL->_bf = -1;
        parent->_bf = 0;
        subLR->_bf = 0;
    }
    else {
        assert(false);
    }
}
(4)右多中高(parent->_____bf == 2 && cur->_bf == -1):

都是需要进行右左双旋,先右旋后左旋

与左多中高相同的逻辑

根据旋转后,调整parent、subL和subLR的平衡因子的情况不同,分三种子情况:

(左旋右旋,都可以看成是,将插入节点所在的那棵树的左右子树各分一边)

<1>当h=0时(subLR->_bf==0)

此时调整subR->_____bf = subRL->_____bf = parent->_bf = 0

<2>当h不等于0,且新插入的在子树的左边(subLR->_bf==-1)

此时调整subR->_____bf = -0;parent->_____bf = 0;subRL->_____bf = 0;

<3>当h不等于0,且新插入的在子树的右边(subLR->_bf==1)

此时调整subR->_____bf = 0;parent->_____bf = -1;subRL->_____bf = 0;

(由于旋转逻辑与左多中高相似,就不展示各种例子,只拿<3>举例)

c++ 复制代码
else if (parent->_bf == 2 || parent->_bf == -2) {
    // 旋转处理
    if (parent->_bf == -2 && cur->_bf == -1) {
        // 右单旋
        RotateR(parent);
    }
    else if (parent->_bf == 2 && cur->_bf == 1) {
        // 左单旋
        RotateL(parent);
    }
    else if (parent->_bf == -2 && cur->_bf == 1) {
        // 左右双旋
        RotateLR(parent);
    }
    else if (parent->_bf == 2 && cur->_bf == -1) {
        // 右左双旋
        RotateRL(parent);
    }
    else {
        assert(false);	// 异常情况
    }
    break;
}

void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    int bf = subRL->_bf;

    RotateR(parent->_right);
    RotateL(parent);

    if (bf == 0)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == 1)
    {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;
    }
    else if (bf == -1)
    {
        subR->_bf = 1;
        subRL->_bf = 0;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

查找

十分的模板化

大了往右,小了往左

c++ 复制代码
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur) {
        if (cur->_kv.first < key) {
            cur = cur->_right;
        }
        else if (cur->_kv.first > key) {
            cur = cur->_left;
        }
        else {
            return cur;
        }
    }
    return nullptr;
}

小结

重点在于理解平衡因子的调整和分析,以及旋转的逻辑。

AVL树其他的接口,比如删除,其逻辑也包含旋转之类,暂且不做分析

二、红黑树

特性

红黑树所保持的平衡是指:确保没有一条路径会比其他路径长出2倍

路径:指从该节点开始,直到nullptr的简单路径

规则

  1. 节点分为红色和黑色,一个节点不是红色就是黑色;
  2. 根节点是黑色;
  3. 父子节点不能同时为红色,但可以都是黑色;
  4. 对于任意一个节点,从这个结点开始的所有路径上,都包含相等数量的黑色节点

以下四种都是符合要求的红黑树

原理

红黑树是如何确保没有一条路径会比其他路径长出2倍的?

根据规则4,任何节点之下的路径,黑色节点数量都相等,那么再结合规则3

  • 路径最短时,节点全是黑色,高度为h
  • 路径最长时,节点黑红相间,高度为2h

效率

假设N是树中节点数量,h是最短路径长度,那么2h-1 <= N <= 22h-1,由此h约等于logN,那么最坏情况也就最长路径2 * logN

那么时间复杂度是O(logN).

结构

与AVL树相比,去掉了平衡因子_bf,增加了颜色_____col的属性

c++ 复制代码
// 通过枚举值表示颜色
enum Colour {
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode {
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
	{}
};

template < class K, class V>
class RBTree {
	typedef RBTreeNode<K, V> Node;
public:
	// ......
private:
	Node * _root = nullptr;
};

插入节点

与二叉搜索树同样的方法找到插入位置插入,只不过插入后要分析节点颜色是否符合红黑树的规则

  • 当树原本是空树时,插入的一定就是黑色,因为树的根是要黑色的
  • 当树原本不是空树,插入的一定就是红色,因为一旦新增节点是红色的话,规则4就会被破坏

那么此时可能会想:新节点都是红色的话,那黑色节点哪里来呢?

在插入节点之后的颜色分析中,会对不同的情况,做出不同操作。其中就包括变色以及旋转,红黑树的旋转方法和AVL树一样,只是不会再有平衡因子的操作了

c++ 复制代码
bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;	// 根为黑
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur) {	// 找插入位置
        if ((cur->_kv).first < kv.first) {
            parent = cur;
            cur = cur->_right;
        }
        else if ((cur->_kv).first > kv.first) {
            parent = cur;
            cur = cur->_left;
        }
        else {
            return false;
        }
    }

    cur = new Node(kv);
    cur->_col = RED;	// 新节点必须为红色
    if ((parent->_kv).first > kv.first) {
        parent->_left = cur;
    }
    else {
        parent->_right = cur;
    }
    cur->_parent = parent;

    // *****分析颜色*****
    // 
    // *****************

    // 最后直接将根变为黑色色
    _root->_col = BLACK;
    return true;
}        
颜色分析

对颜色的分析,会用到四个变量:

1.新增节点_cur

2.新节点的父节点_parent

3.父节点的父节点_grandfather(以下描述为grand)

4.新节点的叔叔节点_uncle,也就是父节点的兄弟节点

第一层分析,对父节点分析
  1. 若父节点为黑色,则此时就已经符合红黑树的所有规则了,不需要再分析了。可以类比一下AVL树的第1大类情况(符合规则,跳出循环)

  2. 若父节点为红色,则此时违反了规则3,父子不能同时为红色,需要进入第二层分析

在进行第二层分析之前,还要先分清楚父节点和叔节点是爷节点的左还是右

父左叔右的第二层分析

(父节点已经是红色了)

  1. uncle节点为红。只变色,再向上分析。(可以类比AVL树的第2类情况)
c++ 复制代码
while (parent && parent->_col == RED) {	// 可层层向上调整
    Node* grandfather = parent->_parent;

    if (grandfather->_left == parent) {	// 确定parent与uncle的左右
        Node* uncle = grandfather->_right;

        if (uncle && uncle->_col == RED) {	// 叔叔为红色
            // 只需变色
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            // 继续向上
            cur = grandfather;
            parent = cur->_parent;
        }
        else {
            // ......
        }
    }
    else {
        // 父在右
    }
}
  1. uncle节点为黑或不存在。要进行旋转+变色,再向上分析。但还需要进入第三层分析
父左叔右的第三层分析

此时已经确定:父在左,叔在右,父为红,叔为黑或空

第三层要分cur插入的位置是在parent的左还是右

  1. cur为左孩子(可以类比AVL树的极端左)。右单旋+变色

grand变红,parent变黑

c++ 复制代码
while (parent && parent->_col == RED) {	// 可层层向上调整
    Node* grandfather = parent->_parent;

    if (grandfather->_left == parent) {	// 确定parent与uncle的左右
        Node* uncle = grandfather->_right;

        if (uncle && uncle->_col == RED) {	// 叔叔为红色
            // 只需变色
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            // 继续向上
            cur = grandfather;
            parent = cur->_parent;
        }
        else {	// 叔叔不存在或叔叔为黑
            if (cur == parent->_left) {	// cur为左(极端左)
                // 右单旋 + 变色
                RotateR(grandfather);
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else {
                // ......
            }
            // 父亲已经是黑,跳出循环
            break;
        }
    }
    else {
        // 父在右
    }
}

void RotateR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    // 修改局部
    parent->_left = subLR;
    if (subLR) {
        subLR->_parent = parent;
    }
    Node* parentParent = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;

    // 连接整体
    if (parentParent == nullptr) {
        _root = subL;
        subL->_parent = nullptr;
    }
    else {
        if (parent == parentParent->_left) {
            parentParent->_left = subL;
        }
        else {
            parentParent->_right = subL;
        }
        subL->_parent = parentParent;
    }
}
  1. cur为右孩子(可以类比AVL树的左多中高)。左右双旋+变色

cur变黑,grand变红

c++ 复制代码
while (parent && parent->_col == RED) {	// 可层层向上调整
    Node* grandfather = parent->_parent;

    if (grandfather->_left == parent) {	// 确定parent与uncle的左右
        Node* uncle = grandfather->_right;

        if (uncle && uncle->_col == RED) {	// 叔叔为红色
            // 只需变色
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            // 继续向上
            cur = grandfather;
            parent = cur->_parent;
        }
        else {	// 叔叔不存在或叔叔为黑
            if (cur == parent->_left) {	// cur为左(极端左)
                // 右单旋 + 变色
                RotateR(grandfather);
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else {	// cur为右(左多中高)
                // 双旋 + 变色
                RotateL(parent);
                RotateR(grandfather);
                cur->_col = BLACK;
                grandfather->_col = RED;
            }
            // 父亲已经是黑,跳出循环
            break;
        }
    }
    else {
        // 父在右
    }
}

void RotateL(Node* parent) {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if (subRL) {
        subRL->_parent = parent;
    }
    Node* parentParent = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if (parentParent == nullptr) {
        _root = subR;
        subR->_parent = nullptr;
    }
    else {
        if (parent == parentParent->_left) {
            parentParent->_left = subR;
        }
        else {
            parentParent->_right = subR;
        }
        subR->_parent = parentParent;
    }
}
父右叔左的第二层分析
  1. uncle节点为红。只变色,再向上分析。(可以类比AVL树的第2类情况)

  2. uncle节点为黑或不存在。要进行旋转+变色,再向上分析。但还需要进入第三层分析

父右叔左的第三层分析
  1. cur为右孩子(可以类比AVL树的极端右)。左单旋+变色

grand变红,parent变黑

  1. cur为左孩子(可以类比AVL树的右多中高)。右左双旋+变色

cur变黑,grand变红

(大致逻辑与父左叔右类似,不再举例说明)

c++ 复制代码
else {	//  父右叔左
    Node* uncle = grandfather->_left;

    if (uncle && uncle->_col == RED) {	// 叔叔为红色
        // 只需变色
        parent->_col = uncle->_col = BLACK;
        grandfather->_col = RED;
        // 继续向上
        cur = grandfather;
        parent = cur->_parent;
    }
    else {	// 叔叔不存在或叔叔为黑
        if (cur == parent->_right) {	// cur为右(极端右)
            // 左单旋 + 变色
            RotateL(grandfather);
            parent->_col = BLACK;
            grandfather->_col = RED;
        }
        else {	// cur为左(右多中高)
            // 双旋 + 变色
            RotateR(parent);
            RotateL(grandfather);
            cur->_col = BLACK;
            grandfather->_col = RED;
        }

        // 父亲已经是黑,跳出循环
        break;
    }
}

查找

模板化

c++ 复制代码
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur) {
        if (cur->_kv.first > key) {
            cur = cur->_left;
        }
        else if (cur->_kv.first < key) {
            cur = cur->_right;
        }
        else {
            return cur;
        }
    }
    return false;
}

小结

旋转逻辑与AVL树相同,之不过将平衡的控制权从平衡节点换成了颜色

着重理解旋转加换色的控制魅力

总体

AVL树和红黑树都是基于二叉搜索树衍生出的平衡树,通过一系列的限制规则,努力将增删查改的时间复杂度控制在logN。

这些精妙的设计,每每思考,都让人感叹前人的智慧。

感谢阅读