二叉树与红黑树的学习总结
先附上学习地址
一、二叉树的概念特性等
- 二叉树的五种结构
- 特性:
- 一个二叉树第i层的最大节点数为 2^(i - 1) ,i >= 1
- 深度为k的二叉树的最大节点总数为 2^k -1,i >= 1
- 对于任何非空二叉树T,若n0表示叶子节点的个数,n2是度为2的非叶子节点,那么两者关系满足 n0 = n2 + 1
- 二叉树的存储常见的是数组和链表,对于完全二叉树按从上至下,从左到右的方式存储,对于非完全二叉树来说,需要转化为完全二叉树,即把缺少的节点置为null,再进行存储,这样会造成很大的空间浪费
最常见的还是使用链表进行存储
对于的树的基本概念,这里就不多进行赘述,我们学习的重点在于对二叉树的操作
二、二叉搜索树(Binary Search Tree)的操作
- BST的封装
// 创建BinarySearchTree类
function BinarySearchTree(){
// 创建节点构造函数
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 保存根的属性
this.root = null
// 相关操作方法
}
- 插入操作
BinarySearchTree.prototype.insert = function (key) {
// 根据key创建对应的node
const node = new Node(key);
// 判断根节点是否有值
if (this.root === null) {
this.root = node;
} else {
this.insertNode(this.root, node);
}
};
BinarySearchTree.prototype.insertNode = function (node, newNode) {
if (newNode.key < node.key) {//往左找
if (node.left === null) {
node.left = newNode
} else {
this.insertNode(node.left,newNode)
}
} else {//往右找
if (node.right === null) {
node.right = newNode
} else {
this.insertNode(node.right,newNode)
}
}
};
- 遍历方式
- 先序遍历(递归实现)
// 先序遍历
BinarySearchTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler) {
if (node !== null) {
handler(node.key)
this.preOrderTraversalNode(node.left,handler)
this.preOrderTraversalNode(node.right,handler)
}
}
测试
let bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
let res = ''
bst.preOrderTraversal(function(node){
res += node + ' '
})
console.log(res);//11 7 5 3 9 8 10 15 13 12 14 20 18 25
- 中序遍历(递归实现)
// 中序遍历
BinarySearchTree.prototype.minOrderTraversal = function (handler) {
this.minOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.minOrderTraversalNode = function (node,handler) {
if (node !== null) {
this.minOrderTraversalNode(node.left,handler)
handler(node.key)
this.minOrderTraversalNode(node.right,handler)
}
}
测试
let res2 = ''
bst.minOrderTraversal(function(node){
res2 += node + ' '
})
console.log(res2);//3 5 7 8 9 10 11 12 13 14 15 18 20 25
- 后序遍历(递归实现)
// 后序遍历
BinarySearchTree.prototype.postOrderTraversal = function (handler) {
this.postOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.postOrderTraversalNode = function (node,handler) {
if (node !== null) {
this.postOrderTraversalNode(node.left,handler)
this.postOrderTraversalNode(node.right,handler)
handler(node.key)
}
}
测试
let res3 = ''
bst.postOrderTraversal(function(node){
res3 += node + ' '
})
console.log(res3);//3 5 8 10 9 7 12 14 13 18 25 20 15 11
- 层序遍历(非递归实现,利用队列)
BinarySearchTree.prototype.levelOrderTraversal = function (){
let node = this.root
if (!node) {
return false
}
let queue = [node]
let pointer = 0
let res = ''
while(pointer < queue.length){
node = queue[pointer++]
res += `${node.key} `
if (node.left) {
queue.push(node.left)
}
if (node.right) {
queue.push(node.right)
}
}
return res
}
测试
console.log('bfs',bst.levelOrderTraversal())//bfs 11 7 15 5 9 13 20 3 8 10 12 14 18 25
- 最大值
BinarySearchTree.prototype.max = function(){
let node = this.root
while(node !== null){
if (node.right) {
node = node.right
}else{
return node.key
}
}
return false
}
console.log('max',bst.max())//max 25
- 最小值
BinarySearchTree.prototype.min = function(){
let node = this.root
while(node !== null){
if (node.left) {
node = node.left
}else{
return node.key
}
}
return false
}
console.log('min',bst.min())//min 3
- 搜索特定值
BinarySearchTree.prototype.search = function (key) {
return this.searchNode(this.root, key);
};
BinarySearchTree.prototype.searchNode = function (node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
};
console.log("search", bst.search(13));//search true
console.log("search", bst.search(20));//search true
console.log("search", bst.search(100));//search false
console.log("search", bst.search(68));//search false
- 删除节点
这个操作比较麻烦,首先要找到节点,删除的节点分为三种情况:
第一种是删除的是叶子节点,这种情况直接删除就好
第二种情况是删除的是有一个子节点的节点,这种情况删除节点后还要把删除节点的子节点与删除节点的父节点相连
第三种情况是删除的有两个子节点的节点,这个时候就要寻找删除节点的前驱或者后继节点与删除节点进行替换,前驱节点即删除节点的左子树最大值,后继为右子树的最小值,前驱和后继节点的值是最接近删除节点的值的,可以保证该树仍为二叉树,这里以寻找后继为例
BinarySearchTree.prototype.remove = function(key){
let isLeftChild = true
let parent = null
let current = this.root
// 寻找节点
while(current.key !== key){
parent = current
if (key < current.key) {
current= current.left
isLeftChild = true
} else {
current = current.right
isLeftChild = false
}
if (current === null) {
return false
}
}
// 删除节点
// 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.删除一个子节点的节点
else if(current.left === null && current.right !== null){
if (current === this.root) {
this.root = current.right
}else if (isLeftChild) {
parent.left = current.right
} else {
parent.right = current.right
}
}
else if(current.right === null && current.left !== null){
if (current === this.root) {
this.root = current.left
}else if (isLeftChild) {
parent.left = current.left
} else {
parent.right = current.left
}
}
// 3.删除两个子节点的节点
else {
// 获取后继
let successor = this.getSuccessor(current)
if (current == this.root) {
this.root = successor
} else if(isLeftChild){
parent.left = successor
}else{
parent.right =successor
}
successor.left = current.left
}
}
// 寻找后继节点
// 后继节点分为两种情况,一种是后继节点为delNode.right,另一种为不是delNode.right
BinarySearchTree.prototype.getSuccessor = function(delNode){
let successor = delNode
let current = delNode.right
let successorParent = delNode;
while(current !== null){
successorParent = successor;
successor = current
current = current.left
}
// 判断寻找的后继结点是否是delNode的right结点
if (successor !== delNode.right) {
successorParent.left = successor.right;
successor.right = delNode.right;
}
return successor
}
测试
console.log("广度优先删除前:", bst.levelOrderTraversal());//广度优先删除前: 11 7 15 5 9 13 20 3 8 10 12 14 18 25
bst.remove(5);
bst.remove(9);
bst.remove(13);
bst.remove(20);
console.log("广度优先删除后:", bst.levelOrderTraversal());//广度优先删除后: 11 7 15 3 10 14 25 8 12 18
三、红黑树
对于一个平衡二叉树来说,插入/查找的效率为O(logN),在最坏情况下,插入有序的值,平衡二叉树会转变成一个链表,这时查找的效率变为O(N),为了能以较快的时间来操作一棵树,我们需要保证树总是尽量平衡的。
常见的平衡树有AVL树和红黑树,两者的时间复杂度都是O(logN),但是AVL树整体效率不如红黑树。
红黑树的规则
- 节点是黑色或者红色的
- 根节点为黑色
- 每个叶子节点是黑色的空节点(NIL节点)
- 每个红色节点的两个子节点都是黑色的(从根到叶子的路径上不能有连续的红色节点)
- 从任意节点到其每个叶子节点的所有路径上都包含相同数目的黑色节点
这些规则确保了红黑树的关键特性:
从根到叶子的最长可能路径,不会超过最短可能路径的两倍长
结果这个树就是基本平衡的,虽然没有做到绝对的平衡,但是在最坏的情况下,依然是高效的
为什么可以做到最长路径不超过最短路径的两倍呢?
1. 性质4决定了路径不能有连续的红色节点
2. 最短路径可能都是黑色节点
3. 最长路径可能都是红黑交替
4. 性质5所有路径都有相同数目的黑色节点
5. 这就表明了没有路径能多于其他路径的两倍长
- 红黑树的变换
插入一个新节点时,有可能树不再平衡,可以通过三种方式让树变得平衡:-
换色,把红色节点变为黑色或者把黑色节点变为红色;
插入节点通常默认为红色,因为在插入红色节点时,有可能插入一次是不违反红黑树任何规则的,而插入黑色节点,必然使路径上多了一个黑色节点,这是很难调整的,而红色节点导致的红红相连的情况可以通过换色和旋转来调整 -
左旋转
-
右旋转
-
- 变换规则
设要插入的节点为N,其父节点为P
其祖父节点为G,其父亲的兄弟节点为U(即P和U是同一个节点的子节点)
- 情况1:
- 新节点位于根上,没有父节点
- 这种情况下,我们直接将红色换成黑色即可,这样满足性质2
- 情况2:
- 新节点的父节点P是黑色
- 这种情况下直接插入即可
- 情况3:
- P为红色,U也为红色,此时G必为黑色
- 将P和U换成黑色,并且将G换为红色
- 如果G的父节点为红色,可以递归调整颜色
- 如果递归到根节点上,就需要进行旋转了
- 情况4:
- N的叔叔节点U为黑色,P为红色,且N是左孩子,此时G必为黑色
- 这种情况下对将G变为红色,P变为黑色,再进行右旋转
- 情况5:
- N的叔叔节点U为黑色,P为红色,且N是右孩子,此时G必为黑色
- 对P进行左旋转,变成情况4
- 再对G进行右旋转并且改变颜色即可
- 案例练习
案例练习