JavaScript数据结构与算法学习笔记(中)

七、JavaScript实现树结构(一)

一、树结构简介

1.1简单了解树结构

树结构对比于数组/链表/哈希表的优势:

数组:

  • 优点:可以通过下标值访问,效率高;
  • 缺点:查找数据时需要先对数据进行排序,生成有序数组,才能提高查找效率;并且在插入和删除元素时,需要大量的位移操作

链表:

  • 优点:数据的插入和删除操作效率都很高;
  • 缺点:查找效率低,需要从头开始依次查找,直到找到目标数据为止;当需要在链表中间位置插入或删除数据时,插入或删除的效率都不高。

哈希表:

  • 优点:哈希表的插入/查询/删除效率都非常高;
  • 缺点:空间利用率不高,底层使用的数组中很多单元没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素;而且不能快速找出哈希表中最大值或最小值这些特殊值。

树结构:

  • 优点:树结构综合了上述三种结构的优点,同时也弥补了它们存在的缺点(虽然效率不一定都比它们高),比如树结构中数据都是有序的,查找效率高;空间利用率高;并且可以快速获取最大值和最小值等。

总的来说:每种数据结构都有自己特定的应用场景

树结构:

  • 树(Tree):由 n(n ≥ 0)个节点构成的有限集合。当 n = 0 时,称为空树

对于任一棵非空树(n > 0),它具备以下性质:

  • 数中有一个称为根(Root)的特殊节点,用 **r **表示;
  • 其余节点可分为 m(m > 0)个互不相交的有限集合 T1,T2,...,Tm,其中每个集合本身又是一棵树,称为原来树的子树(SubTree)

树的常用术语:

b692456e722440f98996cba99e56d23d.png

  • 节点的度(Degree):节点的子树个数,比如节点B的度为2;
  • 树的度:树的所有节点中最大的度数,如上图树的度为2;
  • 叶节点(Leaf)度为0的节点(也称为叶子节点),如上图的H,I等;
  • 父节点(Parent):度不为0的节点称为父节点,如上图节点B是节点D和E的父节点;
  • 子节点(Child):若B是D的父节点,那么D就是B的子节点;
  • 兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点,比如上图的B和C,D和E互为兄弟节点;
  • 路径和路径长度:路径指的是一个节点到另一节点的通道,路径所包含边的个数称为路径长度,比如A->H的路径长度为3;
  • 节点的层次(Level):规定根节点在1层,其他任一节点的层数是其父节点的层数加1。如B和C节点的层次为2;
  • 树的深度(Depth):树种所有节点中的最大层次是这棵树的深度,如上图树的深度为4;

1.2.树结构的表示方式

  • 最普通的表示方法:

cf3b5235f97746d894e14e8da754df11.png

如图,树结构的组成方式类似于链表,都是由一个个节点连接构成。不过,根据每个父节点子节点数量的不同,每一个父节点需要的引用数量也不同。

这种方法的缺点在于我们无法确定某一结点的引用数。

  • 儿子-兄弟表示法:

6619e6dc8fe44de9ae31e4131f65c7ab.png

这种表示方法可以完整地记录每个节点的数据,比如:

//节点A

Node{

  //存储数据

  this.data = data

  //统一只记录左边的子节点

  this.leftChild = B

  //统一只记录右边的第一个兄弟节点

  this.rightSibling = null

}

//节点B

Node{

  this.data = data

  this.leftChild = E

  this.rightSibling = C

}

//节点F

Node{

  this.data = data

  this.leftChild = null

  this.rightSibling = null

}

这种表示法的优点在于每一个节点中引用的数量都是确定的。

  • 儿子-兄弟表示法旋转

以下为儿子-兄弟表示法组成的树结构:

5251e20441d546b4904ddc5c5d4c2065.png

 将其顺时针旋转45°之后:

be5381ebe278417cafc6e4e4300df21e.png

这样就成为了一棵二叉树,由此我们可以得出结论:任何树都可以通过二叉树进行模拟

二、二叉树

2.1.二叉树简介

