学习JavaScript数据结构和算法(部分三)

看了 Loiane Groner 著的《学习JavaScript数据结构与算法》一书,自己写篇博客对着敲敲代码:
全文包含十个部分,分别是:数组、栈、队列、链表、集合、字典与散列表、树、图、排序和搜索算法、算法补充知识。


知识点其他部分参考:
学习JavaScript数据结构和算法(部分一)
学习JavaScript数据结构和算法(部分二)
学习JavaScript数据结构和算法(部分三)
学习JavaScript数据结构和算法(部分四)


7、树

到目前为止,介绍了一些顺序数据结构(例如:数组、队列、链表等),介绍的第一个非顺序数据结构是散列表。现在学习另一种非顺序数据结构——树,它以分层的方式存储数据,对于存储需要快速查找的数据非常有用。

优势:选择树而不是那些基本数据结构,是因为:
* 二叉树上进行查找非常快(链表上查找较慢);
* 二叉树添加或删除元素也非常快(数组执行添加或删除较慢)。

7.1 树的相关关系:

一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个节点)以及零个或多个子节点:
这里写图片描述

  • 位于树顶部的节点叫作根节点(11)。它没有父节点。树中的每个元素都叫作节点,节点分为内部节点和外部节点。至少有一个子节点的节点称为内部节点(7、5、9、15、13和20是内部节点)。没有子元素的节点称为外部节点或叶节点(3、6、8、10、12、14、18和25是叶节点)。

  • 一个节点可以有祖先和后代。一个节点(除了根节点)的祖先包括父节点、祖父节点、曾祖父节点等。一个节点的后代包括子节点、孙子节点、曾孙节点等。例如,节点5的祖先有节点7和节点11,后代有节点3和节点6。

  • 有关树的另一个术语是子树。子树由节点和它的后代构成。例如,节点13、12和14构成了上图中树的一棵子树。

  • 节点的一个属性是深度,节点的深度取决于它的祖先节点的数量。比如,节点3有3个祖先节点(5、7和11),它的深度为3。

  • 树的高度取决于所有节点深度的最大值。一棵树也可以被分解成层级。根节点在第0层,它的子节点在第1层,以此类推。上图中的树的高度为3(最大高度已在图中表示——第3层)。

7.2 二叉树和二叉搜索树:

二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。

二叉搜索树(BST)是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,在右侧节点存储(比父节点)大(或者等于)的值。上一节的图中就展现了一棵二叉搜索树。

7.2.1 二叉搜索树的创建:

二叉搜索树数据结构的组织方式:
二叉搜索树数据结构的组织方式
和链表一样,将通过指针来表示节点之间的关系(术语称其为边)。在双向链表中,每个节点包含两个指针,一个指向下一个节点,另一个指向上一个节点。对于树,使用同样的方式(也使用两个指针)。但是,一个指向左侧子节点,另一个指向右侧子节点。因此,将声明一个Node类来表示树中的每个节点(行{1}),节点将会称为键。

function BinarySearchTree(){
    this.Node = Node;
    let root = null;
    this.insert = insert;    //insert(key):向树中插入一个新的键。
    this.search = search;    //search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false。
    this.min = min;    //min:返回树中最小的值/键。
    this.max = max;    //max:返回树中最大的值/键。
    this.inOrderTraverse = inOrderTraverse;  //inOrderTraverse:通过中序遍历方式遍历所有节点。
    this.preOrderTraverse = preOrderTraverse;  //preOrderTraverse:通过先序遍历方式遍历所有节点。
    this.postOrderTraverse = postOrderTraverse;  //postOrderTraverse:通过后序遍历方式遍历所有节点。
    this.remove = remove;    //remove(key):从树中移除某个键。
}
function Node(key){
    this.key = key;
    this.left = null;
    this.right = null;
}
//1、insert(key):向树中插入一个新的键
function insert(key){
    let newNode = new this.Node(key);
    if(this.root == null){
        this.root = newNode;
    }else{
        insertNode(this.root, newNode);
    }
}
//运用递归
function insertNode(node, newNode){
    if(newNode.key < node.key){
        if(node.left == null){
            node.left = newNode;
        }else{
            insertNode(node.left, newNode);
        }
    }else{
        if(node.right == null){
            node.right = newNode;
        }else{
            insertNode(node.right, newNode);
        }
    }
}

