树的相关术语
- 根节点:位于树顶部的节点叫作根节点,它没有父节点,树中的每个元素都叫作节点。
- 内部节点:至少有一个子节点称为内部节点
- 外部节点或叶节点:没有子元素的节点称为外部节点或叶节点
- 节点的深度:节点的深度取决于它的祖先的节点的数量。
- 树的高度:取决于所有节点深度的最大值
二叉树与二叉搜索树(BST)
- 二叉树:二叉树中的节点最多只能有两个子节点:一个是左侧子节点,另一个是右侧子节点。
- 二叉搜索树:是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值, 在右侧节点存储(比父节点)大(或者等于)的值
ES6语法实现二叉搜索树(BST)
首先创建一个BinarySearchTree
类
需要实现的一些方法如下:
- insert(key):向树中插入一个新的键。
- inOrderTraverse:通过中序遍历方式遍历所有节点,对树进行排序操作。
- preOrderTraverse:通过先序遍历方式遍历所有节点,一个应用是打印一个结构化的文档。
- postOrderTraverse:通过后序遍历方式遍历所有节点,后序遍所有节点,一个应用是计算一个目录和它的子目录中所有文件所占空间的大小。
- search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回 false。
- min:返回树中最小的值/键。
- max:返回树中最大的值/键。
- remove(key):从树中移除某个键。
class BinarySearchTree {
constructor() {
this.root = null
}
insert(key) {
this.root = insertNode(this.root, key)
}
search(key) {
return searchNode(this.root, key)
}
// 中序遍历所有节点,对树进行排序操作
inOrderTraverse(cb) {
inOrderTraverseNode(this.root, cb)
}
// 先序遍历所有节点,一个应用是打印一个结构化的文档
preOrderTraverse(cb) {
preOrderTraverseNode(this.root, cb)
}
// 后序遍所有节点,一个应用是计算一个目录和它的子目录中所有文件所占空间的大小
postOrderTraverse(cb) {
postOrderTraverseNode(this.root, cb)
}
// 返回数中最小的值/键
min() {
return minNode(this.root)
}
// 返回树中最大的值/键
max() {
return maxNode(this.root)
}
// 移除某个键
remove(key) {
removeNode(this.root, key)
}
}
insert 向树中插入一个键的功能
// 创建树节点
class Node {
constructor(key) {
this.key = key
this.left = null
this.right = null
}
}
// 递归插入节点
function insertNode(node, element) {
if (node === null) {
node = new Node(element)
} else if (element < node.key) {
node.left = insertNode(node.left, element)
} else if (element > node.key) {
node.right = insertNode(node.right, element)
}
return node
}
const 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(JSON.stringify(tree.root,null,2))
// 打印结果如下
{
"key": 7,
"left": {
"key": 5,
"left": { "key": 3, "left": null, "right": null },
"right": { "key": 6, "left": null, "right": null }
},
"right": {
"key": 15,
"left": {
"key": 9,
"left": { "key": 8, "left": null, "right": null },
"right": {
"key": 10,
"left": null,
"right": {
"key": 13,
"left": { "key": 12, "left": null, "right": null },
"right": { "key": 14, "left": null, "right": null }
}
}
},
"right": {
"key": 20,
"left": { "key": 18, "left": null, "right": null },
"right": { "key": 25, "left": null, "right": null }
}
}
}
inOrderTraverse 中序遍历操作(left==>selft==>right)
// 中序遍历 left==>self==>right的顺序访问所有节点
function inOrderTraverseNode(node, cb) {
if (node) {
inOrderTraverseNode(node.left, cb)
cb(node.key)
inOrderTraverseNode(node.right, cb)
}
}
tree.inOrderTraverse((key) => {
console.log(key)
})
// 打印结果如下
3
5
6
7
8
9
10
11
12
13
14
15
18
20
25
preOrderTraverse 先序遍历(self==>left==>right)
// 先序遍历 self==>left==>right顺序访问所有节点
function preOrderTraverseNode(node, cb) {
if (node) {
cb(node.key)
preOrderTraverseNode(node.left, cb)
preOrderTraverseNode(node.right, cb)
}
}
tree.inOrderTraverse((key) => {
console.log(key)
})
// 打印结果如下
11
7
5
3
6
9
8
10
15
13
12
14
20
18
25
postOrderTraverse 后序遍历(left==>right==>self)
// 后序遍历 left==>right==>self顺序访问所有节点
function postOrderTraverseNode(node, cb) {
if (node) {
postOrderTraverseNode(node.left, cb)
postOrderTraverseNode(node.right, cb)
cb(node.key)
}
}
tree.postOrderTraverse((key) => {
console.log(key)
})
// 打印结果
3
6
5
8
10
9
7
12
14
13
18
25
20
15
11
search搜索树中的值
-
搜索最小值
// 寻找最小的节点 function minNode(node) { if (!node) return null while (node.left) { node = node.left } return node.key }
-
搜索最大值
// 寻找最大的节点 function maxNode(node) { if (!node) return null while (node.right) { node = node.right } return node.key }
-
搜索特定值
// 搜索节点 function searchNode(node, key) { if (!node || !key) return null if (node.key > key) { return searchNode(node.left, key) } else if (node.key < key) { return searchNode(node.right, key) } else { return true } }
移除一个节点
-
移除一个叶节点
-
移除有一个左侧或右侧子节点的节点
- 移除有两个子节点的节点
// 找到最小的节点
function findMinNode(node) {
while (node && node.left) {
node = node.left
}
return node
}
// 移除节点
function removeNode(node, key) {
if (!node) return null
if (node.key > key) {
node.left = removeNode(node.left, key)
return node
} else if (node.key < key) {
node.right = removeNode(node.right, key)
return node
} else {
// 一个叶节点
if (node.left === null && node.right === null) {
node = null
return node
}
// 一个只有一个子节点的节点
if (node.left === null) {
node = node.right
return node
} else if (node.right === null) {
node = node.left
return node
}
// 一个有两个子节点的节点
// 1、找到右侧子树最小的节点
// 2、并用这个节点去更新这个节点的值
// 3、同时把右侧子树中的最小节点移除
// 4、最后,向它的父节点返回更新后节点的引用
const aux = findMinNode(node.right)
node.key = aux.key
node.right = removeNode(node.right, aux.key)
return node
}
}
自平衡树(AVL树)
AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树。
二叉搜索树存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一 条分支会有很多层,而其他的分支却只有几层
解决办法:
-
右----右(RR):向左的单旋转
假设向上图的树插入节点90,这会造成树失衡(节点50-Y高度为-2),因此需要恢复树的平衡。 下面是我们执行的操作:-
与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为2)所在 的位置(行{1})
-
节点X的右子树保持不变
-
将节点Y的右子节点置为节点X的左子节点Z(行{2})
-
将节点X的左子节点置为节点Y(行{3})
-
-
左----左(LL):向右的单旋转
假设向上图的树插入节点5,这会造成树失衡(节点50 Y高度为+2),需要恢复树的平衡。下 面是我们执行的操作:- 与平衡操作相关的节点有三个(X、Y、Z),将节点X置于节点Y(平衡因子为+2)所在 的位置(行{1})
- 节点X的左子树保持不变
- 将节点Y的左子节点置为节点X的右子节点Z(行{2})
- 将节点X的右子节点置为节点Y(行{3})
-
左----右(LR):向右的双旋转
假设向上图的树插入节点35,这会造成树失衡(节点50 Y高度为+2),需要恢复树的平衡。下 面是我们执行的操作:-
将节点X置于节点Y(平衡因子为+2)所在的位置
-
将节点Y的左子节点置为节点X的右子节点
-
将节点Z的右子节点置为节点X的左子节点
-
将节点X的右子节点置为节点Y
-
将节点X的左子节点置为节点Z
基本上,就是先做一次RR旋转,再做一次LL旋转
-
-
右----左(RL):向左的双旋转
假设向上图的树插入节点75,这会造成树失衡(节点70 Y高度为2),需要恢复树的平衡。下 面是我们执行的操作:- 将节点X置于节点Y(平衡因子为2)所在的位置
- 将节点Z的左子节点置为节点X的右子节点
- 将节点Y的右子节点置为节点X的左子节点
- 将节点X的左子节点置为节点Y
- 将节点X的右子节点置为节点Z
计算平衡因子
在AVL树中,需要对每个节点计算右子树高度(hr)和左子树高度(hl)的差值,该值(hr-hl)应为0、1或1。如果结果不是这三个值之一,则需要平衡该AVL树。这就是平衡因子的概念。
下图举例说明了一些树的平衡因子(所有的树都是平衡的):
// 计算节点树的高度
function heightNode(node) {
if (node === null) {
return -1
} else {
return Math.max(heightNode(node.left), heightNode(node.right)) + 1
}
}
在插入节点时,计算平衡因子,做一下平衡操作,让我们来改造一下insert方法
// 左-左(LL):向右的单旋转
function rotationLL(node) {
var temp = node.left
node.left = temp.right
temp.right = node
return temp
}
// LR 先向左旋转,转换为左-左,然后再右旋转
function rotationLR(node) {
node.left = rotationRR(node.left)
// return node
return rotationLL(node)
}
// RL 先向右旋转,转换为右-右,然后再左旋转
function rotationRL(node) {
node.right = rotationLL(node.right)
return rotationRR(node)
}
// 递归用于插入树节点
// BST存在一个问题:取决于你添加的节点数,树的一条边可能会非常深;也就是说,树的一
// 条分支会有很多层,而其他的分支却只有几层,这会在需要在某条边上添加、移除和搜索某个节点时引起一些性能问题
function insertNode(node, element) {
if (node === null) {
node = new Node(element)
} else if (element < node.key) {
node.left = insertNode(node.left, element)
// 二叉树平衡
if(node.left !== null) {
if(heightNode(node.left) - heightNode(node.right) > 1) {
if(element < node.left.key) {
// 左-左(LL):向右的单旋转
node = rotationLL(node)
} else {
// LR 先向左旋转,转换为左-左,然后再右旋转
node = rotationLR(node)
}
}
}
} else if (element > node.key) {
node.right = insertNode(node.right, element)
// 二叉树平衡
if(node.right !== null) {
if(heightNode(node.right) - heightNode(node.left) > 1) {
if(element > node.right.key) {
// 右-右(RR):向左的单旋转
node = rotationRR(node)
} else {
// RL 先向右旋转,转换为右-右,然后再左旋转
node = rotationRL(node)
}
}
}
}
return node
}) {
if(heightNode(node.right) - heightNode(node.left) > 1) {
if(element > node.right.key) {
// 右-右(RR):向左的单旋转
node = rotationRR(node)
} else {
// RL 先向右旋转,转换为右-右,然后再左旋转
node = rotationRL(node)
}
}
}
}
return node
}
参考:学习JavaScript数据结构与算法.第2版