二叉树的概念:如果树中的每一个节点最多只能有两个子节点,这样的树就称为二叉树

二叉树十分重要,不仅仅是因为简单,更是因为几乎所有的树都可以表示成二叉树形式。

二叉树的组成:

  • 二叉树可以为空,也就是没有节点;
  • 若二叉树不为空,则它由根节点和称为其左子树TL和右子树TR的两个不相交的二叉树组成;

二叉树的五种形态:

dd04e6487e614172a2d76a2f0d4771c1.png

二叉树的特性:

  • 一个二叉树的第 i 层的最大节点树为:2(i-1),i >= 1;
  • 深度为k的二叉树的最大节点总数为:2k - 1 ,k >= 1;
  • 对任何非空二叉树,若 n0 表示叶子节点的个数,n2表示度为2的非叶子节点个数,那么两者满足关系:n0 = n2 + 1;如下图所示:H,E,I,J,G为叶子节点,总数为5;A,B,C,F为度为2的非叶子节点,总数为4;满足n0 = n2 + 1的规律。

bdf12cb3c9bc485e9bee7ad6bd37468d.png

 2.2.特殊的二叉树

完美二叉树

完美二叉树(Perfect Binary Tree)也称为满二叉树(Full Binary Tree),在二叉树中,除了最下一层的叶子节点外,每层节点都有2个子节点,这就构成了完美二叉树。

a2d86417e05741aca53ac565c957c2c7.png

完全二叉树(Complete Binary Tree):

  • 除了二叉树最后一层外,其他各层的节点数都达到了最大值;
  • 并且,最后一层的叶子节点从左向右是连续存在,只缺失右侧若干叶子节点;
  • 完美二叉树是特殊的完全二叉树;

c78ab933278e41c08faf0352a294cece.png

在上图中,由于H缺失了右子节点,所以它不是完全二叉树。

2.3.二叉树的数据存储

常见的二叉树存储方式为数组链表

使用数组:

  • 完全二叉树:按从上到下,从左到右的方式存储数据。

601c8ead7af542dca946615e9b2495dc.png

使用数组存储时,取数据的时候也十分方便:左子节点的序号等于父节点序号 * 2,右子节点的序号等于父节点序号 * 2 + 1

  • 非完全二叉树:非完全二叉树需要转换成完全二叉树才能按照上面的方案存储,这样会浪费很大的存储空间。

b9f1fff6e84c4cce9426d570328b40bd.png

使用链表

二叉树最常见的存储方式为链表:每一个节点封装成一个Node,Node中包含存储的数据、左节点的引用和右节点的引用。

daba40c8da6b4c819a59d71b8c5ce25f.png

 三、二叉搜索树

3.1.认识二叉搜索树

二叉搜索树BST,Binary Search Tree),也称为二叉排序树二叉查找树

二叉搜索树是一棵二叉树,可以为空;

如果不为空,则满足以下性质

  • 条件1:非空左子树的所有键值小于其根节点的键值。
  • 条件2:非空右子树的所有键值大于其根节点的键值;
  • 条件3:左、右子树本身也都是二叉搜索树;

41c33542419b41d1b7394bd4bc4b8d6d.png

八、JavaScript实现树结构(二)

一、二叉搜索树的封装

二叉树搜索树的基本属性:

如图所示:二叉搜索树有四个最基本的属性:指向节点的(root),节点中的(key)、左指针(right)、右指针(right)。

89ed012121f440e9a60fc42acd9171c4.png

所以,二叉搜索树中除了定义root属性外,还应定义一个节点内部类,里面包含每个节点中的left、right和key三个属性:

//封装二叉搜索树

    function BinarySearchTree(){

      //节点内部类

      function Node(key){

        this.key = key

        this.left = null

        this.right = null

      }

      //属性

      this.root = null

  }