//2、search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false。
function search(key){
    return searchNode(this.root, key);
}
function searchNode(node, key){
    if(node == null){
        return false;
    }
    if(key < node.key){
        return searchNode(node.left, key);
    }else if(key > node.key){
        return searchNode(node.right, key);
    }else{
        return true;
    }
}
//3、min函数
function min(){
    let node = this.root;
    if(node){
        while(node && node.left){
            node = node.left;
        }
        return node.key;
    }
    return null;
}
//4、max函数
function max(){
    let node = this.root;
    if(node){
        while(node && node.right){
            node = node.right;
        }
        return node.key;
    }
    return null;
}
//5、inOrderTraverse,中序遍历(左、根、右)
function inOrderTraverse(){
    let res = [];
    inOrder(this.root, res);
    return res;
}
function inOrder(node, res){
    if(node){   
        inOrder(node.left, res);
        res.push(node.key);
        inOrder(node.right, res);
    }
}

//6、preOrderTraverse,先序遍历(根、左、右)
function preOrderTraverse(){
    let res = [];
    preOrder(this.root, res);
    return res;
}
function preOrder(node, res){
    if(node){   
        res.push(node.key);
        preOrder(node.left, res);
        preOrder(node.right, res);
    }
}

//7、postOrderTraverse,后序遍历(左、右、根)
function postOrderTraverse(){
    let res = [];
    postOrder(this.root, res);
    return res;
}
function postOrder(node, res){
    if(node){   
        postOrder(node.left, res);
        postOrder(node.right, res);
        res.push(node.key);
    }
}

//8、remove,移除一个节点
function remove(key){
    this.root = removeNode(this.root, key);
}
function removeNode(node, key){
    if(node == null){
        return null;
    }
    if(key < node.key){
        node.left = removeNode(node.left, key);
    }else if(key > node.key){
        node.right = removeNode(node.right, key);
    }else{   //移除根节点
        //1、没有节点
        if(node.left == null && node.right == null){
            node = null;
            return node;
        }else if(node.left == null){
            node = node.right;
            return node;
        }else if(node.right == null){
            node = node.left;
            return node;
        }else{
            //两个子节点都存在的情况,删除根节点
            //(取右侧的最小节点,将其值赋给根节点,再移除右子节点的那个最小节点)
            let tempNode= minNode(node.right);
            node.key = tempNode.key;
            node.right = removeNode(node.right, tempNode.key);
            return node;
        }
    }
    return node;
}
function minNode(node){
    while(node.left){
        node = node.left
    }
    return node;
}

/*测试插入节点和三种遍历二叉树的方式*/
let tree = new BinarySearchTree();
tree.insert(11); 
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3); 
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);
tree.insert(6); 
//返回中序遍历结果数组
console.log(tree.inOrderTraverse());   //[3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 18, 20, 25] 
//返回先序遍历结果数组
console.log(tree.preOrderTraverse());   //[11, 7, 5, 3, 6, 9, 8, 10, 15, 13, 12, 14, 20, 18, 25]
//返回后序遍历结果数组
console.log(tree.postOrderTraverse());     //[3, 6, 5, 8, 10, 9, 7, 12, 14, 13, 18, 25, 20, 15, 11]
//返回最小值
console.log(tree.min());    //3
//返回最大值
console.log(tree.max());    //25
//搜索特定值
console.log(tree.search(1));    //false
console.log(tree.search(8));    //true
//移除特定值
console.log(tree.search(15));   //true
tree.remove(15);
console.log(tree.search(15));   //false

7.3 平衡二叉树:

平衡二叉搜索树又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。二叉树的常用实现方法有:AVL、红黑树等。

平衡因子: 左子树减去右子树的高度,所以平衡二叉树的平衡因子取值只有:1、0、-1。

作用: 一般的二叉搜索树,其期望高度为(log2(n)),其各种操作的时间复杂度O(log2(n))也由此决定。但是,在某些极端情况下(如插入的序列是有序的),二叉搜索树将退化成类似的单链表,此时,其操作的时间复杂度将退化成线性的,即O(n)。在平衡二叉搜索树中,其高度一般都良好的维持在了O(log2(n)),大大降低了操作的时间复杂度。一棵好的平衡二叉树应该容易维护,也就是说,在做数据项的插入或删除操作时,为平衡树所做的一些辅助操作时间开销为O(1)。

