二叉树2-3-4树红黑树

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

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

假设数组为:{ 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);
    }
}

总结:

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

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

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

 

三、平衡二叉树 2-3树

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

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

如图所示:

左边为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-节点的中间数往上层升级(分左右方向):

 

 

有了上述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树的平衡性还是很好的。

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

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

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

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

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

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

四、红黑树

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

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

红黑树插入操作的几个基本动作:

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

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

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

 

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

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

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

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

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

 

有了以上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)。

 

转载于:https://www.cnblogs.com/leokale-zz/p/11123482.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值