二叉搜索树的常见操作

  • insert(key):向树中插入一个新的键;
  • search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false;
  • inOrderTraverse:通过中序遍历方式遍历所有节点;
  • preOrderTraverse:通过先序遍历方式遍历所有节点;
  • postOrderTraverse:通过后序遍历方式遍历所有节点;
  • min:返回树中最小的值/键;
  • max:返回树中最大的值/键;
  • remove(key):从树中移除某个键;

1.插入数据

实现思路:

  • 首先根据传入的key创建节点对象;
  • 然后判断根节点是否存在,不存在时通过:this.root = newNode,直接把新节点作为二叉搜索树的根节点。
  • 若存在根节点则重新定义一个内部方法insertNode()用于查找插入点。

//insert方法:对外向用户暴露的方法

  BinarySearchTree.prototype.insert = function(key){

        //1.根据key创建节点

        let newNode = new Node(key)

        //2.判断根节点是否存在

        if (this.root == null) {

          this.root = newNode

          //根节点存在时

        }else {

          this.insert(this.root, newNode)

        }

      }

内部方法insert()的实现思路:

根据比较传入的两个节点,一直查找新节点适合插入的位置,直到成功插入新节点为止。

当newNode.key < node.key向左查找:

  • 情况1:当node无左子节点时,直接插入:
  • 情况2:当node有左子节点时,递归调用insert(),直到遇到无左子节点成功插入new后,不再符合该情况,也就不再调用insert(),递归停止。

b5ef2e56e08c489baca0c3d11a46803c.png

当newNode.key >= node.key向右查找,与向左查找类似:

  • 情况1:当node无右子节点时,直接插入:
  • 情况2:当node有右子节点时,依然递归调用insert(),直到遇到传入insert方法的node无右子节点成功插入newNode为止:

cc158f20575943ecbcdffe0666d1f13a.png

 insert()代码实现:

 //内部使用的insert方法:用于比较节点从左边插入还是右边插入


  BinarySearchTree.prototype.insert= function(node, newNode){
        //当newNode.key < node.key向左查找
        if(newNode.key < node.key){ //情况1:node无左子节点,直接插入
          if (node.left == null) {
            node.left = newNode
//情况2:node有左子节点,递归调用insert()
          }else{
            this.insert(node.left, newNode)
          }


 //当newNode.key >= node.key向右查找
        }else{
          //情况1:node无右子节点,直接插入
          if(node.right == null){
            node.right == newNode
          //情况2:node有右子节点,依然递归调用insert()
          }else{
            this.insert(node.right, newNode)
          }
        }
      }

2.遍历数据

这种遍历适用于所有的二叉树。常见的三种二叉树遍历方式为:

  • 先序遍历;
  • 中序遍历;
  • 后序遍历;

还有层序遍历,使用较少。

2.1.先序遍历

先序遍历的过程为:

  • 首先,遍历根节点;
  • 然后,遍历其左子树;
  • 最后,遍历其右子树;

ce44a3a2687945eab53508f3725639ee.png

如上图所示,二叉树的节点遍历顺序为:A -> B -> D -> H -> I -> E -> C -> F -> G。

代码实现:

      //先序遍历
      //掺入一个handler函数方便之后对得到的key进行处理
      BinarySearchTree.prototype.preOrderTraversal = function(handler){
        this.preOrderTraversalNode(this.root, handler)
      }

      //封装内部方法,对某个节点进行遍历

 BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){
        if (node != null) {
          //1.处理经过的节点
          handler(node.key)
          //2.遍历左子树中的节点
this.preOrderTraversalNode(node.left, handler)
          //3.遍历右子树中的节点
          this.preOrderTraversalNode(node.right, handler)
        }
      }

2.2.中序遍历

实现思路:

  • 首先,遍历其左子树;
  • 然后,遍历根(父)节点;
  • 最后,遍历其右子树;

06883e8ff0c8499fab86c4fd1eaa1659.png

输出节点的顺序应为:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 。

代码实现:

//中序遍历

 BinarySearchTree.prototype.midOrderTraversal = function(handler){

 this.midOrderTraversalNode(this.root, handler)

      }

 BinarySearchTree.prototype.midOrderTraversalNode = function(node, handler){

        if (node != null) {

          //1.遍历左子树中的节点

 this.midOrderTraversalNode(node.left, handler)

          //2.处理节点

          handler(node.key)

          //3.遍历右子树中的节点

 this.midOrderTraversalNode(node.right, handler)

        }

      }

2.3.后序遍历

实现思路:

  • 首先,遍历其左子树;
  • 然后,遍历其右子树;
  • 最后,遍历根(父)节点;

d874a7d90a1940b4a15a9d56a251d928.png

输出节点的顺序应为:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 。

代码实现:

      //后序遍历
 BinarySearchTree.prototype.postOrderTraversal = function(handler){
        this.postOrderTraversalNode(this.root, handler)
      }

 BinarySearchTree.prototype.postOrderTraversalNode = function(node, handler){
        if (node != null) {
          //1.遍历左子树中的节点
          this.postOrderTraversalNode(node.left, handler)
          
          //2.遍历右子树中的节点
          this.postOrderTraversalNode(node.right, handler)

          //3.处理节点
          handler(node.key)
        }
      }

3.查找数据

3.1.查找最大值&最小值

在二叉搜索树中查找最值非常简单,最小值在二叉搜索树的最左边,最大值在二叉搜索树的最右边。只需要一直向左/右查找就能得到最值,如下图所示:

18b8c3bc62824a399d5946f3f8e53348.png

代码实现:

      //寻找最大值
      BinarySearchTree.prototype.max = function () {
        //1.获取根节点
        let node = this.root
        //2.定义key保存节点值
        let key = null
        //3.依次向右不断查找,直到节点为null
        while (node != null) {
          key = node.key
          node = node.right
        }
        return key
      }

      //寻找最小值
      BinarySearchTree.prototype.min = function(){
         //1.获取根节点
         let node = this.root
        //2.定义key保存节点值
        let key = null
        //3.依次向左不断查找,直到节点为null
        while (node != null) {
          key = node.key
          node = node.left
        }
        return key
      }

3.2.查找特定值

从根节点开始将需要查找节点的key值与之比较,若node.key < root则向左查找,若node.key > root就向右查找,直到找到或查找到null为止。用递归 / 循环实现。

实现代码:

//查找特定的key

  BinarySearchTree.prototype.search = function(key){

        //1.获取根节点

        let node = this.root

        //2.循环搜索key

        while(node != null){

          if (key < node.key) {

            //小于根(父)节点就往左边找

            node = node.left

            //大于根(父)节点就往右边找

          }else if(key > node.key){

            node = node.right

          }else{

            return true

          }

        } 

        return false

      }

4.删除数据

实现思路:

第一步:先找到需要删除的节点,若没找到,则不需要删除;

首先定义变量current用于保存需要删除的节点、变量parent用于保存它的父节点、变量isLeftChild保存current是否为parent的左节点,这样方便之后删除节点时改变相关节点的指向。

实现代码:

//1.1.定义变量

        let current = this.root

        let parent = null

        let isLeftChild = true

        //1.2.开始寻找删除的节点

        while (current.key != key) {

          parent = current

          // 小于则往左查找

          if (key < current.key) {

            isLeftChild = true

            current = current.left

          } else{

            isLeftChild = false

            current = current.rigth

          }

          //找到最后依然没有找到相等的节点

          if (current == null) {

            return false

          }

        }

        //结束while循环后:current.key = key

第二步:删除找到的指定节点,后分3种情况:

  • 删除叶子节点;
  • 删除只有一个子节点的节点;
  • 删除有两个子节点的节点;

4.1.情况1:没有子节点

没有子节点时也有两种情况:

  • 当该叶子节点为根节点时,直接通过:this.root = null,删除根节点。
  • 当该叶子节点不为根节点时也有两种情况,如下图所示

0ff1f2cb4a3244df8081f561cee33358.png

 代码实现:

        //情况1:删除的是叶子节点(没有子节点)
        if (current.left == null && current.right ==null) {
          if (current == this.root) {
            this.root = null
          }else if(isLeftChild){
            parent.left = null
          }else {
            parent.right =null
          }
        }