7.3.1 平衡二叉树的构造:

在一棵二叉查找树中插入节点后,调整其为平衡二叉树。若向平衡二叉树中插入一个新节点后破坏了平衡二叉树的平衡性。首先要找出插入新节点后失去平衡的最小子树根节点的指针。然后再调整这个子树中有关节点之间的链接关系,使之成为新的平衡子树。当失去平衡的最小子树被调整为平衡子树后,原有其他的所有子树无需调整,整个二叉排序树就又成为了一棵平衡二叉树。

调整方法:

  • 插入点位置必须满足二叉查找树的性质,即任意一棵子树的左节点都小于根节点,右节点大于根节点。

  • 找出插入节点后,对不平衡的最小二叉树进行调整,如果是整个树不平衡,才进行整个树的调整。

调整方式:

  • (1) LL型

LL型:新节点插入位置为左子树的左孩子,需要进行右旋(向右旋转)
这里写图片描述
(图片来源见水印)
由于在A的左孩子B的左子树上插入结点F,使A的平衡因子由1变为2,成为不平衡的最小二叉树根结点。此时A结点顺时针右旋转,旋转过程中遵循“旋转优先”的规则,A结点替换D结点成为B结点的右子树,D结点成为A结点的左孩子。

  • (2) RR型

RR型:新节点插入位置为右子树的右孩子,需要进行左旋(向左旋转)
这里写图片描述
由于在A的右子树C的右子树插入了结点F,A的平衡因子由-1变为-2,成为不平衡的最小二叉树根结点。此时,A结点逆时针左旋转,遵循“旋转优先”的规则,A结点替换D结点成为C的左子树,D结点成为A的右子树。

  • (3) LR型

LR型: 新节点插入位置为左子树的右孩子,要进行两次旋转,先左旋转,再右旋转;第一次最小不平衡子树的根节点先不动,调整插入节点所在子树,使之形成LL型树;第二次再调整LL型树,形成平衡树。
这里写图片描述
由于在A的左子树B的右子树上插入了结点F,A的平衡因子由1变为了2,成为不平衡的最小二叉树根结点。第一次旋转A结点不动,先将B的右子树的根结点D向左上旋转提升到B结点的位置,然后再把该D结点向右上旋转提升到A结点的位置。

  • (4) RL型

RL型: 插入位置为右子树的左孩子,进行两次调整,先右旋转再左旋转;第一次最小不平衡子树的根节点先不动,调整插入节点所在子树,使之形成RR型树;第二次再调整RR型树,形成平衡树。
这里写图片描述
由于在A的左子树C的左子树上插入了结点F,A的平衡因子由-1变为了-2,成为不平衡的最小二叉树根结点。第一次旋转A结点不动,先将C的右子树的根结点D向右上旋转提升到C结点的位置,然后再把该D结点向左上旋转提升到A结点的位置。

上述旋转原理讲解来源:平衡二叉树 构造方法

上面介绍了四种操作方法,但具体什么情况使用什么操作,举例说明:当节点的平衡因子大于2时,说明是左子树深度偏大,此时需要右旋或者左右旋。右旋操作不仅会使父节点降低平衡因子,也会降低左节点的平衡因子。所以当父节点平衡因子为2,而左子节点的平衡因子小于0时,如果使用右旋,得到的二叉树还是一个不平衡的二叉树,所以需要先对左子树进行左旋,然后整体右旋,即左右旋操作。

这里,可以给出平衡二叉树的伪代码:

if 平衡因子 >=  2
    if  左节点的平衡因子  <  0    //LR型
        左右旋
    else
        右旋           //LL型

else if 平衡因子 <=  -2
    if  右节点的平衡因子  >  0    //RL型
        右左旋
    else
        左旋           //RR型

实际代码:

