通俗易懂的红黑树图解(下)

栏目: IT技术 · 发布时间: 4年前

内容简介:回顾一下红黑树删除操作包括两部分,一是查找到删除节点,二是删除节点以及删除之后的自平衡。查找节点与二叉树的查找方式一样。而删除操作,当删除节点不存在时,结束本次删除操作;当删除节点存在时,删除节点,然后找到一个节点替换已删除的节点位置,重新连接上已删除节点的父节点与孩子节点。如下图,删除节点 D ,需要找到一个节点可以替换到 D 节点位置,否则节点 P 和节点 L 及 R 之间的链接会断开,破坏了红黑树的性质,形成独立的树形结构。

前言

回顾一下 通俗易懂的红黑树图解(上) ,上篇首先介绍了二叉树的定义以及二叉树的查找,然后介绍了红黑树的五点性质以及红黑树的变色、左旋以及右旋等操作,最后结合变色、左旋及右旋详细讲解了插入节点的五种场景。而本篇通俗易懂的红黑树图解(下)是在上篇的基础上讲解红黑树最后一种操作-删除节点,删除节点相对插入节点会复杂一点,但通过分类归纳出不同的场景,能更容易理解和记忆。

○ 红黑树删除

红黑树删除操作包括两部分,一是查找到删除节点,二是删除节点以及删除之后的自平衡。查找节点与二叉树的查找方式一样。而删除操作,当删除节点不存在时,结束本次删除操作;当删除节点存在时,删除节点,然后找到一个节点替换已删除的节点位置,重新连接上已删除节点的父节点与孩子节点。

如下图,删除节点 D ,需要找到一个节点可以替换到 D 节点位置,否则节点 P 和节点 L 及 R 之间的链接会断开,破坏了红黑树的性质,形成独立的树形结构。

通俗易懂的红黑树图解(下)

关键字: 查找节点 替换节点

○ 查找节点

查找删除节点与 二叉树查找 节点 逻辑相同,通过与当前节点值比较,返回当前节点或者继续从左子树或者右子树继续查找。

在二叉查找树中查找节点 N ,首先从根节点开始,将根节点设置为当前节点,若当前节点为空,则查找失败,若 N 与当前节点值相等,返回当前节点,若 N 大于当前节点值,则从当前节点的右子节点开始查找,否则从当前节点的左子节点开始查找,直到返回目标节点或者查找失败;

通俗易懂的红黑树图解(下)

○ 替换节点

回顾一下二叉查找树的性质:

  • 若任意节点左子树不为空,它的左子树上所有节点值均小于它的根节点的值
  • 若任意节点的右子树不为空,它的右子树上所有节点的值均大于它的根节点的值

根据二叉查找树的性质,删除节点之后,可以找到两个替换节点,即可以用左子树中的最大值以及右子树中的最小值来替换删除节点。

删除节点找替换节点又分三种情景:

  • 情景1:删除节点无子节点,可以直接删除,无需替换
  • 情景2:删除节点只有一个子节点,用子结点替换删除节点
  • 情景3:删除节点有两个子节点,可以用后继节点或者前继节点替换删除节点。本文采用前者,即后继节点替换删除节点

    后继节点:删除节点的右子树中的最小节点,即右子树中最左节点。

    前继节点:删除节点的左子树中最大节点,即左子树中最右节点。

综上所述,寻找一个节点替换已删除节点位置,在不考虑节点值情况下,可等同于 删除替换节点

○ 节点删除

删除节点可等同于删除替换节点,所以节点删除就转换到了替换节点的各种场景。节点删除又分 9 种场景,在如下的描述场景中,场景 2 中的四种情况与场景 3 中的四种情况分别互为镜像,可参照对比着看。

  • 删除场景1:替换节点是红色节点

    即替换的节点是红色节点,删除之后不影响红黑树的平衡,只需要把替换节点的颜色设成被删除节点的颜色即可重新平衡。

处理: 删除节点D,查找到替换节点R,R设成D节点的颜色,再替换D节点位置。

