什么是树?
在真实生活中的树:
在数据结构中的树正是由真实生活中的树抽象而来,一个模拟树结构的例子:
在上图中,我们可以把
总结:
树是一种分层数据的抽象模型,js
中没有树,但是我们可以用 Object
来构建树
在说树结构的优点时,我们先来回顾一下之前将的 数组/链表/哈希表 这几种数据结构
结构 | 优点 | 缺点 |
---|---|---|
数组 | 根据下标访问值效率很高 | 1. 查找效率低,需要对数组进行排序生成有序数组,才能提高查找效率。2. 另外,插入和删除数据时,需要有大量的位移操作,效率很低 |
链表 | 插入和删除效率很高 | 1. 查找效率低,需要从头开始一次访问链表中的每个数据项,直到找到。2. 如果插入和删除中间位置的数据,还是需要重头先找到对应的数据 |
哈希表 | 插入、查询、删除效率都是非常高的 | 1. 空间利用率不高,底层使用的是数组,并且某些单元是没有被利用的。2. 哈希表中元素是无序的,不能按照固定的顺序来遍历哈希表中的元素。3. 不能快速找出哈希表的最大值或者最小值这些特殊值 |
树结构:
树的术语:
Degree
):节点的子树个数.Degree
):树的所有节点中最大的度数. (树的度通常为节点的个数 N-1
)Leaf
):度为 0
的节点. (也称为叶子节点)Parent
):有子树的节点是其子树的根节点的父节点Child
):若 A
节点是 B
节点的父节点,则称 B
节点是 A
节点的子节点;子节点也称孩子节点。Sibling
):具有同一父节点的各节点彼此是兄弟节点。n1
到 nk
的路径为一个节点序列 n1
, n2
,… , nk
, ni
是 ni+1
的父节点。路径所包含边的个数为路径的长度。1
层,其它任一节点的层数是其父节点的层数加1。如果树中的每个节点最多只能有两个子节点,这样的树就称为二叉树
二叉树的定义:
二叉树几个比较重要的特性:
i
层的最大节点数为:2^(i-1), i >= 1
;k
的二叉树有最大节点总数为: 2^k - 1, k >= 1
;T
,若 n0
表示叶节点(度为 0
的节点)的个数、n2
是度为 2
的非叶节点个数,那么两者满足关系 n0 = n2 + 1
。特殊的二叉树
完美二叉树(Perfect Binary Tree
) , 也称为满二叉树(Full Binary Tree
)
2
个子节点, 就构成了满二叉树.完全二叉树(Complete Binary Tree
)
下面不是完全二叉树, 因为 D
节点还没有节点, 但是 E
节点就有了左右节点.
二叉树的存储常见的方式是数组和链表
完全二叉树:从上至下,从左到右顺序存储
非完全二叉树:要转换成完全二叉树才可以按照上面的方案存储,而且会造成很大的空间浪费
二叉树最常见的方式还是使用链表存储(我们之后的封装也会基于链表):每个节点封装成一个 Node
,Node
中包含存储的数据,左节点的引用,右节点的引用。
二叉搜索树(BST,Binary Search Tree
),也称为二叉排序树或二叉查找树
二叉搜索树是一棵二叉树,可以为空:
如果不为空,需要满足一下性质:
二叉搜索树的特点:
一个二叉搜索树的常见操作应该包含以下:
insert(value)
: 向树中插入一个新的数据search(value)
:在树中查找一个数据,如果节点存在,则返回 true
;如果不存在,则返回 false
min
:返回树中最小的值max
:返回树中最大的值inOrderTraverse
:通过中序遍历方式遍历所有节点preOrderTraverse
:通过先序遍历方式遍历所有节点postOrderTraverse
:通过后序遍历方式遍历所有节点levelOrderTraverse
:通过层序遍历方式遍历所有节点remove(value)
:从树中移除某个数据tsclass TreeNode<T> {
value: T;
left: TreeNode<T> | null = null;
right: TreeNode<T> | null = null;
constructor(value: T) {
this.value = value;
}
}
class BSTree<T> {
root: TreeNode<T> | null = null;
}
代码解析:
TreeNode
TreeNode
类包含三个属性
value
: 节点对应的值left
: 指向左边的子树right
:指向右边的子树BSTree
类作为一棵树BSTree
来说,只需要保存根节点即可,因为其他节点都可以通过根节点找到。结构示意图:
在 BSTree
中添加 insert
方法
insert(value)
的作用为向一棵二叉树插入数据
ts insert(value: T) {
// 1. 根据传入 value 创建 TreeNode 节点
const newNode = new TreeNode(value);
// 2. 判断当前是否已经有了根节点
if (!this.root) {
// 当前树为空
this.root = newNode;
} else {
// 树中已经有其他值
this.insertNode(this.root, newNode);
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
if (newNode.value < node.value) {
// 去左边查找空白位置
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);
}
}
}
测试:
tsconst bst = new BSTree<number>();
bst.insert(20);
bst.insert(30);
bst.insert(18);
bst.insert(14)
bst.insert(12)
bst.insert(19)
bst.insert(22)
树的结构:
20 ┌───────┴───────┐ 18 30 ┌───┴───┐ ┌───┘ 14 19 22 ┌─┘ 12
在 BSTree
中添加 preOrderTraverse
方法
preOrderTraverse
方法的作用是先序遍历一个树
树的先序遍历的过程:
ts preOrderTraverse() {
this.preOrderTraverseNode(this.root);
}
private preOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
console.log(node.value);
this.preOrderTraverseNode(node.left);
this.preOrderTraverseNode(node.right);
}
}
测试:
tsconst bst = new BSTree<number>();
bst.insert(20);
bst.insert(30);
bst.insert(18);
bst.insert(14);
bst.insert(12);
bst.insert(19);
bst.insert(22);
bst.preOrderTraverse();
树的结构:
20 ┌───────┴───────┐ 18 30 ┌───┴───┐ ┌───┘ 14 19 22 ┌─┘ 12
打印结果:
20 18 14 12 19 30 22
在 BSTree
中添加 inOrderTraverse
方法
inOrderTraverse
方法的作用是中序遍历一个树
树的中序遍历的过程:
ts inOrderTraverse() {
this.inOrderTraverseNode(this.root);
}
private inOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.inOrderTraverseNode(node.left);
console.log(node.value);
this.inOrderTraverseNode(node.right);
}
}
测试:
tsconst bst = new BSTree<number>();
bst.insert(20);
bst.insert(30);
bst.insert(18);
bst.insert(14);
bst.insert(12);
bst.insert(19);
bst.insert(22);
bst.inOrderTraverse();
树的结构:
20 ┌───────┴───────┐ 18 30 ┌───┴───┐ ┌───┘ 14 19 22 ┌─┘ 12
打印结果:
12 14 18 19 20 22 30
在 BSTree
中添加 postOrderTraverse
方法
postOrderTraverse
方法的作用是后序遍历一个树
树的后序遍历的过程:
ts postOrderTraverse() {
this.postOrderTraverseNode(this.root);
}
private postOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.postOrderTraverseNode(node.left);
this.postOrderTraverseNode(node.right);
console.log(node.value);
}
}
测试:
tsconst bst = new BSTree<number>();
bst.insert(20);
bst.insert(30);
bst.insert(18);
bst.insert(14);
bst.insert(12);
bst.insert(19);
bst.insert(22);
bst.postOrderTraverse();
树的结构:
20 ┌───────┴───────┐ 18 30 ┌───┴───┐ ┌───┘ 14 19 22 ┌─┘ 12
打印结果:
12 14 19 18 22 30 20
在 BSTree
中添加 levelOrderTraverse
方法
levelOrderTraverse
方法的作用是层序遍历一个树
层序遍历的过程:
ts levelOrderTraverse() {
// 1. 如果没有根节点,那么不需要遍历
if (!this.root) return;
// 2. 创建队列结构
const queue: TreeNode<T>[] = [];
// 第一个节点是根节点
queue.push(this.root);
// 3. 遍历队列中所有的节点(依次出队)
while (queue.length) {
// 3.1 访问节点的过程
const current = queue.shift()!;
console.log(current.value);
// 3.2 将左子节点放入到队列
if (current.left) {
queue.push(current.left);
}
// 3.3 将右子节点放入到队列
if (current.right) {
queue.push(current.right);
}
}
}
测试:
tsconst bst = new BSTree<number>();
bst.insert(20);
bst.insert(30);
bst.insert(18);
bst.insert(14);
bst.insert(12);
bst.insert(19);
bst.insert(22);
bst.levelOrderTraverse();
树的结构:
20 ┌───────┴───────┐ 18 30 ┌───┴───┐ ┌───┘ 14 19 22 ┌─┘ 12
打印结果:
20 18 30 14 19 22 12
在 BSTree
中添加 getMaxValue
方法
getMaxValue
方法的作用是获取树中的最大值
ts getMaxValue(): T | null {
let current = this.root;
while (current && current.right) {
current = current.right;
}
return current?.value ?? null;
}
在 BSTree
中添加 getMinValue
方法
getMinValue
方法的作用是获取树中的最小值
ts getMinValue(): T | null {
let current = this.root;
while (current && current.left) {
current = current.left;
}
return current?.value ?? null;
}
在 BSTree
中添加 search
方法
search
方法的作用:在树中查找一个数据,如果节点存在,则返回 true
;如果不存在,则返回 false
ts search(value: T): boolean {
let current = this.root;
while (current) {
// 找到了节点
if (current.value === value) return true;
if (current.value < value) {
current = current.right;
} else {
current = current.left;
}
}
return false;
}
在 BSTree
中添加 remove
方法
remove
方法的作用:删除某个数据
remove
的逻辑比较复杂,我们大致可以分为以下四种情况:
ts remove(value: T): boolean {
// 1. 获取要删除的节点 current 和 其父节点 parent
let current = this.root;
let parent: TreeNode<T> | null = null;
while (current) {
// 找到了节点
if (current.value === value) break;
parent = current;
if (current.value < value) {
current = current.right;
} else {
current = current.left;
}
}
// 如果没有找到要删除的节点返回 false
if (!current) return false;
// 2. 如果要删除的的节点是叶子结点
if (current.left === null && current.right === null) {
if (current === this.root) {
// current 是根节点
this.root = null;
} else if (parent?.left === current) {
// current 在父节点的左边
parent!.left = null;
} else {
// current 在父节点的右边
parent!.right = null;
}
}
// 3. 如果要删除的的节点只有一个左子节点
else if (current.right === null) {
if (current === this.root) {
this.root = current.left;
} else if (parent?.left === current) {
// current 在父节点的左边
parent!.left = current.left;
} else {
parent!.right = current.left;
}
}
// 4. 如果要删除的的节点只有一个右子节点
else if (current.left === null) {
if (current === this.root) {
this.root = current.right;
} else if (parent?.left === current) {
// current 在父节点的左边
parent!.left = current.right;
} else {
parent!.right = current.right;
}
}
// 5. 如果要删除的的节点有两个子节点
else {
const successor = this.getSuccessor(current);
if (current === this.root) {
this.root = successor;
} else if (parent?.left === current) {
parent!.left = successor;
} else {
parent!.right = successor;
}
}
return true;
}
// 获取后继节点
private getSuccessor(delNode: TreeNode<T>): TreeNode<T> {
// 获取右子树
let current = delNode.right;
let successor: TreeNode<T> | null = null;
while (current) {
successor = current;
current = current.left;
}
// 拿到了后继节点
if (successor !== delNode.right) {
delNode!.right!.left = successor?.right ?? null;
successor!.right = delNode.right;
}
// 一定要进行的操作:将删除后继节点的 left 赋值给后继节点的 left
successor!.left = delNode.left;
return successor!;
}
在上面的的遍历实现中,我们都是使用递归的方式实现。
递归:直接或间接调用自身算法的过程
它的优点就是:
但是它也有很明显的缺点:
在这里我们就用非递归的方式再实现一遍
1. 先序遍历
ts preOrderTraverse() {
let stack: TreeNode<T>[] = [];
let current: TreeNode<T> | null = this.root;
while (current !== null || stack.length !== 0) {
while (current !== null) {
console.log(current.value);
stack.push(current);
current = current.left;
}
current = stack.pop()!;
current = current?.right;
}
}
2. 中序遍历
ts inOrderTraverse() {
let stack: TreeNode<T>[] = [];
let current: TreeNode<T> | null = this.root;
while (current !== null || stack.length !== 0) {
while (current !== null) {
stack.push(current);
current = current.left;
}
current = stack.pop()!;
console.log(current.value);
current = current?.right;
}
}
3. 后序遍历
tspostOrderTraverse() {
let stack: TreeNode<T>[] = [];
let current: TreeNode<T> | null = this.root;
let lastVisitedNode: TreeNode<T> | null = null;
while (current !== null || stack.length !== 0) {
while (current !== null) {
stack.push(current);
current = current.left;
}
current = stack[stack.length - 1];
if (current.right === null || current.right === lastVisitedNode) {
console.log(current.value);
lastVisitedNode = current;
stack.pop();
current = null;
} else {
current = current.right;
}
}
}
二叉搜索树作为数据存储的结构有重要优势:可以快速地找到给定关键字的数据项,并且可以快速插入和删除数据项
但是二叉搜索树有一个很麻烦的问题:如果插入的数据是有序的数据,比如下面的情况
O(logN)
O(N)
所以,为了能以较快的时间O(logN)来操作一棵树,我们需要保证树总是平衡的(至少大部分是平衡的)
常见的平衡树:(后续介绍)
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!