function AVLTree(){
    this.Node = Node;    //创建节点
    let root = null;
    this.insert = insert;    //insert(key):向树中插入一个新的键。
    this.remove = remove;    //remove(key):从树中移除某个键。
}
function Node(key){
    this.key = key;
    this.left = null;
    this.right = null;
}
//获取树的高度
function height(node){
    let hLeft = 0;  //左子树高度
    let hRight = 0;  //右子树高度
    if(node.left){
        hLeft = height(node.left);
    }
    if(node.right){
        hRight = height(node.right);
    }
    return hLeft > hRight ? ++hLeft : ++hRight;
}
//获取树的平衡因子
function balanceFactor(node){
    let bf = 0;
    if(node.left){
        bf += height(node.left);
    }
    if(node.right){
        bf -= height(node.right);
    }
    return bf;
}
//1、insert(key):向树中插入一个新的键
function insert(key){
    let newNode = new this.Node(key);
    if(this.root == null){
        this.root = newNode;
    }else{
        insertNode(this.root, newNode);
        this.root = balanceNode(this.root);   //对树进行调整,使之保持是平衡二叉树
    }
}
//运用递归
function insertNode(node, newNode){
    if(newNode.key < node.key){
        if(node.left == null){
            node.left = newNode;
        }else{
            insertNode(node.left, newNode);
        }
    }else{
        if(node.right == null){
            node.right = newNode;
        }else{
            insertNode(node.right, newNode);
        }
    }
}
function balanceNode(node){
    //1、递归平衡左右子树
    if(node.left){
        node.left = balanceNode(node.left);
    }
    if(node.right){
        node.right = balanceNode(node.right);
    }
    //2、计算平衡因子
    let bf = balanceFactor(node);
    //运用伪代码中的方案
    if(bf >= 2){
        if(balanceFactor(node.left) < 0){
            node = leftRightRotate(node);    //LR
        }else{
            node = rightRotate(node);        //LL
        }
    }else if(bf <= -2){
        if(balanceFactor(node.right) > 0){
            node = rightLeftRotate(node);     //RL
        }else{
            node = leftRotate(node);          //RR
        }
    }
    return node;
}
//LL型、右旋(a为根节点,右旋后,a的左孩子b变成根节点,结合图理解)
function rightRotate(node){
    let a = node;
    let b = a.left;
    a.left = b.right;
    b.right = a;
    return b;
}
//LR型、左右旋
/*根节点a的左孩子b的右孩子c的右节点最后成为a的右节点,c的左节点变成b的右节点。
然后b变成c的左孩子,a变成c的右孩子。节点为null,就等同于赋值了null,牢记旋转优先的规则*/
function leftRightRotate(node){
    let a = node;
    let b = a.left;
    let c = b.right;

    a.left = c.right;
    b.right = c.left;
    c.left = b;
    c.right = a;
    return c;
}
//RL型、右左旋
/*旋转过程与LR类似,自行画图理解即可*/
function rightLeftRotate(node){
    let a = node;
    let b = a.right;
    let c = b.left;

    a.right= c.left;
    b.left= c.right;
    c.left = a;
    c.right = b;
    return c;
}
//RR型、左旋
function leftRotate(node){
    let a = node;
    let b = a.right;
    a.right = b.left;
    b.left= a;
    return b;
}

//删除
function remove(key){
    this.root = removeNode(this.root, key);
    this.root = balanceNode(this.root);   //对树进行调整,使之保持是平衡二叉树
}
function removeNode(node, key){
    if(node == null){
        return null;
    }
    if(node.key < key){
        node.right = removeNode(node.right, key);
    }else if(node.key > key){
        node.left = removeNode(node.left, key);
    }else{
        if(node.left == null && node.right == null){
            node = null;
            return node;
        }else if(node.left == null){
            node = node.right;
            return node;
        }else if(node.right == null){
            node = node.left;
            return node;
        }else{
            let tempNode = minNode(node.right);
            node.key = tempNode.key;
            node.right = removeNode(node.right, tempNode.key);
            return node;
        }
    }
    return node;
}
function minNode(node){
    while(node.left){
        node = node.left;
    }
    return node;
}

将二叉搜索树的插入与平衡二叉树的插入做个比较,得出结果:

/*测试插入节点和中序遍历二叉树的结果*/
let AVLtree = new AVLTree();
AVLtree .insert(1); 
AVLtree .insert(2);
AVLtree .insert(3);
AVLtree .insert(4);
AVLtree .insert(5); 
AVLtree .insert(6);
AVLtree .insert(7);
AVLtree .insert(8);
AVLtree .insert(9);
console.log(height(AVLtree.root));    //4
console.log(balanceFactor(AVLtree.root));    //-1
console.log(AVLtree.inOrderTraverse());     //[1, 2, 3, 4, 5, 6, 7, 8, 9]

这里写图片描述
AVL树结构

