javascript数据结构 -- 红黑树(二)

186 阅读10分钟

红黑树 (Red-Black Tree)

接前文分析,本文先实现RBT中结点的旋转方法,然后对向红黑树中插入新结点的时候的各种情况逐一分析,最终完成红黑树上插入新结点的insert方法。

1. BST结点左旋

根据之前的分析,如下图:

 --→ FR---  
 ↑       ↓  
FRL ←----F  

简记:所谓左旋指的就是-> 父结点作为其右子结点的左子结点

对应的实现代码如下:

  // 调整策略一:左旋转--指的是当前结点的位置被其右子结点取代,而它本身成为其右子结点的左子结点  
  // 此函数执行完毕之后node的位置会发生变化  
  leftRotate(node) {  
    // 第一步:找相关结点  
    // 找到右子结点FR  
    const rightChild = node.right;  
  
    // 第二步:处理孙子结点的位置  
    // 如果FRL存在就作为F的子结点  
    node.right = rightChild.left;  
    if (rightChild.left !== null) {  
      rightChild.left.parent = node;  
    }  
  
    // 第三步:使用右子结点替换位置  
    // 然后使用FR替代F的位置  
    rightChild.parent = node.parent;  
    // 处理F的原来的父结点的子结点问题  
    // 如果F原来的父结点为null,则证明node就是root  
    if (node.parent === null) {  
      this.root = rightChild;  
      // 否则用FR替换F  
    } else if (node === node.parent.left) {  
      node.parent.left = rightChild;  
    } else {  
      node.parent.right = rightChild;  
    }  
  
    // 第四步:作为右子结点的左子结点  
    // F作为其原来右子结点的左子结点  
    rightChild.left = node;  
    node.parent = rightChild;  
  }  

2. BST结点右旋

根据之前的分析,如下图:

 --→ FL---  
 ↑       ↓  
FLR ←----F  

简记:所谓右旋指的就是-> 父结点作为其左子结点的右子结点
对应的实现代码如下:

  // 调整策略二:右旋转--指的是当前结点的位置被其左子结点取代,而它本身成为其左子结点的右子结点  
  // 此函数执行完毕之后node的位置会发生变化  
  rightRotate(node) {  
    // 第一步:找到相关结点FL  
    const leftChild = node.left;  
  
    // 第二步:处理孙子结点的位置  
    node.left = leftChild.right;  
    // 如果左子结点的右子结点存在,则将其挂在本结点下面  
    if (leftChild.right !== null) {  
      leftChild.right.parent = node;  
    }  
  
    // 第三步:使用FL取代F的位置  
    leftChild.parent = node.parent;  
    // 处理本结点是root的情况  
    if (node.parent === null) {  
      this.root = leftChild;  
      // 如果原来的位置不是root则用FL替换原来的位置  
    } else if (node === node.parent.left) {  
      node.parent.left = leftChild;  
    } else {  
      node.parent.right = leftChild;  
    }  
  
    // 第四步:调整F的位置  
    leftChild.right = node;  
    node.parent = leftChild;  
  }  

旋转的一些结论

    1. 结点左旋是为了让自己的右子结点上位;
    1. 右旋是为了让自己的左子结点上位。

3. BST插入结点策略

  • 总体策略:先将新的结点作为一个红色结点插入进去,然后根据插入之后的结果对树进行检查并调整;简称先插后查
  • 格式检查方法,检查某一个结点的局部是否符合红黑树的特性,如果不符合则根据一定的规则调整;
  • 所谓结点N的局部,指的就是:父结点P,叔叔结点U,祖父结点G
  • 理论上,插入新的结点之后应该对每个现有结点进行检查,但是大多数情况下新插入的结点对树的扰动是有限的,所以无需对每个结点都进行检查;
  • 为方便,相比较于BST中的node对象,RBT中的node对象会多一个parent属性指向自己的父结点;
  • 尽管插入之后的情况比较复杂,但是是可以穷举的,只需要通过分类讨论解决每一个可能就行了;