通俗易懂的红黑树图解(下)

  • 删除场景 2:替换节点是黑色节点、且是其父节点的左子节点

    替换节点是黑色节点时,删除之后破坏了红黑树的平衡,需要考虑自平衡处理。而此又细分为 4 种场景。

  • 场景 2.1:替换节点的兄弟节点是红色。删除黑色节点,左子树中黑色节点数减少一个,可以通过一些操作,达到间接借用红色的兄弟节点来补充左子树中黑色节点数。

    处理:替换节点的父节点 P 设置红色、兄弟节点 S 设置成黑色,再对节点 P 左旋操作,变成场景 2.4。

    通俗易懂的红黑树图解(下)

  • 场景2.2:替换节点的兄弟节点是黑色且兄弟节点的右子节点是红色、左子节点任意颜色。同样是间接借用兄弟节点的红色右子节点补充到左子树中,达到红黑树的平衡。

    处理:替换节点的兄弟节点 S 设置成父节点P的颜色,兄弟节点的右子节点 SR 设置为黑色,父节点P设置为黑色,再对节点 P 左旋操作。此时节点R替换到删除节点位置之后,红黑树重新达到平衡状态。

    通俗易懂的红黑树图解(下)

    • 场景2.3:替换节点的兄弟节点是黑色且兄弟节点的左子节点是红色,右子节点是黑色。

    处理:替换节点的兄弟节点 S 设置成红色,兄弟节点的左子节点 SL 设置为黑色,再对节点 S 右旋操作,转换到了场景 2.2,再进行场景 2.2 的操作。

    通俗易懂的红黑树图解(下)

    • 场景2.4:替换节点的兄弟节点的左右子节点都是黑色。兄弟节点的子节点不能借用,就只能借用兄弟节点了。

    处理:替换节点的兄弟节点 S 设置成红色,以父节点 P 当作替换节点,然后自底向上处理。

    通俗易懂的红黑树图解(下)

  • 场景3:替换节点是黑色节点、且是其父节点的右子节点。(与场景 2 镜像)
  • 场景3.1:替换节点的兄弟节点是红色。

    处理:替换节点的父节点 P 设置红色、兄弟节点 S 设置成黑色,再对节点 P 右旋操作,变成场景3.4。

    通俗易懂的红黑树图解(下)

  • 场景3.2:替换节点的兄弟节点是黑色且兄弟节点的左子节点是红色、右子节点任意颜色。

    处理:替换节点的兄弟节点 S 设置成父节点 P 的颜色,兄弟节点的左子节点 SL 设置为黑色,父节点P 设置为黑色,再对节点 P 右旋操作。此时节点 R 替换到删除节点位置之后,红黑树重新达到平衡状态。

    通俗易懂的红黑树图解(下)

  • 场景3.3:替换节点的兄弟节点是黑色且兄弟节点的右子节点是红色、左子节点为黑色。

    处理:替换节点的兄弟节点 S 设置成红色,兄弟节点的右子节点 SL 设置为黑色,再对节点S左旋操作,转换到了场景 3.2,再进行场景 3.2 的操作。

    通俗易懂的红黑树图解(下)

  • 场景3.4:替换节点的兄弟节点的左右子节点都是黑色。

    处理:替换节点的兄弟节点 S 设置成红色,以父节点 P 当作替换节点,然后自底向上处理。

    通俗易懂的红黑树图解(下)

节点删除及平衡代码:

/**
  * 查找节点 
  * @param key 节点key值
  */
search(key) {
  let node = this.root
  while (node) {
    if (key < node.key) {
      node = node.left
    } else if (key > node.key) {
      node = node.right
    } else if (key === node.key) {
      break
    }
  }
  return node
}

/**
 * 替换u节点,重置v节点
 * @param u 待删除节点
 * @param v 子节点
 */
const replace = function(u, v) {
  if(!u.parent){
    // u是根节点,设置v为根节点
    this.root = v
  } else if(u === u.parent.left){
    // 重置u的父节点的左节点
    u.parent.left = v
  } else {
    // 重置u的父节点的右节点
    u.parent.right = v
  }
  // 重置v的父节点
  v.parent = u.parent
}

 /**
  * 查找node节点的后继节点
  */
  findSuccessor(node) {
    while (node.left) {
      node = node.left;
    }
    return node;
  }

/**
 * 删除节点
 * @param key 删除节点key值
 */
