红黑树的插入详解-附源码

278 阅读9分钟

该文章内容及代码都是根据自己从网上看过的资料,然后根据自己所理解的思路和想法写出来的,有不对的地方还请指正,该文章是根据标准的二叉搜索树进行讲解的,也就是BST树,不了解的同学可以网上找资料了解一下,或者在文章下留言,想了解的人多的话会出一篇文章进行讲解,感谢!

红黑树的由来

我们都知道,二叉搜索树可以快速的查找到某一数据项,然后进行删除和插入操作,但是对于某种特殊的情况来讲,二叉搜索树的查找效率就没有那么高了,比如我们在插入一下有序的数据时,可能就会出现下面的情况

链表式的二叉树.png

这个数据结构大家看起来会不会觉得眼熟,没错,如果将图片进行一定角度的翻转,我们会发现,这个跟结构跟链表一模一样,也就是说,BST在一定的情况下,会转变成链表,链表的查找效率是O(N),而正常的BST的查找效率是接近O(logN),这两者的效率明显是有一定的差距的,这种树结构也有另一个名字,叫做不平衡树,效率为O(N),所以,这个时候红黑树的出现,就是为了解决这个补平衡树的问题

红黑树的认识

我们上面说了,不平衡树的其中一种解决方案就是红黑树,还有另一种是AVL树,这里我们主要讲解红黑树,当然,红黑树不是保证百分百平衡,而是保证大部分是平衡的,所以红黑树的查找效率也是接近O(logN)的

红黑树的规则

红黑树除了符合BST规则之外,还多添加了几项规则

1. 节点只能是红色或者黑色

这个比较容易理解,就是我们所插入的节点多增加了一个颜色属性,来描述当前节点的颜色

屏幕截图 2022-05-11 211452.png

2. 根节点是黑色

这个也比较容易理解,就是我们的根节点它的颜色只能是黑色

3. 每个叶子节点都是黑色空节点(Nil节点)

意思就是说在红黑树里面,叶子节点的必须是黑色的空节点,如下图

屏幕截图 2022-05-11 211722.png

4. 从叶子到根路径不能出现两个连续的红色节点,也就是说红色节点的子节点只能是黑色节点

在红黑树里面,在任一路径上,是不能出现连个相连的红色节点的

5. 任一节点到其叶子节点路径上的黑色节点数目都相同

在红黑树里面,我们从任一节点到其叶子节点路径上出现的黑色节点数目都是相同的,拿下图进行解释,比如我们从节点7到任一叶子节点的路径上的黑色节点数目都是一样的 7-5-6-N,7-5-2-N,7-5-4-N,7-9-8-N,7-9-10-N

屏幕截图 2022-05-11 221221.png

红黑树是如何维持树的平衡的

当我们将一个新节点插入的时候,难免会造成树的不平衡性,所以,红黑树为了保持树的平衡性,主要有三个操作方式,分别是变色、左旋转、右旋转,下面会对这三种保持平衡的操作进行详细的介绍

红黑树的插入补充

当我们在对红黑树进行插入操作的时候,都会默认给插入的节点变为红色节点,因为这样可以保证我们在执行一次插入操作的时候可以避免违反红黑树的规则

比如我们再插入一个节点11,如果这个时候我们将这个节点的颜色默认设置为黑色,那么问题就出现了,因为该条路径上的黑色节点与其他路径上的黑色节点数目不一致,违反了红黑树的规则五 屏幕截图 2022-05-11 222700.png 如果我们将节点11的颜色默认设置为红色,那么就不会有问题了,任一节点到其叶子节点的黑色节点数目都是一致的,这就是为什么在插入节点的时候默认设置颜色为红色的原因之一

屏幕截图 2022-05-11 222737.png 但是如果插入红色的节点,就有可能会出现两个红色节点相连的情况,违反了红黑树的规则四,这个时候就需要对红黑树进行变色,左右旋转了

红黑树的旋转

在真正的讲解红黑树的插入操作之前,我们先来了解一下它的旋转操作,方便理解后面的插入操作

首先是右旋转,如下图所示,将图中的节点进行右旋转,我将旋转的过程拆分成几个步骤,首先我们以A节点为轴,进行右旋转

屏幕截图 2022-05-12 002158.png 接着就是左旋转,将图中的节点进行左旋转,分成几个步骤,还是以A节点为轴,进行左旋转

屏幕截图 2022-05-12 002844.png 在这里多加两张旋转的动态图,方便理解