下面是插入结点(但是不包括结点位置检查)的代码实现

  // 插入结点  
  insert(value) {  
    const newNode = new Node(value);  
    // 传统的BST中结点不会保持对父结点的引用,所以再遍历的时候始终使用一个变量保存当前结点的父结点  
    let current = this.root;  
    let parent = null;  
  
    // 传统的BST新插入的结点一定是叶子结点,所以只需使用while循环往最后一层找就可以了  
    while (current !== null) {  
      parent = current;  
  
      if (newNode.value < current.value) {  
        current = current.left;  
      } else {  
        current = current.right;  
      }  
    }  
    // 找到最后一层也就知道了自己的父结点是谁  
    newNode.parent = parent;  
  
    // 然后处理新插入的结点到底是左子结点还是右子结点的问题  
    // 处理空树情况  
    if (parent === null) {  
      this.root = newNode;  
    } else if (newNode.value < parent.value) {  
      // 通过和根结点比较大小确定插入的结点时左结点还是右结点  
      parent.left = newNode;  
    } else {  
      parent.right = newNode;  
    }  
    // 先尝试性的将红色结点插入进去,然后再根据插入之后的结果对树进行调整  
    this.fixInsertViolation(newNode);  
  }  

4. 新插入结点影响的有限性

如何界定新插入的结点对树的影响是有限的呢?有如下结论:
只有旋转和变色的操作,改变了G的颜色,此时新增结点产生的影响才会向上层结点传播

5. N P G 之间的关系 -- 撇捺顺柺

“撇捺顺柺”是我自己想出来的,用来记忆“N P G” 之间的关系的一种简称: 

    1. 撇 N P都是左子结点
    1. 捺 N P都是右子结点
    1. 顺 N P关系位撇或者捺
    1. 柺 N P的子结点方向相反
      P.S. 需要对P旋转将柺变成顺

6. 谁会旋转

在检查结点方法中,只涉及两种旋转,它们及它们的特点是:

    1. 父结点的旋转:和其位置相同(即如果P是左子结点则向左旋转),父结点旋转前不变色,旋转完之后此位置变成黑色,相邻的两个结点都变成红色;
    1. 祖父结点的旋转:为了让父结点上位,所以父在右祖左旋,父在左祖右旋。祖父结点旋转后,各个结点均不变色。

7. 被检验的结点所有可能出现的位置关系

  1. N: 表示被检测的结点
  2. P: N的父结点
  3. U: P的兄弟结点
  4. G: P的父结点
    1. N本身为null;
    1. N的父亲为null,这说明被检测对象是root结点;
    1. P为黑色;
      -    P为红色:
    1. P为红色,U也为红色;
      x. P为红色,U为黑色;
    1. P为红色,U不存在;
      - 4.1 P为左子结点        
      - (1)N为左子结点 撇
      - (2)N为右子结点 右柺
      - 4.2 P为右子结点        
      - (1)N为右子结点 捺
      - (2)N为左子结点 左柺

8. 各种情况解决方法

  • case 0:
    - 显然这种情况下什么都不用做;
  • case 1:
    - 这种情况下需要保证被检测对象是黑色,所以需要红变黑;
  • case 2:
    - P为黑色时插入一个红色节点是符合规定的,什么都不需要做;
  • case 3:
    - 将P和U都从红变成黑,将G从黑变成红(注意此时G一定是黑的),然后递归检查G结点;
  • case x:
    - P为红色时G一定是黑色,此时如果U也为黑色,则从G出发到达P子树叶子节点和U子树叶子结点的黑色结点数目必不相同,所以这种情况不存咋!
  • case 4.1:
    - 如果右拐,先左旋P(见:6. 谁会旋转)纠正右拐为撇,然后变色,最后右旋G;
  • case 4.2:
    - 如果左拐,先右旋P(见:6. 谁会旋转)纠正左拐为捺,然后变色,最后左旋G;