AVLtree .remove(2);
AVLtree .remove(1);
console.log(height(AVLtree.root));    //3
console.log(balanceFactor(AVLtree.root));    //0
console.log(AVLtree.inOrderTraverse()); //[3, 4, 5, 6, 7, 8, 9]

删除了两个节点2、1,破坏了平衡二叉树的平衡结构,经过调整,再次平衡,结构如下图所示:
这里写图片描述

//二叉搜索树结果比较:
let tree = new BinarySearchTree();
tree.insert(1); 
tree.insert(2);
tree.insert(3);
tree.insert(4);
tree.insert(5); 
tree.insert(6);
tree.insert(7);
tree.insert(8);
tree.insert(9);
console.log(height(tree.root));        //9
console.log(balanceFactor(tree.root));  //-8
console.log(tree.inOrderTraverse());    //[1, 2, 3, 4, 5, 6, 7, 8, 9]

二叉搜索树就是类似单向链表了,可以看见,经过平衡,不会改变中序遍历结果。
参考文章链接:大话数据结构之平衡二叉树

7.4 红黑树:

红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似于平衡的。

树的每个结点包含5个属性:color、key、left、right、p(父结点)。如果一个结点没有子节点或父节点,则该结点相应指针属性的值为NIL,我们可以把这些NIL视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的节点视为树的内部结点。
这里写图片描述
或者所有的NIL结点都用一个T.NIL代替:
这里写图片描述
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。

一棵红黑树是满足以下性质的二叉搜索树:

  • 1、每个节点或是红色的,或是黑色的;
  • 2、根节点是黑色的;
  • 3、每个叶结点(NIL)是黑色的;
  • 4、如果一个结点是红色的,则它的两个子结点是黑色的;
  • 5、对每个结点,从该结点到其所有后代叶结点的所有路径上,均包含相同数目的黑色结点。

红黑树也是二三树的实现,原理可以看这篇博文:查找(一)史上最简单清晰的红黑树讲解

7.4.1 红黑树的构建:

红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。红黑树的旋转主要是左旋和右旋,可以参考平衡二叉树中的旋转。
这里写图片描述

插入结点的步骤:

  • 1、将红黑树当做一棵二叉查找树,将结点插入;
  • 2、将插入的结点着色为“RED”;
  • 3、通过一系列的旋转或者着色等操作,使之重新成为一棵红黑树。

插入结点的修正:

