数据结构与算法-学习笔记(18)

253 阅读6分钟

平衡二叉查找树

平衡二叉查找树的严格定义:二叉树中任意一个节点的左右子树的高度相差不能大于1。

设计平衡二叉查找树的初衷:为了解决普通二叉查找树在频繁的插入、删除等动态更新的情况下(尤其是插入的一组数据是有序的情况),出现时间复杂度退化的问题。

但是在实际的开发中,并不能完全严格的按照定义去做,只要保证平衡即可,也就是让整棵树左右看起来比较对称、比较平衡、更新数据时不要造成一边很高、一边又很低的情况。这样就能让整棵树的高度相对来说低一些(如树的高度是对数量级的,不必log2n大很多)。只要能保证这样,就可以说它是一个合格的平衡二叉查找树。

平衡二叉查找树-红黑树(R-B Tree)

红黑树中的节点,一类被标记为黑色,一类被标记为红色。并且要求:

    1. 每个节点不是黑色就是红色,根节点必须是黑色的(可以在节点数据结构中加一个数据字段代表颜色);
    1. 每个叶子节点都是黑色的空节点(nil)(为了保证除了根以外,每个节点都有兄弟节点,也就是每个父节点都有两个叉),也就是说叶子节点不存储数据;
    1. 任何相邻(线连着)的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
    1. 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点(说明黑色节点是要成对出现的,否则失去平衡了)。

为什么说红黑树是“近似平衡的”

近似平衡就是说查找等动态更新的性能不会退化太严重。

二叉查找树很多操作的性能都很树的高度成正比,因此只要证明红黑树的高度能比较稳定的趋近log2n就好。

去掉红色节点的黑色节点(无父节点则祖父节点代替,这样防止原本黑色节点的高度发生改变)可能变成3叉树/4叉树。他们要比相同节点数相同的完全二叉树的高度要小,所以它的高度不超过log2n。

而红色节点必须是被黑色节点隔开的,一红一黑,因此红色节点加回去,树的高度应该不大于2log2n。高度仅是大了一倍,性能上下降不多,可以近似O(logn)。

同时红黑树是近似平衡,因此维护平衡的成本较低,性能比较稳定。对于工程应用来说,要面对各种异常情况,稳定性很重要,因此红黑树的应用范围较广。

动态数据结构

动态数据结构是指支持动态的更新操作,里面存储的数据是时刻在变化的,它支持查询、删除、插入等操作,并且这些操作是非常高效。这样的数据结构才能算作动态数据结构。

如何维护红黑树的近似平衡

红黑树规定插入的节点必须是红色的且在修改过程中不能改变颜色,因为插入红色节点比黑色节点违背规则的可能性更小。插入黑色节点一定会改变黑色高度(违背规则4),而插入红色只有一半机会违背规则3。且3比4更易修正。

当插入一个新节点(二叉查找树插入新节点会插到叶子节点上,那么在红黑树上也就是插入到黑色空节点上层了)就可能会破坏红黑树原本结构,打破平衡,那么如何修正呢?

主要有三种方式:改变节点颜色、左旋、右旋。

  1. 变色 假设原本只有节点E,然后插入了节点A和S,再插入F:

  2. 左旋 通常左旋操作用于将一个向右倾斜的红色链接转为向左链接。

    动图效果:

  3. 左旋

动图效果:

红黑树的操作

  1. 节点数据
typedef struct TreeNode {
    int key;
    struct TreeNode *left;
    struct TreeNode *right;
    struct TreeNode *father; // 双向链表
    BOOL color;
    
} Node;
  1. 左旋的实现

/*
 * 左旋示意图:对节点x进行左旋
 *     p                       p
 *    /                       /
 *   x                       y
 *  / \                     / \
 * lx  y      ----->       x  ry
 *    / \                 / \
 *   ly ry               lx ly
 * 左旋做了三件事:
 * 1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
 * 2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
 * 3. 将y的左子节点设为x,将x的父节点设为y
 */
  1. 右旋的实现
/*
 * 左旋示意图:对节点y进行右旋
 *        p                   p
 *       /                   /
 *      y                   x
 *     / \                 / \
 *    x  ry   ----->      lx  y
 *   / \                     / \
 * lx  rx                   rx ry
 * 右旋做了三件事:
 * 1. 将x的右子节点赋给y的左子节点,并将y赋给x右子节点的父节点(x右子节点非空时)
 * 2. 将y的父节点p(非空时)赋给x的父节点,同时更新p的子节点为x(左或右)
 * 3. 将x的右子节点设为y,将y的父节点设为x
 */
  1. 插入操作
  • 如果第一次插入,原本树是空的,则只需要将它涂黑即可。
  • 如果最终插入后,插入节点的父节点是黑色的,则不违反红黑树规则,不需改变。
  • 如果最终插入后,插入节点的父节点是红色的,就违反规则了,需要修改保持树的平衡了。此时又有三种情况:
    • ①插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;
    • ②插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
    • ③插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。

具体过程为例: 插入新节点4,如图1(也就是情况①)。此时违反3,因此将它的父节点涂黑,但是如果只涂5,就会造成黑色节点不平衡了,违反了4。因此新节点的父节点5和叔叔节点8都要涂黑。这时758都是黑色(黑色太多了?)将祖父7涂红。这时又变成情况②,把7当做新节点 ,以新节点 为支点做左旋操作。如图2->3。此时27节点仍有问题继续修改,此时那2作为新节点,将父节点7涂黑,祖父节点11涂红。在以11为支点右旋。这样整棵树就又平衡。

从上面的步骤可以看出,如果插入数据最终是情况1,则需要走完2、3步骤;如果插入数据出现的是情况2,则只需走完3;插入后是3,则完成3即可了。

总体:变色->左旋->右旋

资料