结点位置检测代码如下:

 // 对红黑树中的某个结点的位置进行检查  
  fixInsertViolation(node) {  
    // case 0:  
    if(!node) return;  
  
    // 爸爸进场  
    const parent = node.parent;  
    // case 1:  
    if(!parent) {  
      node.color = 'black';  
      return;  
    }  
    // case 2:  
    if(parent.color === "black") return;  
  
    // 爷爷进场  
    const grandP = parent.parent;  
    // 叔叔进场  
    const _tmp = grandP.left;  
    const uncle = _tmp === parent ? grandP.right : _tmp;  
      
    // case 3:  
    if(uncle?.color === "red") {  
      parent.color = "black";  
      uncle.color = "black";  
      this.fixInsertViolation(grandP);  
    }  
  
    // 判断P的位置  
    const isPLeft = P === grandP.left;  
    // 判断N的位置  
    const isNLeft = N === P.left;  
    if(isPLeft) {  
      // case 4.1:  
      // 先处理右拐  
      if(!isNLeft){  
        this.leftRotate(parent);  
        // 然后变色 -- 注意此时N和F的位置调换了  
        node.color = "black";  
      } else {  
        // G右旋之前需要先变色  
        parent.color = "black";  
      }  
      grandP.color = "red";  
      this.rightRotate(grandP);  
  
    } else {  
      // case 4.2:  
      if(isNLeft){  
        // 先处理左拐  
        this.rightRotate(parent);  
        // 然后变色 -- 注意此时N和F的位置调换了  
        node.color = "black";  
      } else {  
        // G左旋之前需要先变色  
        parent.color = "black";  
      }  
      grandP.color = "red";  
      this.leftRotate(grandP);  
    }  
  }  

9. 扰动传播分析

  • 对于case 0, 1, 2, 均不涉及G变色问题,所以检查时候改变树的结构不会将扰动传播到上层结点中;
  • 对于case 4, 画图分析之后发现旋转G不会造成G所在的位置的结点的颜色发生改变,所以扰动同样也不会传播到上层结点;
  • 对于case 3, 由于G从黑变成了红色,所以可以将G子树整体看成是一个红色结点,此时对原来的N的检查已经结束,开始检查G结点,通过这样的方式将扰动传递到了上层结点中。

10. 实现代码

// 定义红黑树结点  
class Node {  
  constructor(value, color) {  
    this.value = value;  
    this.color = color || "red";  
    this.left = null;  
    this.right = null;  
    this.parent = null;  
  }  
}  
  
// 定义红黑树  
class RedBlackTree {  
  constructor() {  
    this.root = null;  
  }  
  
  // 调整策略一:左旋转--指的是当前结点的位置被其右子结点取代,而它本身成为其右子结点的左子结点  
  // 此函数执行完毕之后node的位置会发生变化  
  leftRotate(node) {  
    // 第一步:找相关结点  
    // 找到右子结点FR  
    const rightChild = node.right;  
  
    // 第二步:处理孙子结点的位置  
    // 如果FRL存在就作为F的子结点  
    node.right = rightChild.left;  
    if (rightChild.left !== null) {  
      rightChild.left.parent = node;  
    }  
  
    // 第三步:使用右子结点替换位置  
    // 然后使用FR替代F的位置  
    rightChild.parent = node.parent;  
    // 处理F的原来的父结点的子结点问题  
    // 如果F原来的父结点为null,则证明node就是root  
    if (node.parent === null) {  
      this.root = rightChild;  
      // 否则用FR替换F  
    } else if (node === node.parent.left) {  
      node.parent.left = rightChild;  
    } else {  
      node.parent.right = rightChild;  
    }  
  
    // 第四步:作为右子结点的左子结点  
    // F作为其原来右子结点的左子结点  
    rightChild.left = node;  
    node.parent = rightChild;  
  }  
  
  // 调整策略二:右旋转--指的是当前结点的位置被其左子结点取代,而它本身成为其左子结点的右子结点  
  // 此函数执行完毕之后node的位置会发生变化  
  rightRotate(node) {  
    // 第一步:找到相关节点FL  
    const leftChild = node.left;  
  
    // 第二步:处理孙子结点的位置  
    node.left = leftChild.right;  
    // 如果左子结点的右子结点存在,则将其挂在本结点下面  
    if (leftChild.right !== null) {  
      leftChild.right.parent = node;  
    }  
  
    // 第三步:使用FL取代F的位置  
    leftChild.parent = node.parent;  
    // 处理本结点是root的情况  
    if (node.parent === null) {  
      this.root = leftChild;  
      // 如果原来的位置不是root则用FL替换原来的位置  
    } else if (node === node.parent.left) {  
      node.parent.left = leftChild;  
    } else {  
      node.parent.right = leftChild;  
    }  
  
    // 第四步:调整F的位置  
    leftChild.right = node;  
    node.parent = leftChild;  
  }  
  