4.2.情况2:有一个子节点

有六种情况分别是:

current存在左子节点时(current.right == null):

  • 情况1:current为根节点;
  • 情况2:current为父节点parent的左子节点(isLeftChild == true);
  • 情况3:current为父节点parent的右子节点(isLeftChild == false);

d280084db4624702b3a2c9fee1b49a0e.png

current存在右子节点时(current.left = null):

  • 情况4:current为根节点(current == this.root);
  • 情况5:current为父节点parent的左子节点(isLeftChild == true);
  • 情况6:current为父节点parent的右子节点(isLeftChild == false);

db81090a69f241e9b7f40b65cfb3f0f5.png

4.3.情况3:有两个子节点

这种情况十分复杂,首先依据以下二叉搜索树,讨论这样的问题:

3f859a9a153449b0a993681ab6105bfa.png

删除节点9

在保证删除节点9后原二叉树仍为二叉搜索树的前提下,有两种方式:

  • 方式1:从节点9的左子树中选择一合适的节点替代节点9,可知节点8符合要求;
  • 方式2:从节点9的右子树中选择一合适的节点替代节点9,可知节点10符合要求;

98f25aa6a0574c078fd389fd7583287d.png

删除节点7

在保证删除节点7后原二叉树仍为二叉搜索树的前提下,也有两种方式:

  • 方式1:从节点7的左子树中选择一合适的节点替代节点7,可知节点5符合要求;
  • 方式2:从节点7的右子树中选择一合适的节点替代节点7,可知节点8符合要求;

42035c636c0d4e3bbe43ed2e920b29f1.png

删除节点15

在保证删除节点15后原树二叉树仍为二叉搜索树的前提下,同样有两种方式:

  • 方式1:从节点15的左子树中选择一合适的节点替代节点15,可知节点14符合要求;
  • 方式2:从节点15的右子树中选择一合适的节点替代节点15,可知节点18符合要求;

23307d3dbe3e44959f0096831f35df45.png

规律总结:如果要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从要删除节点下面的子节点中找到一个合适的节点,来替换当前的节点。

若用current表示需要删除的节点,则合适的节点指的是:

  • current左子树中比current小一点点的节点,即current左子树中的最大值;
  • current右子树中比current大一点点的节点,即current右子树中的最小值;

前驱&后继

在二叉搜索树中,这两个特殊的节点有特殊的名字:

  • 比current小一点点的节点,称为current节点的前驱。比如下图中的节点5就是节点7的前驱;
  • 比current大一点点的节点,称为current节点的后继。比如下图中的节点8就是节点7的后继;

98310e077ed645ad934683bf146ac931.png

代码实现:

  • 查找current的后继,需要在current的右子树中查找最小值
  • 查找前驱时,则需要在current的左子树中查找最大值