delete(key) {
  const node = search(key)
  if(!node){
    return
  }
  let fix
  let color = node.color
  if(!node.left){
    //左节点为空值
    fix = node.right
    this.replace(node, node.right)
  } else if(!node.right){
    //右节点为空值
    fix = node.left
    this.replace(node, node.left)
  } else {
    // 左右节点都不为空值
    const successor = this.findSuccessor(node.right)
    //替换节点的颜色
    color = successor.color
    //后继节点只存在右节点或者两个nil子节点情况
    fix = successor.right
    //如果后继节点是父节点的非直接子节点
    if(successor.parent !== node){
      this.replace(successor, successor.right)
      successor.right = node.right
      successor.right.parent = successor
    }
    this.replace(node, successor)
    successor.color = node.color
    successor.left = node.left
    successor.left.parent = successor
  }
  if(color === Color.BLACK){
    this.balanceDeletion(fix)
  }
}
/**
 * 删除节点平衡修正
 * @param node 节点
 */
  balanceDeletion(node) {
    while (node !== this.root && node.color === Color.BLACK) {
      // 节点是父节点的左子节点
      if (node === node.parent.left) {
        //兄弟节点
        let sibling = node.parent.right;
        if (sibling.color === Color.RED) {
          // 场景2.1:兄弟节点是红色
          // 兄弟节点设置为黑色
          sibling.color = Color.BLACK;
          //替换节点的父节点设置为红色
          node.parent.color = Color.RED;
          // 左旋
          this.rotateLeft(node.parent);
          sibling = node.parent.right;
        }
        if (sibling.left.color === Color.BLACK && sibling.right.color === Color.BLACK) {
          // 场景2.4: 兄弟节点两个子节点都是黑色
          sibling.color = Color.RED;
          //再次以父节点为新节点作自平衡处理。
          node = node.parent;
          continue;
        } else if (sibling.left.color === Color.RED) {
          // 场景2.3: 兄弟节点的左子节点是黑色,转换到场景2.2.
          sibling.left.color = Color.BLACK;
          sibling.color = Color.RED;
          //对兄弟节点右旋
          this.rotateRight(sibling)
          sibling = node.parent.right;
        }
        if (sibling.right.color === Color.RED) {
          //场景2.2:兄弟节点的右节点是红色
          sibling.color = node.parent.color;
          node.parent.color = Color.BLACK;
          sibling.right.color = Color.BLACK;
          //对父节点左旋
          this.rotateLeft(node.parent);
          // 左旋之后,红黑树重新平衡
          node = this.root;
        }
      } else {
        //节点是父节点的左节点
        let sibling = node.parent.left;
        if (sibling.color === Color.RED) {
          // 场景 3.1:替换节点的史弟节点是红色
          sibling.color = Color.BLACK;
          node.parent.color = Color.RED;
          this.rotateRight(node.parent);
          sibling = node.parent.left;
        }
        if (sibling.right.color === Color.BLACK && sibling.left.color === Color.BLACK) {
          //场景3.4:替换节点的两个子节点都是黑色
          sibling.color = Color.RED;
          //再次以父节点为新节点作自平衡处理。
          node = node.parent;
          continue
        } else if (sibling.right.color === Color.RED) {
          // 场景3.3:兄弟节点的右子节点是红色
          sibling.right.color = Color.BLACK;
          sibling.color = Color.RED;
          this.rotateLeft(sibling);
          sibling = node.parent.left;
        }
        if (sibling.left.color === Color.RED) {
          // 场景3.2:兄弟节点的左子节点是红色
          sibling.color = node.parent.color;
          node.parent.color = Color.BLACK;
          sibling.left.color = Color.BLACK;
          this.rotateRight(node.parent);
          node = this.root;
        }
      }
    }
    node.color = Color.BLACK;
  }
}

红黑树应用

红黑树广泛用在 Java 的集合框架 (HashMap、TreeMap、TreeSet)、Nginx 的 Timer 管理、 Linux 虚拟内存管理以及 C++ 的 STL 等等场景。

在Linux内核中,每个用户进程都可以访问4GB的线性虚拟空间,虚拟空间往往需要多个虚拟内存区域描述,对这些内存区域,Linux内核采用了链表以及红黑树形式组织。内存区域按地址排序,链接成一个链表以及一颗红黑树,寻找空闲区域时只需要遍历这个链表,在发生缺页中断时通过红黑树快速检索特定内存区域。

总结

红黑树的删除操作就基本介绍完了,总结一下删除操作就是,删除节点等同于删除替换节点,若替换节点是红色节点时,直接删除不会影响平衡;若替换节点是黑色节点时,就需要借用兄弟节点的右子节点、左子节点或者兄弟节点。

红黑树最吸引人的是它的所有操作在最差情况下可以保证 O(logN) 的时间复杂度,稳定且高效。例如要在10 万条(2^20)数据中查找一条数据,只需要 20 次的操作就能完成。但这些保证有一个前置条件,就是数据量不大,且数据可以完全放到内存中。在数据量比较大时,因为红黑树的深度比较大造成磁盘 IO 的频繁读写,会导致它的效率低下。 另外推荐 Data Structure Visualizations 网站,它包含非常多的数据结构方面的可视化算法题。其中就有 红黑树的算法 ,对照着在线生成的红黑树看,会更容易理解红黑树中各种操作场景。

❉ 作者介绍 ❉

通俗易懂的红黑树图解(下)

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JAVA 2核心技术 卷Ⅰ

JAVA 2核心技术 卷Ⅰ

[美] 霍斯特曼、[美] 科奈尔 / 叶乃文、邝劲筠 等 / 机械工业出版社 / 2006-5 / 88.00元

本书是Java技术经典参考书,多年畅销不衰,第7版在保留以前版本风格的基础上,涵盖Java2开发平台标准版J2SE5.0的基础知识,主要内容包括面各对象程序设计、反射与代理、接口与内部类、事件监听器模型、使用Swing UI工具箱进行图形用户界面设计,异常处理、流输入/输出和对象序列化、泛型程序设计等。 本书内容翔实、深入浅出,附有大量程序实例,极具实用价值,是Java初学者和Java程序员......一起来看看 《JAVA 2核心技术 卷Ⅰ》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试