v2-665f59c694f38e71c8c7fddb7a4cb183_b.gif

20181125155223734.gif

红黑树的插入

在进行插入操作之前我们需要给一些节点给予特定的名称,例如当前插入的节点叫当前节点,用N来表示,N的父节点用P来表示,P的兄弟节点,也就是N的叔叔节点用U来表示,N的爷爷节点,也就是P的父亲节点用G来表示,方便我们后续的讲解

屏幕截图 2022-05-12 110520.png

我们在红黑树插入新节点的时候,可能会遇到几种情况,一般分为五种情况,这五种情况都有对应的解决方法,接下来就看一下这五种情况分别是什么

情况一

红黑树里没有任何的元素,新插入的节点直接作为红黑树的根,且没有父节点,但是我们新插入的节点颜色默认是为红色,但是这样就违反了红黑树的规则二,根节点必须是黑色,这个时候我们只需要对该节点进行变色操作即可,将该节点变成黑色节点,具体代码如下

// 节点类
class Node {
  constructor(key){
    this.left = null
    this.key = key
    this.color = 'red'
    this.right = null
    this.parent = null
  }
}

// 黑色空节点类
class Nil {
  constructor(){
    this.left = null
    this.key = 'Nil'
    this.color = 'black'
    this.right = null
  }
}
// 红黑树
class RedBlackTree {
  constructor(){
    this.root = null
    this.Nil = new Nil()
  }

  // 插入操作
  insert(key){
    // 创建新的节点对象
    const node = new Node(key)
    // 情况一:如果根节点为空
    if(this.root === null){
      // 则新节点直接插入即可,并且将颜色改为黑色
      this.root = node
      node.color = 'black'
    }
    // 将节点的子节点设置为黑色的空节点Nil
    if(node.left === null){
      node.left = this.Nil
    }
    if(node.right === null){
      node.right = this.Nil
    }
    // 执行完插入操作后将根节点颜色设置为黑色
    this.root.color = 'black'
  }

情况二

插入新节点的父节点颜色为黑色,这个时候直接插入即可,但是需要考虑一种情况,就是父节点的子节点为Nil节点,且为根节点,则直接将新节点插入,并且将新节点的父节点属性指向它即可,如果不是则循环查找对应的G、P、U节点,查找后的P节点为黑色,且子节点为Nil就直接插入

insertNode(oldNode,newNode){
    let G = oldNode
    let P = oldNode
    let U = oldNode
    // 定义一个变量,判断是否是左子节点
    let isLifeChild = true
    // 如果新节点小于旧节点
    if(newNode.key < oldNode.key){
        // 且旧节点左节点为Nil,并且为根节点
      if(oldNode.left.key === 'Nil' && oldNode === this.root){
        oldNode.left = newNode
        newNode.parent = oldNode
      }else{
          // 循环找出G、P、U节点
        while(P.key !== 'Nil'){
          if(newNode.key < P.key){
            isLifeChild = true
            if(P.left.key === 'Nil') break;
            G = P
            P = G.left
            U = G.right
          }else {
            isLifeChild = false
            if(P.right.key === 'Nil') break;
            G = P
            P = G.right
            U = G.left
          }
        }
        // 找出G、P、U节点后就进行判断,如果P节点颜色为黑色,则直接插入
        if(P.color === 'black'){
            // 判断新节点是否是左子节点
          if(isLifeChild){
            if(P.left.key === 'Nil'){
              P.left = newNode
              // 设置新节点的父节点为P
              newNode.parent = P
            }
          }else {
            if(P.right.key === 'Nil'){
              P.right = newNode
              newNode.parent = P
            }
          }
        }
      }
    }
  }

情况三

P节点为红色,U节点为红色,G节点为黑色,这种情况我们需要进行变色处理,将P、U节点变成黑色,G节点变成红色即可

屏幕截图 2022-05-12 131539.png 但是还有一种特殊的情况就是,G节点不是根节点的情况,这种情况下,我们G节点变成红色,假如他的父节点也是红色,那么就出现红红相连的情况,这个时候就需要再做一步操作,就是将G节点以及它的子节点当成一个节点,再次插入到原来的位置即可

屏幕截图 2022-05-12 132228.png 对应的代码如下