4.4.实现查找后继

      //删除节点
  BinarySearchTree.prototype.remove = function(key){
/*-----1.寻找要删除的节点------*/
        let current = this.root
        let parent = null
        let isLeftChild = true


        while (current.key != key) {
          parent = current
          // 小于则往左查找
          if (key < current.key) {
            isLeftChild = true
            current = current.left
          } else{
            isLeftChild = false
            current = current.right
          }
          //找到最后依然没有找到相等的节点
          if (current == null) {
            return false
          }
        }
        //结束while循环后:current.key = key

   /*--2.根据对应情况删除节点-----*/
        //情况1:删除的是叶子节点(没有子节点)
        if (current.left == null && current.right ==null) {
          if (current == this.root) {
            this.root = null
          }else if(isLeftChild){
            parent.left = null
          }else {
            parent.right =null
          }
        }
        //情况2:删除的节点有一个子节点
        //当current存在左子节点时
        else if(current.right == null){
            if (current == this.root) {
              this.root = current.left
            } else if(isLeftChild) {
                parent.left = current.left
            } else{
                parent.right = current.left
            }
        //当current存在右子节点时
      } else if(current.left == null){
            if (current == this.root) {
              this.root = current.right
            } else if(isLeftChild) {
                parent.left = current.right
            } else{
                parent.right = current.right
            } 
      }
        //情况3:删除的节点有两个子节点
        else{
          //1.获取后继节点
          let successor = this.getSuccessor(current)

          //2.判断是否根节点
          if (current == this.root) {
            this.root = successor
          }else if (isLeftChild){
            parent.left = successor
          }else{
            parent.right = successor
          }

          //3.将后继的左子节点改为被删除节点的左子节点
          successor.left = current.left
        }
      }

      //封装查找后继的方法
      BinarySearchTree.prototype.getSuccessor = function(delNode){
        //1.定义变量,保存找到的后继
        let successor = delNode
        let current = delNode.right
        let successorParent = delNode

        //2.循环查找current的右子树节点
        while(current != null){
          successorParent = successor
          successor = current
          current = current.left
        }

        //3.判断寻找到的后继节点是否直接就是删除节点的right节点
        if(successor != delNode.right){
          successorParent.left = successor.right
          successor.right = delNode.right 
        }
        return successor
      }

二、平衡树

二叉搜索树的缺陷:

当插入的数据是有序的数据,就会造成二叉搜索树的深度过大,严重影响二叉搜索树的性能。

29ebd351b5374672aa8555afcd989183.png

非平衡树

  • 插入连续数据后,二叉搜索树中的数据分布就变得不均匀,我们称这种树为非平衡树
  • 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN)
  • 而对于一棵非平衡二叉树来说,相当于编写了一个链表,查找效率变成了O(N);

树的平衡性

为了能以较快的时间O(logN)来操作一棵树,我们需要保证树总是平衡的:

  • 起码大部分是平衡的,此时的时间复杂度也是接近O(logN)的;
  • 这就要求树中每个节点左边的子孙节点的个数,应该尽可能地等于右边的子孙节点的个数;

常见的平衡树

  • AVL树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于AVL树是平衡树,所以它的时间复杂度也是O(logN)。但是它的整体效率不如红黑树,开发中比较少用。
  • 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是O(logN)。进行插入/删除等操作时,性能优于AVL树,所以平衡树的应用基本都是红黑树。

九、图解红黑树

一、红黑树的五条规则

红黑树除了符合二叉搜索树的基本规则外,还添加了以下特性:

  • 规则1:节点是红色或黑色的;
  • 规则2:根节点是黑色的;
  • 规则3:每个叶子节点都是黑色的空节点(NIL节点);
  • 规则4:每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不可能有两个连续的红色节点);
  • 规则5:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点;

18384f84ff0243b0a57569a6f2f8a90f.png

红黑树的相对平衡

前面5条规则的约束确保了以下红黑树的关键特性:

  • 根到叶子节点最长路径,不会超过最短路径两倍
  • 结果就是这棵树基本是平衡的;
  • 虽然没有做到绝对的平衡,但是可以保证在最坏的情况下,该树依然是高效的;

为什么可以做到最长路径不超过最短路径的两倍呢?

  • 性质4决定了路径上不能有两个相连的红色节点;
  • 所以,最长路径一定是红色节点和黑色节点交替而成的;
  • 由于根节点和叶子节点都是黑色的,最短路径可能都是黑色节点,并且最长路径中一定是黑色节点多于红色节点;
  • 性质5决定了所有路径上都有相同数目的黑色节点;
  • 这就表明了没有路径能多于其他任何路径两倍长。

二、红黑树的三种变换

插入一个新节点时,有可能树不再平衡,可以通过三种方式的变换使树保持平衡:

  • 变色;
  • 左旋转;
  • 右旋转;

2.1.变色

为了重新符合红黑树的规则,需要把红色节点变为黑色,或者把黑色节点变为红色

插入的新节点通常都是红色节点

  • 当插入的节点为红色的时候,大多数情况不违反红黑树的任何规则;

  • 插入黑色节点,必然会导致一条路径上多了一个黑色节点,这是很难调整的;

  • 红色节点虽然可能导致红红相连的情况,但是这种情况可以通过颜色调换和旋转来调整;