关于红黑树满足的五条性质中,由于我们新插入的结点为红色,所以1、3、5性质是不会被破坏的。当插入结点到一棵空树时,会破坏性质2,此时改变根结点颜色即可;当插入结点的父节点也是红色时,性质4就被破坏,此时调整需要分为三种情况考虑(都已默认插入结点的父结点为红色):

  • 情况一、z的叔结点y是红色,其中z为要调整的结点:
    由于z的父结点是红色,所以z的祖父结点一定是黑色(因为未插入前满足红黑性),对这种情况,处理办法: 将z的父结点和叔结点都染黑,而将z的祖父结点染红。如下图所示:
    这里写图片描述
    由图可知,我们并不能确定祖父结点(21)的父结点(9)的颜色,当它为红色时,我们经过修正把结点21染成红色,红黑树还是在结点21处破坏了性质4。所以,需要把z指向z的祖父结点位置(结点21),然后继续进入循环。

  • 情况二、z的叔结点y是黑色的且z是一个右孩子:
    处理办法: 利用旋转将情况二转换成情况三,但是我们不希望z的位置发生变化,所以先让z指向z的父结点,再以z为支点进行左旋,如下图所示:
    这里写图片描述
    这样,情况二就转变成了情况三的问题。

  • 情况三、z的叔结点y是黑色的且z是一个左孩子:
    处理办法: 将z的父结点染黑,将z的祖父结点染红,然后以z的祖父结点为支点进行右旋。如下图所示:
    这里写图片描述
    由上图可知,通过将21染黑和26染红,我们可以解决性质4,但是却让原本26-39这条路径上少了一个黑色结点,一定会破坏性质5,所以通过以26为支点进行右旋,让21上升到祖父结点,这样从21-39的路径上黑色结点与以前一样,性质5得到保证。
    上述讲解来源:[【算法导论】红黑树详解之一(插入)]
    (https://blog.csdn.net/cyp331203/article/details/42677833)
    上述举例均以在左边插入新结点为例,实际上,在右边插入新节点与左边是对称的,具体可查看红黑树的插入详解及Javascript实现方法示例得出结论:当插入结点的父结点与插入结点同为左子树与右子树时,就是情况3;当两者一左一右时,就是情况2,我们需要先进行旋转将其转化成情况3。

红黑树的删除情况比插入更加复杂,但是看了这篇博文红黑树:删除是属于讲的简洁易懂的,把删除总结为了6种情况。

//红黑树的构建
function RBTree(){
    this.Node = Node;    //创建节点
    let root = null;
    this.insert = insert;    //insert(key):向树中插入一个新的键。
    this.insertFixNode = insertFixNode;  //插入的修正函数
    this.remove = remove;    //remove(key):从树中移除某个键。
    this.removeNode = removeNode;
    this.RBremoveFix = RBremoveFix;  //remove的修正函数
    this.inOrderTraverse = inOrderTraverse;  //inOrderTraverse:通过中序遍历方式遍历所有节点。
}
let nil= new NIL();   //哨兵
function Node(key){

    this.key = key;
    this.left = nil;
    this.right = nil;
    this.color = 'red';  //结点颜色默认为红色
    this.parent = nil;  //指向父节点
}
function NIL(){
    this.key = 'nil';
    this.color = 'black';  //结点颜色默认为红色
    this.parent = null;  //指向父节点
}
//1、insert(key):向树中插入一个新的键
function insert(key){
    let newNode = new this.Node(key);
    if(this.root == null){
        this.root = newNode;
    }else{
        insertNode(this.root, newNode);
    }
    this.insertFixNode(newNode);
}
//运用递归
function insertNode(node, newNode){
    if(newNode.key < node.key){
        if(node.left == nil){
            node.left = newNode;
            newNode.parent = node;
        }else{
            insertNode(node.left, newNode);
        }
    }else{
        if(node.right == nil){
            node.right = newNode;
            newNode.parent = node;
        }else{
            insertNode(node.right, newNode);
        }
    }
}
//对结点进行修正
function insertFixNode(node){
    //node.parent颜色为red时,为情况2和3;
    while(node.parent !== nil && node.parent.color == 'red'){
        let father = node.parent;
        let grandFather = father.parent;
        let uncle = grandFather[grandFather.left == father ? 'right' : 'left'];  //父节点的兄弟节点
        if(uncle == nil || uncle.color == 'black'){
            let drctionFromFatherNode = (father.left == node)? 'left' : 'right';
            let dirctionFromGrandNode = (grandFather.left == father) ? 'left' : 'right';
            //判断插入结点与其父节点的方向,看是情况2还是情况3
            if(drctionFromFatherNode == dirctionFromGrandNode){   //情况3
                rotateNode(father);  //将左旋右旋合并为一个函数
                father.color = 'black';   //变色处理
                grandFather.color = 'red';
            }else{
                //情况2
                rotateNode(node);  //第一次旋转,将情况2转为情况3
                rotateNode(node);  //情况3的旋转处理
                node.color = 'black';
                grandFather.color = 'red';
            }
            break;  //完成插入,跳出循环
        }else{  //情况1
            grandFather.color = 'red';
            father.color = 'black';
            uncle.color = 'black';
            node = grandFather;  //将node指向祖父结点,继续循环
        }
    }
    //当node.parent不存在时,即为情形1;必须放后边,当递归到根节点为红色时,需修正为黑色
    if(node.parent == nil){
        node.color = 'black';
        this.root = node;   //旋转之后,把更改后的根节点赋给root。
    }
}
//旋转:与平衡二叉树的旋转相比,这里还要注意节点的父节点的指向
function rotateNode(node){  //都是以子结点为支点
    let y = node.parent;
    if(y.right == node){   //右孩子左旋
        if(y.parent !== nil){  //祖父结点,经过旋转,node应该变成祖父结点的子结点
            y.parent[y.parent.left == y ? 'left' : 'right'] = node; 
        }
        node.parent = y.parent;
        y.right = node.left;
        node.left.parent = y;   
        node.left = y;
        y.parent = node;  
    }else{   //左孩子右旋
        if(y.parent !== nil){
            y.parent[y.parent.left == y ? 'left' : 'right'] = node;
        }
        node.parent = y.parent;
        y.left = node.right;
        if(node.right !== nil){
            node.right.parent = y;  
        }
        node.right = y;
        y.parent = node;
    }
}
let arr = [11, 2, 14, 1, 7, 15, 5, 8, 4, 16];
let tree = new RBTree();
arr.forEach(i => tree.insert(i));
console.log(tree.root);
console.log(tree.inOrderTraverse());

//2、remove函数,删除
function remove(key){
    this.root = this.removeNode(this.root, key);
}
function removeNode(node, key){
    if(node == null){
        return null;
    }
    let x = null; //指向要代替被删除结点的结点
    let y = node;
    let yColor = node.color;  //记录要被删除结点的颜色
    if(node.key < key){
        node.right = this.removeNode(node.right, key);
    }else if(node.key > key){
        node.left = this.removeNode(node.left, key);
    }else{
        if(node.left == nil && node.right == nil){
            x = node.right;
            RBTransplant(node,node.right);
        }else if(node.left == nil){
            x = node.right;
            RBTransplant(node,node.right);
        }else if(node.right == nil){
            x = node.left;
            RBTransplant(node,node.left);
        }else{
            let tempNode = minNode(node.right);
            node.key = tempNode.key;
            node.right = this.removeNode(node.right, tempNode.key);
        }
    }
    if(x && yColor == 'black'){  //如果被删除的结点为红色,则可以直接用黑色子结点代替,无需修正
        this.RBremoveFix(x);   //对树进行调整,使之保持是红黑树
    }
    return node;
}
function minNode(node){
    while(node.left !== nil){
        node = node.left;
    }
    return node;
}
function RBTransplant(supNode,subNode){
    if(supNode.parent == nil){
        supNode = subNode;
    }else if(supNode.parent.left == supNode){
        supNode.parent.left = subNode;
    }else{
        supNode.parent.right = subNode; 
    }
    subNode.parent = supNode.parent;
}
function RBremoveFix(X){
    while(X.parent !==  nil && X.color == 'black'){
        if(X.parent.left == X){  //要替代结点X是其父节点的左孩子
            let S = X.parent.right;   //得到X的兄弟节点
            if(S.color == 'red'){  //情况5
                //此时父节点一定是黑色:在保证黑高的情况下,我们通过染色和旋转来进行修正
                S.color = 'black';
                X.parent.color = 'red';
                rotateNode(S);  //右孩子左旋
                S = X.parent.right;
            }
            if(S.left.color == 'black' && S.right.color == 'black'){  //S本身也是黑色,属于情况2
                //通过染色将X上移一层,如果X.parent是根节点或颜色是红色,则结束循环
                S.color = 'red';
                X = X.parent;
            }else{
                if(S.right.color == 'black'){  //情况4,兄弟节点及兄弟节点右孩子是黑色,左孩子是红色,染色和旋转可变成情况3
                    S.color = 'red';
                    S.left.color = 'black';
                    rotateNode(S.left);
                    S = X.parent.right;
                }
                //情况4直接转为情况3
                S.color = S.parent.color;
                S.parent.color = 'black';
                S.right.color = 'black';
                rotateNode(S);
                X = this.root;
            }
        }else{  //X是父结点的右孩子的情况
            let S = X.parent.left;
            if(S.color == 'red'){   //情况5的反向
                S.parent.color = 'red';
                S.color = 'black';
                rotateNode(S);
                S = X.parent.left;
            }else if(S.left.color == 'black' && S.right.color == 'black'){
                S.color = 'red';
                X = X.parent;   
            }else{
                if(S.left.color == 'black'){
                    S.right.color = 'black';
                    S.color = red;
                    rotateNode(S);
                    S = X.parent.left;
                }
                S.color = X.parent.color;
                X.parent.color = 'black';
                S.left.color = 'black';
                rotateNode(S);  
                x = this.root;
            }
        }   
    }
    X.color = 'black';  //修正根节点颜色为黑色
}
//inOrderTraverse,中序遍历(左、根、右)
function inOrderTraverse(){
    let res = [];
    inOrder(this.root, res);
    return res;
}
function inOrder(node, res){
    if(node){   
        inOrder(node.left, res);
        res.push(node.key);
        inOrder(node.right, res);
    }
}

//测试结果
tree.remove(1);
console.log(tree.root);
console.log(tree.inOrderTraverse());

输出结果图:这里写图片描述
红黑树的删除还有问题,心累。。。。

做题还碰到过哈夫曼树,可以了解下:哈夫曼树和哈夫曼编码

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值