目录
树结构是一种分层的、非线性的数据结构,用于组织和存储数据。它由节点(node
)和边(edge
)组成,通常被用来表示具有父子关系的层次化数据。树结构在计算机科学中非常重要,广泛用于管理数据(如文件系统、数据库索引、网页 DOM
等)
认识
-
树在我们的生活中处处可见,树通常有一个根,连接着根的是树干
-
树干到上面之后会进行分叉成树枝,树枝还会分叉成更小的树枝,在树枝的最后是叶子
-
专家们对树的结构进行了抽象,发现树可以模拟生活中的很多场景
-
比如我们熟悉的
DOM Tree
术语
-
空树:
n(n≥0)
个节点构成的有限集合,当n=0
时,称为空树 -
子树(
SubTree
):对于任一棵非空树(n > 0
),树中有一个称为根(Root
)的特殊节点用r
表示,其余节点可分为m(m>0)
个互不相交的有限集T1,T2,...,Tm
,其中每个集合本身又是一棵树,称为原来树的子树 -
边: 父节点与子节点之间的关系就相当于一条边
-
节点的度(
Degree
):节点的子树个数 -
树的深度(
Depth
):对于任意节点n
,n
的深度为从根到n
的唯一路径长,根的深度为0
-
树的高度(
Height
):从根节点到叶子节点的最长路径上的边数称为树的高度 -
叶子节点(
Leaf
):没有子节点的节点称为叶子节点 -
父节点(
Parent
):一个节点的直接上层节点称为它的父节点 -
子节点(
Child
):一个节点下直接连接的节点称为它的子节点,子节点也称孩子节点 -
兄弟节点(
Sibling
):具有同一父节点的各节点彼此是兄弟节点 -
路径:指从一个节点到另一个节点的一系列连接顺序,是一条唯一的路径,因为树的结构不允许循环
-
路径长度:路径长度是指路径上边的总数量,在树结构中常用来计算节点的深度或高度
-
节点的层级(
Level
):从根节点开始,根节点所在的层级记为1
,往下每一层的层级递增1
表示方式
-
普通表示
-
儿子-兄弟表示:是一种用二叉树来表示普通多叉树的数据结构方法
-
每个节点的左子节点表示它的第一个孩子
-
每个节点的右子节点表示它的下一个兄弟
-
旋转45
度不是必要的步骤,但它让我们更清晰地理解多叉树如何被转换成二叉树
二叉树
如果树中每个节点最多只能有两个子节点,这样的树就称为二叉树,几乎所有的树都可以表示成二叉树的形式,所以二叉树是很重要的
-
二叉树的定义:二叉树可以为空,也就是没有节点。若不为空,则它是由根节点和称为其左子树
TL
和右子树TR
的两个不相交的二叉树组成 -
二叉树有五种形态:
特性
二叉树有几个比较重要的特性,在笔试题中比较常见,以下图作为参考:
-
层为
h
的二叉树,至少有h
个节点,最多可以有2^h − 1
个节点 -
一棵二叉树第
i
层的最大节点数为:2^(i-1),i >= 1
-
在任何非空的二叉树中,如果叶子节点数为
n0
,度为2
的节点(有两个子节点的节点)数为n2
,则满足关系:n0 = n2 + 1
,比如上图叶子结点为HIEFJ
,度为2
的为ABCD
,满足公式
完美二叉树
完美二叉树(Perfect Binary Tree
) ,也称为满二叉树(Full Binary Tree
),在二叉树中,除了最下一层的叶节点外,每层节点都有2
个子节点,就构成了满二叉树
-
节点数:对于树的高度为
h
的满二叉树,总节点数N
是:N = 2^(h+1) - 1
-
叶子节点数:一棵高度为
h
的满二叉树,叶子节点数为:2^h
-
非叶子节点数:在满二叉树中,非叶子节点的数量(即内部节点数)为:
2^h - 1
-
层节点数:在满二叉树的第
k
层(从0
开始编号)上,节点数为:2^k
完全二叉树
完全二叉树是一种特殊的二叉树,其中所有层都被完全填满,除了最后一层。最后一层的节点都从左到右连续排列,没有空位
-
节点排列:从根节点到倒数第二层,每一层的节点数都达到最大值。最后一层的节点从左到右依次排列,但最后一层不必完全填满
-
节点数关系:具有
n
个节点的完全二叉树的深度为⌊log2n⌋ + 1
(⌊ ⌋
表示向下取整) -
节点编号规律:节点编号可以用于定位树中每个节点的父节点和子节点位置,使得完全二叉树在实现堆等数据结构时特别高效
-
根节点编号为
1
:从根节点开始编号,根节点的编号是1
-
子节点的编号:
若节点编号为
i
,左子节点的编号为2i
,右子节点的编号为2i+1
例如,编号为
1
的节点的左子节点编号为2×1=2
,右子节点编号为2×1+1=3
-
父节点的编号:若节点编号为
i
,父节点的编号为⌊i/2⌋
,例如编号为4
的节点的父节点编号为⌊4/2⌋=2
-
-
叶子节点:所有叶子节点都位于倒数第一层或倒数第二层,且位于倒数第一层的叶子节点从左到右连续排列
-
数组表示:完全二叉树可以很方便地用数组表示,每个节点按上述编号存储在数组的对应位置。这样的表示方式可以减少指针的使用,便于存储和查找。这一特性使得完全二叉树在实现堆结构时尤其有用
存储
二叉树的存储常见的方式是顺序存储(数组实现)和链式存储(链表实现)
顺序存储
-
完全二叉树:按从上至下、从左到右顺序存储
-
非完全二叉树:要转成完全二叉树才可以按照上面的方案存储,但是会造成很大的空间浪费
链式存储
二叉树最常见的方式还是使用链表存储,每个节点封装成一个Node
,Node
中包含存储的数据有左节点的引用和右节点的引用
二叉搜索树
二叉搜索树(BST,Binary Search Tree
)特殊的二叉树可以为空,也称二叉排序树或二叉查找树,由于其性质,二叉搜索树能够高效地执行查找、插入和删除操作,平均时间复杂度为 O(logn)
,在最坏情况下(如退化为链表)为 O(n)
特点
-
是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上
-
左子树的值小于根节点的值:在每个节点的左子树上,所有节点的值都小于这个节点的值
-
右子树的值大于根节点的值:在每个节点的右子树上,所有节点的值都大于这个节点的值
-
左右子树本身也是二叉搜索树:这意味着二叉搜索树具有递归的结构,子树也符合二叉搜索树的定义
-
这种方式就是二分查找的思想,查找所需的最大次数等于二叉搜索树的深度
常见操作
-
insert(value)
:向树中插入一个新的数据 -
search(value)
:在树中查找一个数据,如果节点存在,则返回true
;如果不存在,则返回false
-
min
:返回树中最小的值/数据 -
max
:返回树中最大的值/数据 -
preOrderTraverse
:通过先序遍历方式遍历所有节点 -
inOrderTraverse
:通过中序遍历方式遍历所有节点 -
postOrderTraverse
:通过后序遍历方式遍历所有节点 -
levelOrderTraverse
:通过层序遍历方式遍历所有节点 -
remove(value)
:从树中移除某个数据
封装
封装BSTree
的类:
-
先封装一个用于保存每一个节点的类
Node
,包含属性:节点对应的value
,指向的左子树left
,指向的右子树right
,指向父节点的parent
-
对于
BSTree
来说,只需要保存根节点即可,因为其他节点都可以通过根节点找到
class TreeNode<T> {
value: T;
left: TreeNode<T> | null = null;
right: TreeNode<T> | null = null;
parent: TreeNode<T> | null = null;
constructor(value: T) {
this.value = value;
}
get isLeft() {
return !!(this.parent && this === this.parent.left);
}
get isRight() {
return !!(this.parent && this === this.parent.right);
}
}
class BSTree<T = number> {
private root: TreeNode<T> | null = null;
}
插入数据
空树是直接设置为root
,不为空树时直接循环比较插入
insert(value: T) {
const newNode = new TreeNode(value);
if (!this.root) {
this.root = newNode;
} else {
/* 非递归 */
// while (current) {
// if (current.value > value) {
// if (!current.left) {
// current.left = newNode;
// current = null;
// } else {
// current = current.left;
// }
// } else {
// if (!current.right) {
// current.right = newNode;
// current = null;
// } else {
// current = current.right;
// }
// }
// }
/* 递归 */
this.insertNode(this.root, newNode);
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
if (newNode.value < node.value) {
// 插入的值小于比较值,判断其左边,为空直接插入,不为空递归再次判断
if (!node.left) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (!node.right) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
遍历数据
针对所有的二叉树都是适用的,不仅仅是二叉搜索树,遍历一棵树是指访问树的每个节点(也可以对每个节点进行某些操作,我们实现简单的打印),二叉树的遍历常见的有四种方式:先序遍历、中序遍历、后序遍历和层序遍历
先序遍历
根 -> 左 -> 右
-
首先访问根节点
-
再先序遍历其左子树
-
最后先序遍历其右子树
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);
}
}
非递归代码参考:
中序遍历
左 -> 根 -> 右
-
先遍历其左子树
-
再访问根节点
-
最后遍历其右子树
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);
}
}
非递归代码参考:
后序遍历
左 -> 右 -> 根
-
先遍历其左子树
-
再遍历其右子树
-
最后访问根节点
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);
}
}
非递归代码参考:
层序遍历
层序遍历通常借助队列来完成,这也是队列的一个经典应用场景,我们这里直接使用数组来模拟队列
-
先把根节点放入队列
-
取出根节点,队列放入根节点的左右子树
-
取出左子树,队列放入左子树的左右子树,如此循环
levelOrderTraverse() {
if (!this.root) return;
let queue: TreeNode<T>[] = [this.root];
while (queue.length) {
const current = queue.shift()!;
console.log(current.value);
current.left && queue.push(current.left);
current.right && queue.push(current.right);
}
}
最小最大值
getMinValue(): T | null {
if (!this.root) return null;
let current = this.root;
while (current.left) {
current = current.left;
}
return current.value;
}
getMaxValue(): T | null {
if (!this.root) return null;
let current = this.root;
while (current.right) {
current = current.right;
}
return current.value;
}
搜索特定值
search(value: T): boolean {
/* 递归 */
return !!this.searchNode(this.root, value);
/* 非递归 */
// if (!this.root) return false;
// let current: TreeNode<T> | null = this.root;
// while (current) {
// if (value > current.value) {
// current = current.right;
// } else if (value < current.value) {
// current = current.left;
// } else {
// return true;
// }
// }
// return false;
}
private searchNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
if (!node) return null;
if (value < node.value) {
return this.searchNode(node.left, value);
} else if (value > node.value) {
return this.searchNode(node.right, value);
} else {
return node;
}
}
删除数据
删除比较复杂,我们需要一点点的来解析,主要分为以下几种情况:
-
没有子节点时:要删除节点的左右子树都为
null
-
如果要删除的是根节点,则直接删除设为
null
即可
-
如果不是根节点,判断要删除节点是父节点的左子树还是右子树,则需让其父节点的相关子树指向
null
即可
if (!delNode.left && !delNode.right) { // 1.1 是根节点时 if (delNode === this.root) { this.root = null; } else { if (delNode.isLeft) { delNode.parent!.left = null; } else { delNode.parent!.right = null; } } }
-
-
有一个子节点时:要删除节点的左右子树其中一个不为
null
-
如果要删除的是根节点,让根指向其子节点
-
如果不是根节点,判断要删除节点是父节点的左子树还是右子树,则需让其父节点的相关子树指向子节点
if ((delNode.left && !delNode.right) || (delNode.right && !delNode.left)) { // 2.1 有左子结点 if (delNode.left) { if (delNode === this.root) { this.root = delNode.left; } else { if (delNode.isLeft) { delNode.parent!.left = delNode.left; } else { delNode.parent!.right = delNode.left; } } } else { if (delNode === this.root) { this.root = delNode.right; } else { if (delNode.isLeft) { delNode.parent!.left = delNode.right; } else { delNode.parent!.right = delNode.right; } } } }
-
-
有两个子节点时:要删除节点的左右子树都不为
null
-
要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下需要从下面的子节点中找到一个节点,来替换当前的节点
-
这个替换的节点怎么找呢?
比删除节点小一点点的节点,一定是删除节点左子树的最大值
比删除节点大一点点的节点,一定是删除节点右子树的最小值
那么这两个节点就是前驱节点和后继节点是指在中序遍历(即左-根-右顺序)中的前一个节点和后一个节点,我们以后继节点实现代码
-
找到后继节点后,让删除节点替换为后继节点,若后继节点等于删除节点的右子树时,让删除节点的右子树指向后继节点的右子树
-
当后继节点有右子树时,把右子树提到后继节点的位置
if (delNode.left && delNode.right) { const successor = this.getSuccessor(delNode); delNode.value = successor.value; if (delNode.right === successor) { delNode.right = successor.right; } else { successor.parent!.left = successor.right; if (successor.right) { successor.right.parent = successor.parent; } } }
-
-
抽取后完整代码如下:
// 获取删除节点的后继节点 private getSuccessor(node: TreeNode<T>): TreeNode<T> { let current = node.right!; while (current.left) { current = current.left; } return current; } // 删除值 remove(value: T): boolean { const delNode = this.searchNode(this.root, value); if (!delNode) return false; if (delNode.left && delNode.right) { const successor = this.getSuccessor(delNode); delNode.value = successor.value; if (delNode.right === successor) { delNode.right = successor.right; } else { successor.parent!.left = successor.right; if (successor.right) { successor.right.parent = successor.parent; } } } else { const replaceNode = delNode.left || delNode.right; if (delNode === this.root) { this.root = replaceNode; } else if (delNode.isLeft) { delNode.parent!.left = replaceNode; } else { delNode.parent!.right = replaceNode; } if (replaceNode) { replaceNode.parent = delNode.parent; } } return true; }
全部代码
import { btPrint } from "hy-algokit";
class TreeNode<T> {
value: T;
left: TreeNode<T> | null = null;
right: TreeNode<T> | null = null;
parent: TreeNode<T> | null = null;
constructor(value: T) {
this.value = value;
}
get isLeft() {
return !!(this.parent && this === this.parent.left);
}
get isRight() {
return !!(this.parent && this === this.parent.right);
}
}
class BSTree<T = number> {
private root: TreeNode<T> | null = null;
// 打印树结构
print() {
btPrint(this.root);
}
// 插入数据
insert(value: T) {
const newNode = new TreeNode(value);
if (!this.root) {
this.root = newNode;
} else {
/* 非递归 */
// while (current) {
// if (current.value > value) {
// if (!current.left) {
// current.left = newNode;
// current = null;
// } else {
// current = current.left;
// }
// } else {
// if (!current.right) {
// current.right = newNode;
// current = null;
// } else {
// current = current.right;
// }
// }
// }
/* 递归 */
this.insertNode(this.root, newNode);
}
}
private insertNode(node: TreeNode<T>, newNode: TreeNode<T>) {
if (newNode.value < node.value) {
// 插入的值小于比较值,判断其左边,为空直接插入,不为空递归再次判断
if (!node.left) {
node.left = newNode;
newNode.parent = node;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (!node.right) {
node.right = newNode;
newNode.parent = node;
} else {
this.insertNode(node.right, newNode);
}
}
}
// 先序遍历
preOrderTraverse() {
this.preOrderTraverseNode(this.root);
}
private preOrderTraverseNode(node: TreeNode<T> | null) {
if (node) {
this.preOrderTraverseNode(node.left);
// console.log(node.value); // 先遍历打印出根节点
this.preOrderTraverseNode(node.right);
console.log(node.value); // 先遍历打印出根节点
}
}
// 中序遍历
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);
}
}
// 后序遍历
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);
}
}
// 层序遍历
levelOrderTraverse() {
if (!this.root) return;
let queue: TreeNode<T>[] = [this.root];
while (queue.length) {
const current = queue.shift()!;
console.log(current.value);
current.left && queue.push(current.left);
current.right && queue.push(current.right);
}
}
// 获取最小值
getMinValue(): T | null {
if (!this.root) return null;
let current = this.root;
while (current.left) {
current = current.left;
}
return current.value;
}
// 获取最大值
getMaxValue(): T | null {
if (!this.root) return null;
let current = this.root;
while (current.right) {
current = current.right;
}
return current.value;
}
// 搜索值
search(value: T): boolean {
/* 递归 */
return !!this.searchNode(this.root, value);
/* 非递归 */
// if (!this.root) return false;
// let current: TreeNode<T> | null = this.root;
// while (current) {
// if (value > current.value) {
// current = current.right;
// } else if (value < current.value) {
// current = current.left;
// } else {
// return true;
// }
// }
// return false;
}
private searchNode(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
if (!node) return null;
if (value < node.value) {
return this.searchNode(node.left, value);
} else if (value > node.value) {
return this.searchNode(node.right, value);
} else {
return node;
}
}
// 获取删除节点的后继节点
private getSuccessor(node: TreeNode<T>): TreeNode<T> {
let current = node.right!;
while (current.left) {
current = current.left;
}
return current;
}
// 删除值
remove(value: T): boolean {
const delNode = this.searchNode(this.root, value);
if (!delNode) return false;
if (delNode.left && delNode.right) {
const successor = this.getSuccessor(delNode);
delNode.value = successor.value;
if (delNode.right === successor) {
delNode.right = successor.right;
} else {
successor.parent!.left = successor.right;
if (successor.right) {
successor.right.parent = successor.parent;
}
}
} else {
const replaceNode = delNode.left || delNode.right;
if (delNode === this.root) {
this.root = replaceNode;
} else if (delNode.isLeft) {
delNode.parent!.left = replaceNode;
} else {
delNode.parent!.right = replaceNode;
}
if (replaceNode) {
replaceNode.parent = delNode.parent;
}
}
return true;
}
}
class Phone {
constructor(public name: string, public price: number) {}
/*
在 JavaScript 中,valueOf() 方法的作用是让对象在参与一些特定运算(比如数学运算和比较运算)时,
将对象转换为一个原始值。通过添加 valueOf() 方法,
可以让对象在数值操作中自动返回指定的属性值(在此例中为 price)
*/
valueOf() {
return this.price;
}
}
const phone1 = new Phone("sanxing", 910);
const phone2 = new Phone("xiaomi", 760);
const phone3 = new Phone("oppo", 740);
const phone4 = new Phone("huawei", 800);
const phone5 = new Phone("apple", 1200);
const obst = new BSTree<Phone>();
obst.insert(phone1);
obst.insert(phone2);
obst.insert(phone3);
obst.insert(phone4);
obst.insert(phone5);
obst.print();
/*
1. 这时打印是下面的内容,显然是错误的,这是因为比较的问题,我们想让他根据价格形成树结构,该怎么做哪?
[object Object]
└───────────────┐
[object Object]
└───────┐
[object Object]
└───┐
[object Object]
└─┐
[object Object]
2. 我们根绝价格形成树结构只需要在Phone中加入valueOf方法即可
910
┌───┴───┐
760 1200
┌─┴─┐
740 800
*/
const bst = new BSTree();
// 插入测试
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);
bst.insert(6);
bst.print();
/*
11
┌───────┴───────┐
7 15
┌───┴───┐ ┌───┴───┐
5 9 13 20
┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐
3 6 8 10 12 14 18 25
*/
// 遍历测试
// bst.preOrderTraverse();
// bst.inOrderTraverse();
// bst.postOrderTraverse();
// bst.levelOrderTraverse();
console.log(bst.getMinValue()); // 3
console.log(bst.getMaxValue()); // 25
console.log(bst.search(3)); // true
console.log(bst.search(11)); // true
console.log(bst.search(13)); // true
console.log(bst.search(30)); // false
// 删除测试
// 1. 删除叶子节点
console.log(bst.remove(3));
console.log(bst.remove(14));
console.log(bst.remove(18));
bst.print();
/*
11
┌───────┴───────┐
7 15
┌───┴───┐ ┌───┴───┐
5 9 13 20
└─┐ ┌─┴─┐ ┌─┘ └─┐
6 8 10 12 25
*/
// 2. 删除有一个节点的
console.log(bst.remove(5));
console.log(bst.remove(13));
console.log(bst.remove(20));
bst.print();
/*
11
┌───────┴───────┐
7 15
┌───┴───┐ ┌───┴───┐
6 9 12 25
┌─┴─┐
8 10
*/
// 删除有两个节点的
bst.remove(11);
bst.print();
/*
12
┌───────┴───────┐
7 15
┌───┴───┐ └───┐
6 9 25
┌─┴─┐
8 10
*/
bst.remove(12);
bst.print();
/*
15
┌───────┴───────┐
7 25
┌───┴───┐
6 9
┌─┴─┐
8 10
*/
bst.remove(7);
bst.print();
/*
15
┌───────┴───────┐
8 25
┌───┴───┐
6 9
└─┐
10
*/
缺陷
-
二叉搜索树作为数据存储的结构有重要的优势:可以快速地找到给定关键字的数据项,并且可以快速地插入和删除数据项
-
但是二叉搜索树有一个很麻烦的问题:如果插入的数据是有序的数据,比如有一棵初始化为
33 20 77
的二叉树,插入下面的数据:15 11 4 2
-
插入连续数据后,二叉树分布的不均匀,我们称这种树为非平衡树
-
对于一棵平衡二叉树来说,插入/查找等操作的效率是
O(logN)
,对于一棵非平衡二叉树,相当于编写了一个链表,查找效率变成了O(N)
平衡二叉树
平衡二叉树是为了解决普通二叉搜索树在极端情况下(例如插入有序数据时)退化成链表的问题,在这种树中,每个节点的左子树和右子树的高度差不能超过一定的值
常见的平衡树
AVL
树和红黑树是两种常见的平衡二叉树实现,它们通过旋转等方式来维持树的平衡,确保查找、插入和删除操作的时间复杂度保持在 O(logn)
以内。在实际应用中,红黑树由于其效率和实现复杂度的平衡,广泛用于各种编程语言的标准库中
AVL
树:-
AVL
树是最早的一种平衡树,它有些办法保持树的平衡(每个节点多存储了一个额外的数据) -
因为
AVL
树是平衡的,所以时间复杂度也是O(logN)
,但是每次插入/删除操作相对于红黑树效率都不高,所以整体效率不如红黑树
-
- 红黑树:
-
红黑树也通过一些特性来保持树的平衡
-
因为是平衡树,所以时间复杂度也是在
O(logN)
-
另外插入/删除等操作,红黑树的性能要优于
AVL
树,所以现在平衡树的应用基本都是红黑树
-
AVL树和红黑树
具体学习这篇文章:待后面补充