2.2.左旋转

以节点X为根逆时针旋转二叉搜索树,使得父节点原来的位置被自己的右子节点替代,左子节点的位置被父节点替代;

详解:

如上图所示,左旋转之后:

  • 节点X取代了节点a原来的位置;
  • 节点Y取代了节点X原来的位置;
  • 节点X的左子树 a 仍然是节点X的左子树(这里X的左子树只有一个节点,有多个节点时同样适用,以下同理);
  • 节点Y的右子树 c 仍然是节点Y的右子树
  • 节点Y的左子树 b 向左平移成为了节点X的右子树

除此之外,二叉搜索树左旋转之后仍为二叉搜索树:

2.3.右旋转

以节点X为根顺时针旋转二叉搜索树,使得父节点原来的位置被自己的左子节点替代,右子节点的位置被父节点替代;

详解:

如上图所示,右旋转之后:

  • 节点X取代了节点a原来的位置;
  • 节点Y取代了节点X原来的位置;
  • 节点X的右子树 a 仍然是节点X的右子树(这里X的右子树虽然只有一个节点,但是多个节点时同样适用,以下同理);
  • 节点Y的左子树 b 仍然是节点Y的左子树
  • 节点Y的右子树 c 向右平移成为了节点X的左子树

除此之外,二叉搜索树右旋转之后仍为二叉搜索树

三、红黑树的插入操作

首先需要明确,在保证满足红黑树5条规则的情况下,新插入的节点必然是红色节点

为了方便说明,规定以下四个节点:新插入节点为N(Node),N的父节点为P(Parent),P的兄弟节点为U(Uncle),U的父节点为G(Grandpa),如下图所示:

3.1.情况1

当插入的新节点N位于树的根上时,没有父节点。

这种情况下,只需要将红色节点变为黑色节点即可满足规则2 。

3.2.情况2

新节点N的父节点P为黑色节点,此时不需要任何变化。

此时既满足规则4也满足规则5。尽管新节点是红色的,但是新节点N有两个黑色节点NIL,所以通向它的路径上黑色节点的个数依然相等,因此满足规则5 。

3.3.情况3

节点P为红色,节点U也为红色,此时节点G必为黑色,即父红叔红祖黑

在这种情况下需要:

  • 先将父节点P变为黑色;
  • 再将叔叔节点U变为黑色;
  • 最后将祖父节点G变为红色;

即变为父黑叔黑祖红,如下图所示:

可能出现的问题:

  • N的祖父节点G的父节点也可能是红色,这就违反了规则4,此时可以通过递归调整节点颜色;
  • 当递归调整到根节点时就需要旋转了,如下图节点A和节点B所示,具体情况后面会介绍;

3.4.情况4

节点P是红色节点,节点U是黑色节点,并且节点N为节点P的左子节点,此时节点G一定是黑色节点,即父红叔黑祖黑

在这种情况下需要:

  • 先变色:将父节点P变为黑色,将祖父节点G变为红色;
  • 后旋转:以祖父节点G为根进行右旋转;

3.5.情况5

节点P是红色节点,节点U是黑色节点,并且节点N为节点P的右子节点,此时节点G一定是黑色节点,即父红叔黑祖黑

在这种情况下需要:

  • 先以节点P为根进行左旋转,旋转后如图b所示;
  • 随后将红色节点P黑色节点B看成一个整体的红色节点N1,将新插入的红色节点N看成红色节点P1 如图c所示。此时整体就转换为了情况4。

接着可以按照情况4进行处理:

  • 先变色:将N1节点的父节点P1变为黑色,将祖父节点G变为红色;

  • 后旋转:以祖父节点G为根进行右旋转,旋转后如图 e 所示;

  • 最后将节点N1和P1变换回来,完成节点N的插入,如图 f 所示;

猜你喜欢

转载自blog.csdn.net/m0_65835778/article/details/126482262