 // 本段代码接上一段代码
 // 父红叔红祖黑
else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && isLifeChild){
  // 进行变色和指向操作
  G.color = 'red'
  P.color = 'black'
  U.color = 'black'
  P.left = newNode
  newNode.parent = P
  if(G === this.root){
    G.color = 'black'
  }else {
    // 变色操作做完可能会出现红红相连的情况
    // 所以将G的父节点的left节点指向Nil节点
    // 然后再将G节点当成一个整体进行递归调用重新插入即可
    G.parent.left = this.Nil
    this.insertNode(this.root,G)
  }
}else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && !isLifeChild){
  G.color = 'red'
  P.color = 'black'
  U.color = 'black'
  P.right = newNode
  newNode.parent = P
  if(G === this.root){
    G.color = 'black'
  }else {
    G.parent.left = this.Nil
    this.insertNode(this.root,G)
  }
}

情况四

P节点为红色,U节点为黑色,G节点为黑色,且新插入的节点是左子节点,这种情况需要先将P节点变成黑色,G节点变成红色,然后进行右旋转 (在根节点的左边,如果是在右边,则进行左旋转),以G节点为轴

屏幕截图 2022-05-12 134122.png 代码如下

// 本段代码接上一段代码
else if(P.color === 'red' && U.color === 'black' && G.color === 'black' && isLifeChild){
  // 右旋转操作
  P.color = 'black'
  G.color = 'red'
  // 相当于剩下的节点进行右移操作
  G.left = P.right
  // 相当于P节点上移,然后右节点指向G节点,成为G节点的父节点
  P.right = G
  // G.parent = P
  if(G === this.root){
    P.parent = null
    this.root = P
  }else {
    // 节点父属性的重新指向
    G.parent.left = P
    P.parent = G.parent
  }
  // 新节点插入以及G节点的父属性重新指向
  G.parent = P
  P.left = newNode
  newNode.parent = P
}

情况五

P节点为红色,U节点为黑色,G节点为黑色,且新插入的节点是右子节点,这种情况就比较复杂,需要进行两步操作,需要先以P节点为轴,进行左旋转,然后将P节点当成新的节点插入重新考虑情况即可

屏幕截图 2022-05-12 134614.png 代码如下

// 父红叔黑,且新插入的节点为右子节点
else if(P.color === 'red' && U.color === 'black' && !isLifeChild){
  console.log(newNode.key);
  // 进行左旋转操作,以P节点为轴
  P.parent.left = newNode
  newNode.parent = P.parent
  newNode.left = P
  P.parent = newNode
  newNode.left = this.Nil
  if(!newNode.right) newNode.right = this.Nil
  // 旋转后将P节点当成一个整体重新插入考虑情况即可
  this.insertNode(this.root,P)
}

插入测试

红黑树的插入情况就已经讲解完了,现在我们来测试一下几种情况

插入1,2,3,4,5,6,7,8,9,10

树结构如下

微信图片_20220512144210.png 代码测试

const redBlackTree = new RedBlackTree()
redBlackTree.insert(1)
redBlackTree.insert(2)
redBlackTree.insert(3)
redBlackTree.insert(4)
redBlackTree.insert(5)
redBlackTree.insert(6)
redBlackTree.insert(7)
redBlackTree.insert(8)
redBlackTree.insert(9)
redBlackTree.insert(10)

// 在类型添加一个获取根节点的方法,直接返回根节点
console.log(redBlackTree.getRoot())

打印结果如下,可以看到4节点的左节点为2,右节点为6

屏幕截图 2022-05-12 144543.png 2节点的左节点为1,右节点为3

Snipaste_2022-05-12_14-47-35.png 6的左节点为5,右节点为8

Snipaste_2022-05-12_14-49-11.png 8的左节点为7,右节点为9

Snipaste_2022-05-12_14-50-03.png 9的左节点为Nil节点,右节点为10节点

Snipaste_2022-05-12_14-50-46.png

插入10,7,18,21,2,20,4,1

树结构如下

屏幕截图 2022-05-12 145237.png

代码测试

const redBlackTree = new RedBlackTree()
redBlackTree.insert(10)
redBlackTree.insert(7)
redBlackTree.insert(18)
redBlackTree.insert(21)
redBlackTree.insert(2)
redBlackTree.insert(20)
redBlackTree.insert(4)
redBlackTree.insert(1)

console.log(redBlackTree.getRoot())

打印结果如下,10的左节点为红色4节点,右节点为20节点

屏幕截图 2022-05-12 145425.png 20节点的左节点为红色18节点,右节点为红色21节点