  // 可以看出来,不管是左旋还是右旋都是先改变层级比较深的结点的位置,然后才是一层一层往上修改的  
  
  // 插入结点  
  insert(value) {  
    const newNode = new Node(value);  
    // 传统的BST中结点不会保持对父结点的引用,所以再遍历的时候始终使用一个变量保存当前结点的父结点  
    let current = this.root;  
    let parent = null;  
  
    // 传统的BST新插入的结点一定是叶子结点,所以只需使用while循环往最后一层找就可以了  
    while (current !== null) {  
      parent = current;  
  
      if (newNode.value < current.value) {  
        current = current.left;  
      } else {  
        current = current.right;  
      }  
    }  
    // 找到最后一层也就知道了自己的父结点是谁  
    newNode.parent = parent;  
  
    // 然后处理新插入的结点到底是左子结点还是右子结点的问题  
    // 处理空树情况  
    if (parent === null) {  
      this.root = newNode;  
    } else if (newNode.value < parent.value) {  
      // 通过和根结点比较大小确定插入的结点时左结点还是右结点  
      parent.left = newNode;  
    } else {  
      parent.right = newNode;  
    }  
    // 先尝试性的将红色结点插入进去,然后再根据插入之后的结果对树进行调整  
    this.fixInsertViolation(newNode);  
  }  
  
  // 对红黑树中的某个结点的位置进行检查  
  fixInsertViolation(node) {  
    // case 0:  
    if(!node) return;  
  
    // 爸爸进场  
    const parent = node.parent;  
    // case 1:  
    if(!parent) {  
      node.color = 'black';  
      return;  
    }  
    // case 2:  
    if(parent.color === "black") return;  
  
    // 爷爷进场  
    const grandP = parent.parent;  
    // 叔叔进场  
    const _tmp = grandP.left;  
    const uncle = _tmp === parent ? grandP.right : _tmp;  
      
    // case 3:  
    if(uncle?.color === "red") {  
      parent.color = "black";  
      uncle.color = "black";  
      this.fixInsertViolation(grandP);  
    }  
  
    // 判断P的位置  
    const isPLeft = P === grandP.left;  
    // 判断N的位置  
    const isNLeft = N === P.left;  
    if(isPLeft) {  
      // case 4.1:  
      // 先处理右拐  
      if(!isNLeft){  
        this.leftRotate(parent);  
        // 然后变色 -- 注意此时N和F的位置调换了  
        node.color = "black";  
      } else {  
        // G右旋之前需要先变色  
        parent.color = "black";  
      }  
      grandP.color = "red";  
      this.rightRotate(grandP);  
  
    } else {  
      // case 4.2:  
      if(isNLeft){  
        // 先处理左拐  
        this.rightRotate(parent);  
        // 然后变色 -- 注意此时N和F的位置调换了  
        node.color = "black";  
      } else {  
        // G左旋之前需要先变色  
        parent.color = "black";  
      }  
      grandP.color = "red";  
      this.leftRotate(grandP);  
    }  
  }  
  
  // 中序遍历红黑树  
  inOrderTraversal(node, handle) {  
    if (node !== null) {  
      this.inOrderTraversal(node.left, handle);  
      handle(node);  
      this.inOrderTraversal(node.right, handle);  
    }  
  }  
}  
  
// 测试红黑树  
const redBlackTree = new RedBlackTree();  
  
redBlackTree.insert(10);  
redBlackTree.insert(20);  
redBlackTree.insert(30);  
redBlackTree.insert(15);  
redBlackTree.insert(25);  
  
redBlackTree.inOrderTraversal(redBlackTree.root, console.log);