看了 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());
输出结果图:
红黑树的删除还有问题,心累。。。。
做题还碰到过哈夫曼树,可以了解下:哈夫曼树和哈夫曼编码