Snipaste_2022-05-12_14-55-59.png 4节点的左节点为2节点,右节点为7节点

Snipaste_2022-05-12_14-56-52.png 2的左节点为红色1节点,右节点为Nil节点

Snipaste_2022-05-12_14-57-35.png

总结

红黑树的插入详解到这就结束了,上面给插入做了几个测试,可以看到结果跟结构图是一样的,如果后续你们做测试的时候发现有不对的地方可以私信我,一起讨论,我也是根据自己的理解写出的代码,所以难免会有些错误。 总的来说红黑树的插入操作也不是很难,主要是理解遇到什么情况应该怎么解决,剩下的就是用代码表示出来而已,也谢谢大家看到这里,如果觉得文章写的不错的话可以帮忙点赞转发一下哦。

全部源码

// 节点类
class Node {
  constructor(key){
    this.left = null
    this.key = key
    this.color = 'red'
    this.right = null
    this.parent = null
  }
}

// 黑色空节点类
class Nil {
  constructor(){
    this.left = null
    this.key = 'Nil'
    this.color = 'black'
    this.right = null
  }
}

// 红黑树
class RedBlackTree {
  constructor(){
    this.root = null
    this.Nil = new Nil()
  }

  // 插入操作
  insert(key){
    // 创建新的节点对象
    const node = new Node(key)
    // 情况一:如果根节点为空
    if(this.root === null){
      // 则新节点直接插入即可,并且将颜色改为黑色
      this.root = node
      node.color = 'black'
    }else {
      this.insertNode(this.root,node)
    }

    if(node.left === null){
      node.left = this.Nil
    }
    if(node.right === null){
      node.right = this.Nil
    }
    this.root.color = 'black'
  }

  insertNode(oldNode,newNode){
    let G = oldNode
    let P = oldNode
    let U = oldNode
    // 定义一个变量,判断是否是左子节点
    let isLifeChild = true
    if(newNode.key < oldNode.key){
      if(oldNode.left.key === 'Nil' && oldNode === this.root){
        oldNode.left = newNode
        newNode.parent = oldNode
      }else{
        while(P.key !== 'Nil'){
          if(newNode.key < P.key){
            isLifeChild = true
            if(P.left.key === 'Nil') break;
            G = P
            P = G.left
            U = G.right
          }else {
            isLifeChild = false
            if(P.right.key === 'Nil') break;
            G = P
            P = G.right
            U = G.left
          }
        }
        // 如果父节点颜色为黑色
        if(P.color === 'black'){
          if(isLifeChild){
            if(P.left.key === 'Nil'){
              P.left = newNode
              newNode.parent = P
            }
          }else {
            if(P.right.key === 'Nil'){
              P.right = newNode
              newNode.parent = P
            }
          }
        }
        // 父红叔红祖黑
        else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && isLifeChild){
          G.color = 'red'
          P.color = 'black'
          U.color = 'black'
          P.left = newNode
          newNode.parent = P
          if(G === this.root){
            G.color = 'black'
          }else {
            G.parent.left = this.Nil
            this.insertNode(this.root,G)
          }
        }else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && !isLifeChild){
          G.color = 'red'
          P.color = 'black'
          U.color = 'black'
          P.right = newNode
          newNode.parent = P
          if(G === this.root){
            G.color = 'black'
          }else {
            G.parent.left = this.Nil
            this.insertNode(this.root,G)
          }
        }
        // 父红叔黑
        else if(P.color === 'red' && U.color === 'black' && !isLifeChild){
          console.log(newNode.key);
          P.parent.left = newNode
          newNode.parent = P.parent
          newNode.left = P
          P.parent = newNode
          newNode.left = this.Nil
          if(!newNode.right) newNode.right = this.Nil
          this.insertNode(this.root,P)
        }else if(P.color === 'red' && U.color === 'black' && G.color === 'black' && isLifeChild){
          P.color = 'black'
          G.color = 'red'
          G.left = P.right
          P.right = G
          // G.parent = P
          if(G === this.root){
            P.parent = null
            this.root = P
          }else {
            G.parent.left = P
            P.parent = G.parent
          }
          G.parent = P
          P.left = newNode
          newNode.parent = P
        }
      }
    }else {
      if(oldNode.right.key === 'Nil' && oldNode === this.root){
        oldNode.right = newNode
        newNode.parent = oldNode
      }else {
        while(P.key !== 'Nil'){
          if(newNode.key < P.key){
            isLifeChild = true
            if(P.left.key === 'Nil') break;
            G = P
            P = G.left
            U = G.right
          }else {
            isLifeChild = false
            if(P.right.key === 'Nil') break;
            G = P
            P = G.right
            U = G.left
          }
        }
        if(P.color === 'black'){
          if(isLifeChild){
            if(P.left.key === 'Nil'){
              P.left = newNode
              newNode.parent = P
            }
          }else {
            if(P.right.key === 'Nil'){
              P.right = newNode
              newNode.parent = P
            }
          }
        }else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && !isLifeChild){
          G.color = 'red'
          P.color = 'black'
          U.color = 'black'
          P.right = newNode
          newNode.parent = P
          if(G === this.root){
            G.color = 'black'
          }else {
            G.parent.right = this.Nil
            this.insertNode(this.root,G)
          }
        }else if(G.color === 'black' && P.color === 'red' && U.color === 'red' && isLifeChild){
          G.color = 'red'
          P.color = 'black'
          U.color = 'black'
          P.left = newNode
          newNode.parent = P
          if(G === this.root){
            G.color = 'black'
          }else {
            G.parent.right = this.Nil
            this.insertNode(this.root,G)
          }
        }else if(P.color === 'red' && U.color === 'black' && isLifeChild){
          console.log(newNode.key);
          P.parent.right = newNode
          newNode.parent = P.parent
          newNode.right = P
          P.parent = newNode
          newNode.right = this.Nil
          if(!newNode.left) newNode.left = this.Nil
          this.insertNode(this.root,P)
        }else if(P.color === 'red' && U.color === 'black' && G.color === 'black'){
          P.color = 'black'
          G.color = 'red'
          G.right = P.left
          P.left = G
          // G.parent = P
          if(G === this.root){
            P.parent = null
            this.root = P
          }else {
            G.parent.right = P
            P.parent = G.parent
          }
          G.parent = P
          P.right = newNode
          newNode.parent = P
        }
      }
    }
  }

  // 中序遍历
  midOrderTraversal(){
    const allNode = []
    this.midOrderTraversalNode(this.root,allNode)
    return allNode
  }
  midOrderTraversalNode(node,handelNode){
    if(node.key !== 'Nil'){
      this.midOrderTraversalNode(node.left,handelNode)
      handelNode.push(node)
      this.midOrderTraversalNode(node.right,handelNode)
    }
  }

  getRoot(){
    return this.root
  }
}

