• 如何生成红黑树?一步一步画给你看(从二叉查找树到2-3树再到红黑树)


    直接进入正题:暂时只讨论了节点的插入,节点删除还未纳入。

    一、如何从数组生成一个二叉查找树

    假设数组为:{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 },我们不对数组排序,直接生成二叉查找树。

    创建流程:

    1.将第一数作为根节点:

    2.插入13,13小于30,放在30的左边子节点。

     

    3.插入7,7小于30,7小于13,放在13的左边子节点。

    4.插入43,43大于30,放在30的右边子节点。

    5.放入23,23小于30,23大于13,放入13的右边子节点。

    6.放入12,12小于30,12小于13,12大于7,放入7的右边子节点。

    7.放入9,9小于30,9小于13,9大于7,9小于12,放入12的左边子节点。

    8.中间省略,最后生成的二叉查找树为如下:

    9.由上图可以看出,普通二叉查找树是不平衡的,最坏的情况可能会形成以下情况:

    在这种情况下,当我们需要查找1的时候,时间复杂度就是O(n)。

    普通二叉查找树的查找时间复杂度为[O(log2n),O(n)]之间。如果让其始终保持为O(log2n)的时间复杂度呢,我们就要创建平衡二叉树。

    2-3树和红黑树都是平衡二叉树,我们先看2-3树,然后再由2-3树引出红黑树的原理。

    二、二叉树的遍历

    二叉树的遍历的几种方式:

    如何理解先序、中序和后序?假设我们有一个一共三个节点的二叉树:

    • 先序遍历:根节点作为第一位,然后左孩子、右孩子。遍历顺序是A->B->C。
    • 中序遍历:先左孩子,根节点作为中间位,然后右孩子。遍历顺序是B->A->C。
    • 后序遍历:先左孩子,再右孩子,最后根节点。遍历顺序是B->C->A。

    总结:遍历的顺序是根据根节点所在的遍历顺序来定义的,除了根节点所处位置的变化,其他子节点遍历顺序是不变的。

    我们以一个简单二叉树作为例子,如图:

    我们根据遍历顺序不同,将各节点的值(圆圈里的字母)打印出来:

    1.前序遍历

    打印结果:A->B->D->G->C->E->F

    前序遍历递归方法的代码如下:

    //前序递归遍历
    void PreOrder(Node * nd) {
        if (nd != NULL) {
            cout << nd->data << endl;
            PreOrder(nd->lchild);
            PreOrder(nd->rchild);
        }
    }

    前序遍历非递归方法的代码如下:

    //先序非递归遍历
    void NonRecPreOrder(Node * nd) {
        //定义一个栈
        stack<Node*> s;
        Node * p = nd;
        //当根节点不为空时,先打印其值(先序)
        while (p != NULL) {
            cout << p->data << endl;
            //该节点被压如栈
            s.push(p);
            //遍历其左子节点(左子树)
            p = p->lchild;
        }
        //所有左子树的左节点都遍历完后,开始通过栈回退到根节点,并开始遍历右子树。
        while (!s.empty()) {
            //取栈顶的节点指针
            p = s.top();
            //取他的右子树,然后重复两个while循环。
            p = p->rchild;
            //弹出
            s.pop();
        }
    }

    2.中序遍历

    打印结果:D->G->B->A->E->C->F

    中序遍历递归方法的代码如下:

    //中序递归遍历
    void MedOrder(Node * nd) {
        if (nd != NULL) {
            PreOrder(nd->lchild);
            cout << nd->data << endl;
            PreOrder(nd->rchild);
        }
    }

    中序遍历非递归方法的代码如下:

    //中序非递归遍历
    void NonRecPreOrder(Node * nd) {
        //定义一个栈
        stack<Node*> s;
        Node * p = nd;
        while(p || !s.empty()){
            //当根节点不为空时,继续往左边走
            while (p != NULL) {
                //该节点被压如栈
                s.push(p);
                //遍历其左子节点(左子树)
                p = p->lchild;
            }
            //所有左子树的左节点都遍历完后,开始通过栈回退到根节点,并开始遍历右子树。
            while (!s.empty()) {
                //取栈顶的节点指针
                p = s.top();
                //中序,打印完左节点值,回来的时候打印根节点值
                cout << p->data << endl;
                //取他的右子树,然后重复两个while循环。
                p = p->rchild;
                //弹出
                s.pop();
            }
        }
    }

    3.后续遍历

    打印结果:G->D->B->E->F->C->A

    后序遍历递归方法的代码如下:

    //后序递归遍历
    void  PostOrder(Node * nd) {
        if (nd != NULL) {
            PreOrder(nd->lchild);
            PreOrder(nd->rchild);
            cout << nd->data << endl;
        }
    }

    后序遍历非递归方法的代码如下:

    //后序非递归遍历
    void NonRecPreOrder(Node * nd) {
        //定义一个栈
        stack<Node*> s;
        Node * p = nd;
        //定义一个指针r,用来记录是否访问过右子节点
        Node * r = NULL;
    
        while (p || !s.empty()) {
            if (p) {
                s.push(p);
                p = p->lchild;
            }
            else {
                //获取栈顶部元素,注意不是弹出
                p = s.top();
                //判断右子节点的状态,1.为NULL,2.已经被访问
                if (p->rchild && p->rchild != r) {
                    p = p->rchild;
                }
                else {
                    //如果右子节点不存在,或已经被访问了(r已经记录),打印子树根节点值
                    cout << p->data << endl;
                    s.pop();
                    r = p; //将辅助指针指向访问过的右子树根节点
                    p = NULL;    //将p置为空,从而继续访问栈顶
                }
            }
        }
    }

    4.层次遍历

    层次遍历也是非递归的一种遍历方法,遍历的结果是非有序的,顺序为A->B->C->D->E->F,即从上到下一层一层遍历。代码如下:

    //层次遍历(非递归)
    void LevelOrder(Node * nd) {
        queue<Node *> q;
        Node * p = nd;
        //先将根节点入队列
        q.push(p);
        while (!q.empty()) {
            //从队列中获取最先压如的节点
            p = q.front();
            //打印值
            cout << p->data << endl;
            //弹出
            q.pop();
            //判断该节点是否存在左右子节点,如果存在,就压入队列
            if (p->lchild != NULL) q.push(p->lchild);
            if (p->rchild != NULL) q.push(p->rchild);
        }
    }

    5.总结

    前序遍历、中序遍历、后序遍历的递归写法很类似,唯一的不同就是遍历左子树、遍历右子树、打印当前节点值三者之间的顺序不同。 

    非递归遍历方法,特别需要注意后序遍历,需要依靠一个辅助指针记录右节点是否已经访问。

    层次遍历需要借助队列来实现,即每一层在遍历的时候,就检查其下一层元素是否存在,存在的话压入队列,这样就能实现一层一层遍历的结果。

    三、平衡二叉树 2-3树

    1.什么是2-3树

    2-3树的意思就是,某个节点有两种可能:

    一是正常的2-节点,包含一个值(或键),包含左右两个子节点。二是3-节点,包含两个值,包含左中右三个子节点。

    如图所示:

    左边为2-节点,右边为3-节点。

    2.2-3树插入基本操作

    向一个2-3树的节点中插入一个新的元素有以下几种基础操作:

    1.插入一个比2-节点值小的元素,例如对值为30的2-节点插入20:

    2.插入一个比2-节点值大的元素,例如对值为30的2-节点插入40:

     

    3.插入一个比3-节点左边值更小的值,例如对值为20 30的3-节点插入15:

    注意,这里对3-节点插入数据后,形成了一个4-节点,可以分解为最右边的二叉子树。

    4.插入一个比3-节点左边值更大、比右边值更小的值,例如对值为20 30的3-节点插入25:

    5.插入一个比3-节点右边值更大的值,例如对值为20 30的3-节点插入40:

     

    6.当下面一层的元素形成了4-节点,将4-节点的中间数往上层升级(分左右方向):

    3.创建2-3树流程

    有了上述6个基本操作,我们开始使用前面的数组来创建2-3树:

    数组为{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 }。

    创建流程:

    1.将30作为根节点。

    2.插入13,13比30小,形成一个值为13 30的3-节点。

    3.插入7,7比13小,形成一个值为7 13 30的4-节点,然后分解。

    4.插入43,43大于13,43大于30,与30一起形成3-节点。

    5.插入23,23大于13,23小于30,与30 43形成4-节点,然后分解。

    6.插入12,12小于13,12大于7,与7组成3-节点

     

    7.插入9,9小于13,9大于7,9小于12,与7 12组成4-节点,然后分解。分解后9升级到上一层,与13 30形成4-节点,再次分解。

    8.插入33,33大于13,33大于30,33小于43,与43组成3-节点。

    9.插入42,42大于13,42大于30,42大于33,42小于43,与33 43形成4-节点,然后分解。

    10.省略后面过程,最终生成结果为:

    至此,我们创建了一颗2-3树,可以看出2-3树的平衡性还是很好的。

    4.将2-3树转换为红黑树

    得到2-3树以后,我们可以将其进行结构上的一些变化:

    1.将其中的所有3-节点,变换为以下形状,以左边子树为例:

    2.将所有的3-节点进行变换:

    3.得到的就是一颗红黑树,所以2-3树和红黑树是可以一一对应的,但是需满足三个条件

    • 红链接均为左链接。
    • 没有任何一个节点同时和两条红链接相连。
    • 任意空链接到根节点路径上的黑色连接数目相同。

     从图中可以看出,我们的红连接都是左链接,满足条件一。没有节点同时链接两条红线,满足条件二。每个叶子节点下得空链接到根节点30的路径中黑连接数量都是2,所以满足条件三。

    四、红黑树

    红黑树的红和黑主要指红黑连接,而不是指节点是红色和黑色,但是一般可以将红连接下面的子节点图为红色。

    我们先将上面的2-3树转换为真正的红黑树:

    1.红黑树插入的基本动作

    注意:新插入的节点都使用红线连接,我们将节点也表示为红色,方便观察。

    1.向一个黑色节点插入一个元素比他小的元素:

    2.向一个黑色节点插入一个元素比他大的元素,要进行一次左旋:

    3.向一个已经有一个子节点(红色)的黑色节点插入一个大于他的元素:

    在这种情况下,节点6同时存在左右两条红色连接,那么直接将其都变为黑色。

    如果这个黑色节点有父节点,则需要将与父节点之间的连接也变为红色,自己变为红色:

    4.向一个已经有一个子节点(红色)的黑色节点插入一个小于红色子节点的元素:

    5.向一个已经有一个子节点(红色)的黑色节点插入一个大于红色子节点的元素:

    2.创建红黑树流程

    有了以上5个基本动作,我们就可以开始构建一颗红黑树了:

    数组为{ 30, 13, 7, 43, 23, 12, 9, 33, 42, 21, 18, 6, 3, 50 }。

    创建流程:

    1.将第一个数30作为根节点:

    只有一个节点,直接变黑。

    2.插入13,13小于30,放在左边子节点,颜色为红色,连接为红色:

    3.插入7,7小于30,7小于13,放在13的左边子节点,红色。然后右旋,再进行变色:

    4.插入43,43大于13,43大于30,放在30的右边子节点,然后局部左旋:

    5.插入23,23大于13,23小于43,23小于30,放在30的左边子节点,红色,然后右旋,变色,再左旋:

    6.插入12,12小于30,12小于13,12大于7,放在7的右边子节点,然后左旋:

    7.插入9,9小于30,9小于13,9小于12,9大于7,放在7的右边,左旋,右旋,变色,右旋,变色:

    8.插入33,33大于13,33大于30,33小于43,放在43的左边子节点:

    9.插入42,42大于13,42大于30,42小于43,42大于33,放在33右边子节点,左旋,右旋,变色,左旋:

    10.插入21,21大于13,21小于42,,21小于30,21小于23,放在23左边子节点:

    11.插入18,18大于13,18小于42,18小于30,18小于23,18小于21,放在21左边子节点,右旋,变色,右旋,变色,左旋:

    12.插入6,6小于30,6小于13,6小于9,6小于7,放在7的左边子节点:

    13.插入3,3小于30,3小于13,3小于9,3小于7,3小于6,放在6的左边子节点,右旋,变色:

    14.插入50,50大于30,,50大于42,50大于43,放在43的右边子节点,左旋:

    15.红黑树生成完毕,检查一下几个点:

    • 每个节点不存在同时链接两条红线。
    • 所有红线都是左连接。
    • 不存在两条连续的红连接。
    • 每个叶节点的空连接到根节点经过的黑连接数量相同(这里都是2)。
  • 相关阅读:
    Atitit.eclise的ide特性abt 编译
    Atitit python3.0 3.3 3.5 3.6 新特性 Python2.7新特性1Python 3_x 新特性1python3.4新特性1python3.5新特性1值得关注的新特性1Pyth
    Atitit. Atiposter 发帖机 新特性 poster new feature   v7 q39
    Atitit.eclipse 4.3 4.4  4.5 4.6新特性
    atitit.错误:找不到或无法加载主类 的解决 v4 qa15.doc
    Atitit RSA非对称加密原理与解决方案
    Atitti.数字证书体系cer pfx attilax总结
    Atitit ftp原理与解决方案
    Atitit qzone qq空间博客自动点赞与评论工具的设计与实现
    Atitit 软件国际化原理与概论
  • 原文地址:https://www.cnblogs.com/leokale-zz/p/11123482.html
Copyright © 2020-2023  润新知