真实的树:相信每个人对现实生活中的树都会非常熟悉
我们来看一下树有什么特点?
▫️ 树通常有一个根。连接着根的是树干。
▫️ 树干到上面之后会进行分叉成树枝,树枝还会分叉成更小的树枝。
▫️ 在树枝的最后是叶子。
树的抽象:
专家们对树的结构进行了抽象,发现树可以模拟生活中的很多场景。
❤️ 我们再将里面的数据移除,仅仅抽象出来结构,那么就是我们要学习的树结构
我们之前已经学习了多种数据结构来保存数据,为什么要使用树结构来保存数据呢?
树结构和数组/链表/哈希表的对比有什么优点呢?
数组:
❤️ 优点:
缺点:
链表:
❤️ 优点:
缺点:
哈希表:
❤️ 优点:
缺点:
树结构:
❤️ 优点:
而且为了模拟某些场景,我们使用树结构会更加方便。
在描述树的各个部分的时候有很多术语。
❤️ 树(Tree):n(n≥0)个节点构成的有限集合。
对于任一棵非空树(n>0),它具备以下性质:
树的术语
完全二叉树(Complete Binary Tree)
下面不是完全二叉树,因为D节点还没有右节点,但是E节点就有了左右节点。
二叉树的存储常见的方式是数组和链表
二叉搜索树(BST,Binary Search Tree),也称 二叉排序树或 二叉查找树。
二叉搜索树是一颗二叉树,可以为空
如果部位空,满足以下性质:
下面哪些是二叉搜索树,哪些不是?
二叉搜索树的特点:
先封装一个BSTree的类
代码解析
代码
import { Node } from '../types/INode';
class IBSTreeNode<T> extends Node<T> {
left: IBSTreeNode<T> | null = null;
right: IBSTreeNode<T> | null = null;
}
class BSTree<T> {
root: IBSTreeNode<T> | null = null;
}
export {};
export class Node<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
insert (value):向树中插入一个新的数据。
search (value):在树中查找一个数据,如果节点存在,则返回true;如果不存在,则返回false 。
min:返回树中最小的值/数据。
max:返回树中最大的值/数据。
inOrderTraverse :通过中序遍历方式遍历所有节点。
preOrderTraverse :通过先序遍历方式遍历所有节点。
postOrderTraverse :通过后序遍历方式遍历所有节点。
levelOrderTraverse :通过层序遍历方式遍历所有节点。
remove (value):从树中移除某个数据。
首先,外界调用的 insert
方法
value
,创建对应的Node
。insertNode
方法,我们还没有实现,也是我们接下来要完成的任务。其次,插入非根节点
代码:
// 插入
private insertNode(node: BSTreeNode<T>, newNode: BSTreeNode<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);
}
}
}
insert(value: T): boolean {
// 1. 根据value创建TreeNode节点
const newNode = new BSTreeNode(value);
// 2. 判断是否有根节点
if (!this.root) {
//当前树为空
this.root = newNode;
} else {
// 递归
this.insertNode(this.root, newNode);
}
return false;
}
// 打印树结构
import { btPrint } from 'hy-algokit';
// 打印树结构
print() {
btPrint(this.root);
}
// 测试代码
const bst = new BSTree<number>();
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();
前面,我们向树中插入了很多的数据,为了能很多的看到测试结果。我们先来学习一下树的遍历。
private preOrderTraverseNode(node: BSTreeNode<T> | null) {
if (node) {
console.log(node.value);
this.preOrderTraverseNode(node.left);
this.preOrderTraverseNode(node.right);
}
}
preOrderTraverse() {
this.preOrderTraverseNode(this.root);
}
private inOrderTraverseNode(node: BSTreeNode<T> | null) {
if (node) {
this.inOrderTraverseNode(node.left);
console.log(node.value);
this.inOrderTraverseNode(node.right);
}
}
inOrderTraverse() {
this.inOrderTraverseNode(this.root);
}
private postOrderTraverseNode(node: BSTreeNode<T> | null) {
if (node) {
this.postOrderTraverseNode(node.left);
this.postOrderTraverseNode(node.right);
console.log(node.value);
}
}
postOrderTraverse() {
this.postOrderTraverseNode(this.root);
}
levelOrderTraverse() {
// 1. 如果没有根节点,那么不需要遍历
if (!this.root) return;
// 2. 创建一个队列
const queue: BSTreeNode<T>[] = [];
// 队列中第一个节点是根节点
queue.push(this.root);
// 3. 遍历队列的长度
while (queue.length) {
// 弹出队列中当前元素
const current = queue.shift()!;
console.log(current.value);
// 将当前元素的左子节点和右子节点添加到队列
if (current.left) {
queue.push(current.left);
}
if (current.right) {
queue.push(current.right);
}
}
}
getMaxValue(): T | null {
let current = this.root;
while (current && current.right) {
current = current.right;
}
return current?.value ?? null;
}
getMinValue(): T | null {
let current = this.root;
while (current && current.left) {
current = current.left;
}
return current?.value ?? null;
}
二叉搜索树不仅仅获取最值效率非常高,搜索特定的值效率也非常高。
代码解析:
代码:
// 搜索特定值
private searchNode(node: BSTreeNode<T> | null, value: T): boolean {
// 如果node为空
if (node === null) return false;
// 判断node节点的value和value
if (node.value < value) {
return this.searchNode(node.right, value);
} else if (node.value > value) {
return this.searchNode(node.left, value);
} else {
return true;
}
}
search(value: T): boolean {
return this.searchNode(this.root, value);
}
二叉搜索树的删除有些复杂,我们一点点完成。
删除节点要从查找要删的节点开始,找到节点后,需要考虑三种情况:
我们先从查找要删除的节点入手
代码分析:
class BSTreeNode<T> extends Node<T> {
.
.
.
parent: BSTreeNode<T> | null = null;
get isLeft(): boolean {
return !!(this.parent && this.parent.left === this);
}
get isRight(): boolean {
return !!(this.parent && this.parent.right === this);
}
}
// 重构searchNode
private searchNode(value: T): BSTreeNode<T> | null {
let current = this.root;
let parent: BSTreeNode<T> | null = null;
while (current) {
// 如果找到current,直接返回
if (current.value === value) return current;
// 继续往下找
parent = current;
if (current.value < value) {
current = current.right;
} else {
current = current.left;
}
// 如果current有值,那么current保存自己的父节点
if (current) {
current.parent = parent;
}
}
return null;
}
remove(value: T): boolean {
// 1. 搜索:当前要删除的值
const current = this.searchNode(value);
// 1.1 节点不存在,不需要任何操作
if (!current) return false;
return true;
}
情况一:没有子节点
如果只有一个单独的根,直接删除即可
如果是叶节点,那么处理方式如下:
remove(value: T): boolean {
// 2.获取三个元素:当前节点、当前节点父节点、是属于左子节点/右子节点
// console.log('当前节点:', current.value, '当前节点父节点:', current.parent?.value);
// 2. 删除的节点是一个叶子结点
if (current.left === null && current.right === null) {
if (current === this.root) {
// 如果只有一个单独的根,直接删除即可
this.root = null;
} else if (current.isLeft) {
// 父节点的左子节点
current.parent!.left = null;
} else {
current.parent!.right = null;
}
return true;
}
return true;
}
remove(value: T): boolean {
// 3. 有一个子节点
// 3.1 只有左子节点
else if (current.right === null) {
if (current === this.root) {
this.root = current.left;
} else if (current.isLeft) {
current.parent!.left = current.left;
} else {
current.parent!.right = current.left;
}
}
// 3.2 只有右子节点
else if (current.left === null) {
if (current === this.root) {
this.root = current.right;
} else if (current.isLeft) {
current.parent!.left = current.right;
} else {
current.parent!.right = current.right;
}
}
return true;
}
看下面的集中情况你怎么处理?
如果我们要删除的节点有两个子节点,甚至子节点还有子节点,这种情况下我们需要从下面的子节点中找到一个节点,来替换当前的节点.
// 获取右子树
private getSuccessor(delNode: BSTreeNode<T>): BSTreeNode<T> {
// 获取右子树
let current = delNode.right;
let successor: BSTreeNode<T> | null = null;
while (current) {
successor = current;
current = current.left;
if (current) {
current.parent = successor;
}
}
// 如果拿到了后继节点
// console.log('删除节点:', delNode.value, '后继节点:', successor?.value);
if (successor !== delNode.right) {
successor!.parent!.left = successor!.right;
successor!.right = delNode.right;
}
//一定: 将删除节点的left,赋值给后继节点的left
successor!.left = delNode.left;
return successor!;
}
remove(value: T): boolean {
// 4. 有两个子节点
else {
const successor = this.getSuccessor(current);
if (current === this.root) {
this.root = successor;
} else if (current.isLeft) {
current.parent!.left = successor;
} else {
current.parent!.right = successor;
}
}
return true;
}
1 、【数据结构与算法——TypeScript】数组、栈、队列、链表
2 、【数据结构与算法——TypeScript】算法的复杂度分析、 数组和链表的对比
3 、【数据结构与算法——TypeScript】哈希表