const redBlackTree = new RedBlackTree()
// debugger
redBlackTree.insert(10)
redBlackTree.insert(7)
redBlackTree.insert(5)
redBlackTree.insert(3)
redBlackTree.insert(1)
redBlackTree.insert(6)
redBlackTree.insert(2)

// redBlackTree.insert(10)
// redBlackTree.insert(9)
// redBlackTree.insert(8)
// redBlackTree.insert(7)
// redBlackTree.insert(6)
// redBlackTree.insert(5)
// redBlackTree.insert(4)
// redBlackTree.insert(3)
// redBlackTree.insert(2)
// redBlackTree.insert(1)

// redBlackTree.insert(1)
// redBlackTree.insert(2)
// redBlackTree.insert(3)
// redBlackTree.insert(4)
// redBlackTree.insert(5)
// redBlackTree.insert(6)
// redBlackTree.insert(7)
// redBlackTree.insert(8)
// redBlackTree.insert(9)
// redBlackTree.insert(10)

// redBlackTree.insert(1)
// redBlackTree.insert(3)
// redBlackTree.insert(5)
// redBlackTree.insert(7)
// redBlackTree.insert(10)
// redBlackTree.insert(6)
// redBlackTree.insert(2)

// redBlackTree.insert(10)
// redBlackTree.insert(7)
// redBlackTree.insert(18)
// redBlackTree.insert(21)
// redBlackTree.insert(2)
// redBlackTree.insert(20)
// redBlackTree.insert(4)
// redBlackTree.insert(1)

// redBlackTree.insert(23)
// redBlackTree.insert(15)
// redBlackTree.insert(6)
// redBlackTree.insert(31)
// redBlackTree.insert(4)
// redBlackTree.insert(10)
// redBlackTree.insert(21)
// redBlackTree.insert(19)
// redBlackTree.insert(3)
// redBlackTree.insert(5)

console.log(redBlackTree.getRoot());