五、通用树的封装
由于树及树结构的遍历涉及篇幅较多,这里拆分为两部分进行讲解,第一部分代码篇,第二部分理论篇。
1、代码
树的封装,本身只涉及一个文件,即 TreeNode.ts:
/*
树数据结构
-------------------------root--------------------
/ | \
node1 node2 node3
/ | \ / \ |
node4 node5 node6 node7 node8 node9
| | |
node10 node11 node12
|
node13
*/
import {
Indexer, IndexerL2R } from "./Indexer";
import {
NodeCallback } from "./NodeCallback";
// 要实现如上所示的树结构,一般需要使用在内存中方便寻址到父节点和儿子节点的存储方式
export class TreeNode<T> {
// 父子节点的存储方式
private _parent: TreeNode<T> | undefined; // 指向当前节点的父节点,如果 this 是根节点,则指向 undefined,因为根节点肯定没有父节点
private _children: Array<TreeNode<T>> | undefined; // 数组中保存所有的直接儿子节点,如果是叶子节点,则 _children 为 undefined 或 _children.length = 0
public name: string; // 当前节点的名称,有利于 debug 时信息输出或按名字获取节点(集合)操作
public data: T | undefined; // 一个 泛型对象, 指向一个需要依附到当前节点的对象
// 树节点的构造函数
public constructor(
data: T | undefined = undefined,
parent: TreeNode<T> | undefined = undefined,
name: string = ""
) {
this._parent = parent;
this._children = undefined;
this.name = name;
this.data = data;
// 如果有父节点,则将 this 节点添加到父节点的子节点列表中
if (this._parent !== undefined) {
this._parent.addChild(this);
}
}
// 树节点添加节点的三种情况及操作(当要向树节点中添加子节点时):
/*
树数据结构
-------------------------root--------------------
/ | \
node1 node2 node3
/ | \ / \ |
node4 node5 node6 node7 node8 node9
| | |
node10 node11 node12
|
node13
*/
// 1、首先要判断要添加的儿子节点如果是当前节点的父节点或祖先节点,则什么都不处理,直接退出操作
// (如 向 node4 节点下添加一个子节点(node1),因为此时 node1 是 node4 的父节点,则不允许这种操作,这样会导致循环引用,程序崩溃)
// 2、然后判断要添加的儿子节点是否有父节点,如果有父节点,则需要先将儿子节点从父亲节点中移除,再添加到当前节点
// (如 向 node4 节点 添加一个 node11 节点,由于 node11 节点的父节点是 node7,则应该先从 node7 节点移除其子节点 node11,再将 node11 添加为 node4 的子节点)
// 3、最后如果要添加的子节点是新创建的节点,没有父节点,也不会有循环引用,则使用标准方式处理
// (如新建了一个 node14 节点,则 node14 节点可以直接添加为 node4 的子节点)
// 要实现上述子节点添加算法,需要先实现必要的辅助方法
// 判断一个要添加的子节点是否是当前节点的祖先节点
public isDescendantOf(ancestor: TreeNode<T> | undefined): boolean {
// undefined 值检查
if (ancestor === undefined) {
return false;
}
// 从当前节点的父节点开始向上遍历
for (
let node: TreeNode<T> | undefined = this._parent;
node !== undefined;
node = node._parent
) {
// 逐级向上遍历祖先节点,直至祖先节点为 undefined,即遍历到根节点
// 如果当前节点的祖先等于 ancestor,则说明当前节点时 ancestor 的子孙,即 ancestor 是当前节点的祖先节点,否则返回 false
if (node === ancestor) {
return true;
}
}
return false;
}
// 移除某个节点:根据索引
public removeChildAt(index: number): TreeNode<T> | undefined {
// 由于使用延迟初始化,必须要进行 undefined 检查
if (this._children === undefined) {
return undefined;
}
// 根据索引从 _children 数组中获取节点
let child: TreeNode<T> | undefined = this.getChildAt(index); // 索引可能会越界,这是 getChildAt 函数处理的,如果索引越界了,getChildAt 函数返回 undefined
if (child === undefined) {
return undefined;
}
this._children.splice(index, 1); // 从子节点列表中移除掉
child._parent = undefined; // 将子节点的父节点设置为 undefined
return child;
}
// 移除某个节点:依据节点引用本身
public removeChild(child: TreeNode<T> | undefined): TreeNode<T> | undefined {
// 参数为 undefined 的处理
if (child === undefined) {
return undefined;
}
// 如果当前节点是叶子节点的处理
if (this._children === undefined) {
return undefined;
}
// 由于使用数组线性存储方式,从索引查找元素是最快的
// 但是从元素查找索引,必须遍历整个数组
let index: number = -1;
for (let i = 0; i < this._children.length; i++) {
if (this.getChildAt(i) === child) {
index = i; // 找到要删除的子节点 记录索引
break;
}
}
// 没有找到索引
if (index === -1) {
return undefined;
}
// 找到要移除的子节点的索引,调用 removeChildAt 方法即可
return this.removeChildAt(index);
}
// 移除某个节点:将 this 节点从父节点删除
public remove(): TreeNode<T> | undefined {
if (this._parent !== undefined) {
return this._parent.removeChild(this);
}
return undefined;
}
// 实现 addChild 等方法
// 有了 isDescendantOf 方法和 remove 相关方法,就能实现添加子节点的方法
public addChildAt(
child: TreeNode<T>,
index: number
): TreeNode<T> | undefined {
// 第一种情况:要添加的子节点是当前节点的祖先的判断,换句话说就是当前节点已经是 child 节点的子孙节点,这样会循环引用,那就直接退出
if (this.isDescendantOf(child)) {
return undefined;
}
// 延迟初始化的处理
if (this._children === undefined) {
// 有两种方式初始化数组,这里用了 []
this._children = [];
// this._children = new Array<TreeNode<T>>();
}
// 索引越界检查
if (index >= 0 && index <= this._children.length) {
if (child._parent !== undefined) {
// 第二种情况:要添加的节点是有父节点的,需要从父节点中移除
child._parent.removeChild(child);
}
// 第三种情况:要添加的节点不是当前节点的祖先并且也没有父节点(新节点或已从父节点移除)
// 设置父节点并添加到 _children 中
child._parent = this;
this._children.splice(index, 0, child); // 向指定位置插入元素
return child;
} else {
return undefined;
}
}
public addChild(child: TreeNode<T>): TreeNode<T> | undefined {
if (this._children === undefined) {
this._children = [];
}
// 在列表最后添加一个节点
return this.addChildAt(child, this._children.length);
}
// 通过以上就实现了树结构的两个重要操作:即树节点的添加和移除操作,这样就能使用编程方式生成一颗节点树,也能删除某个节点,甚至整棵节点树
// 而通过 parent/getChild/depth 等以下只读属性或方法,就能查询树节点的各种层次关系,能唯一改变树节点层次关系的操作只能是 addChild 和 removeChild 系列方法
// 获取当前节点的父节点和子节点的相关信息和操作
public get parent(): TreeNode<T> | undefined {
return this._parent;
}
// 从当前节点中获取索引指向的子节点
public getChildAt(index: number): TreeNode<T> | undefined {
if (this._children === undefined) {
return undefined;
}
if (index < 0 || index >= this._children.length) {
return undefined;
}
return this._children[index];
}
// 获取当前节点的子节点个数
public get childCount(): number {
if (this._children !== undefined) {
return this._children.length;
} else {
return 0;
}
}
// 判断当前节点是否有子节点
public hasChild(): boolean {
return this._children !== undefined && this._children.length > 0;
}
// 从当前节点获取根节点及获取当前节点的深度
public get root(): TreeNode<T> | undefined {
let curr: TreeNode<T> | undefined = this;
// 从 this 开始,一直向上遍历
while (curr !== undefined && curr.parent !== undefined) {
curr = curr.parent;
}
// 返回 root 节点
return curr;
}
public get depth(): number {
let curr: TreeNode<T> | undefined = this;
let level: number = 0;
while (curr !== undefined && curr.parent !== undefined) {
curr = curr.parent;
level++;
}
return level;
}
// 规范化输出节点名称(节点名称结合节点深度,深度越深,打印在控制台的节点名称前空格越多,用来在控制台用树结构的方式打印出来相对可视化的树结构)
public repeatString(n: number, target: string = " ") {
let total: string = "";
for (let i = 0; i < n; i++) {
total += target;
}
return total;
}
// 以深度优先的方式递归遍历其子孙方法
public visit(
preOrderFunc: NodeCallback<T> | null = null,
postOrderFunc: NodeCallback<T> | null = null,
indexFunc: Indexer = IndexerL2R
): void {
// 在 子节点递归调用 visit 之前,触发先根(前序)回调
// 注意前序回调的时间点在此处
if (preOrderFunc !== null) {
preOrderFunc(this);
}
// 遍历所有子节点
let arr: Array<TreeNode<T>> | undefined = this._children;
if (arr !== undefined) {
for (let i = 0; i < arr.length; i++) {
// 根据 indexFunc 选取左右遍历还是右左遍历
let child: TreeNode<T> | undefined = this.getChildAt(
indexFunc(arr.length, i)
);
if (child !== undefined) {
// 递归调用 visit
child.visit(preOrderFunc, postOrderFunc, indexFunc);
}
}
}
// 在这个时机触发 postOrderFunc 回调
// 注意后根(后序)回调的时间点在此处
if (postOrderFunc !== null) {
postOrderFunc(this);
}
}
}
对于树结构的解析、遍历,则涉及较多:
迭代器:由于迭代遍历,IEnumerator.ts:
// 迭代器
export interface IEnumerator<T> {
// 将迭代器重置为初始位置
reset(): void;
// 如果没越界, moveNext 将 current 设置为下一个元素并返回 true
// 如果已越界, moveNext 返回 false
moveNext(): boolean;
// 获取当前的元素
readonly current: T | undefined;
}
枚举器:用于确定索引顺序:是从左到右,还是从右到左
Indexer.ts:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-01-29 22:17:26
* @LastEditTime: 2023-01-29 22:20:32
* @LastEditors: tianyw
*/
// 枚举器
// 回调函数类型定义
export type Indexer = (len: number, idx: number) => number;
// 实现获取从左到右的索引号
export function IndexerL2R(len: number, idx: number): number {
return idx;
}
// 实现获取从右到左的索引号
export function IndexerR2L(len: number, idx: number): number {
return len - idx - 1;
}
回调函数:NodeCallback.ts(用于遍历时的回调):
import {
TreeNode } from "./TreeNode";
export type NodeCallback<T> = (node: TreeNode<T>) => void;
先根(前序)枚举器:NodeT2BEnumerator.ts
/*
* @Description:
* @Author: tianyw
* @Date: 2023-01-29 22:20:43
* @LastEditTime: 2023-01-29 22:48:30
* @LastEditors: tianyw
*/
// 先根/前序枚举器
import {
IAdapter } from "./IAdapter";
import {
IEnumerator } from "./IEnumerator";
import {
Indexer } from "./Indexer";
import {
TreeNode } from "./TreeNode";
export class NodeT2BEnumerator<
T,
IdxFunc extends Indexer,
Adapter extends IAdapter<TreeNode<T>>
> implements IEnumerator<TreeNode<T>>
{
private _node: TreeNode<T> | undefined; // 头节点,指向输入的根节点
private _adapter!: IAdapter<TreeNode<T>>; // 枚举器内部持有一个队列或堆栈的适配器,用于存储遍历的元素,指向泛型参数
// 这里 ! 和 ?是相对的: 如 private a1?: string; 则说明 a1 可能是 undefined、null(没有值) ;而 private a1!:string,表示 a1 一定有值
private _currNode!: TreeNode<T> | undefined; // 当前正在操作的节点类型
private _indexer!: IdxFunc; // 当前的 Indexer,用于从左到右还是从右到左遍历,指向泛型参数
public constructor(
node: TreeNode<T> | undefined,
func: IdxFunc,
adapter: new () => Adapter
) {
// 必须要有根节点 否则无法遍历
if (node === undefined) {
return;
}
this._node = node; // 头节点,指向输入的根节点
this._indexer = func; // 设置回调函数
this._adapter = new adapter(); // 调用 new 回调函数
this._adapter.add(this._node); // 初始化时将根节点放入到堆栈或队列中
this._currNode = undefined; // 设定当前 node 为 undefined
}
public reset(): void {
if (this._node === undefined) {
return;
}
this._currNode = undefined;
this._adapter.clear();
this._adapter.add(this._node);
}
public moveNext(): boolean {
// 当队列或栈中没有任何元素时,说明遍历已经全部完成了,返回 false
if (this._adapter.isEmpty) {
return false;
}
// 弹出头部或尾部元素,依赖于 adapter 是 stack 还是 queue
this._currNode = this._adapter.remove();
// 如果当前的节点不为 undefined
if (this._currNode !== undefined) {
// 获取当前节点的儿子个数
let len: number = this._currNode.childCount;
// 遍历所有的儿子
for (let i = 0; i < len; i++) {
// 儿子是从左到右,还是从右到左进入队列或堆栈
// 注意,_indexer 是在这里调用的
let childIdx: number = this._indexer(len, i);
let child: TreeNode<T> | undefined =
this._currNode.getChildAt(childIdx);
if (child !== undefined) {
this._adapter.add(child);
}
}
}
return true;
}
public get current(): TreeNode<T> | undefined {
return this._currNode;
}
}
后根(后序)枚举器:NodeB2TEnumerator.ts:
/*
* @Description:
* @Author: tianyw
* @Date: 2023-01-29 22:48:42
* @LastEditTime: 2023-01-29 22:58:53
* @LastEditors: tianyw
*/
// 后根/后序枚举器
import {
IEnumerator } from "./IEnumerator";
import {
TreeNode } from "./TreeNode";
// 后根/后序枚举器
export class NodeB2TEnumerator<T> implements IEnumerator<TreeNode<T>> {
private _iter: IEnumerator<TreeNode<T>>; // 持有一个枚举器接口
private _arr!: Array<TreeNode<T> | undefined>; // 声明一个数组对象
private _arrIdx!: number; // 当前数组索引
public constructor(iter: IEnumerator<TreeNode<T>>) {
this._iter = iter; // 指向先根迭代器
this.reset(); // 调用 reset,填充数组内容及 _arrIdx
}
public reset(): void {
this._arr = []; // 清空数组
// 调用先根枚举器,将结果全部存入数组
while (this._iter.moveNext()) {
this._arr.push(this._iter.current);
}
// 设置 _arrIdx 为数组的 length
// 因为后根遍历是先根遍历的逆操作,所以是从数组尾部向顶部的遍历
this._arrIdx = this._arr.length;
}
public get current(): TreeNode<T> | undefined {
// 数组越界检查
if (this._arrIdx >= this._arr.length) {
return undefined;
} else {
// 从数组中获取当前节点
return this._arr[this._arrIdx];
}
}
public moveNext(): boolean {
this._arrIdx--;
return this._arrIdx >= 0 && this._arrIdx < this._arr.length;
}
}
遍历工厂:NodeEnumeratorFactory.ts,用于实现深度、广度、左右、上下 八种组合的遍历方法:
import {
IEnumerator } from "./IEnumerator";
import {
IndexerL2R, IndexerR2L } from "./Indexer";
import {
NodeB2TEnumerator } from "./NodeB2TEnumerator";
import {
NodeT2BEnumerator } from "./NodeT2BEnumerator";
import {
Queue } from "./Queue";
import {
Stack } from "./Stack";
import {
TreeNode } from "./TreeNode";
export class NodeEnumeratorFactory {
// 创建深度优先(stack)、从左到右(IndexerR2L)、从上到下的枚举器
public static create_df_l2r_t2b_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
let iter: IEnumerator<TreeNode<T>> = new NodeT2BEnumerator(
node,
IndexerR2L,
Stack
);
return iter;
}
// 创建深度优先(stack),从右到左(IndexerL2R)、从上到下的枚举器
public static create_df_r2l_t2b_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
let iter: IEnumerator<TreeNode<T>> = new NodeT2BEnumerator(
node,
IndexerL2R,
Stack
);
return iter;
}
// 创建广度优先(Queue)、从左到右(IndexerL2R)、从上到下的枚举器
public static create_bf_l2r_t2b_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
let iter: IEnumerator<TreeNode<T>> = new NodeT2BEnumerator(
node,
IndexerL2R,
Queue
);
return iter;
}
// 创建广度优先(Queue)、从左到右(IndexerR2L)、从上到下的枚举器
public static create_bf_r2l_t2b_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
let iter: IEnumerator<TreeNode<T>> = new NodeT2BEnumerator(
node,
IndexerR2L,
Queue
);
return iter;
}
// 上面都是从上到下遍历(先根)遍历
// 下面都是从下到上遍历(后根)遍历,是对上面从上到下(先根)枚举器的包装
// 创建深度优先、从左到右、从下到上的枚举
public static create_df_l2r_b2t_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
// 向上转型,自动(向下转型,需要as或<>手动)
let iter: IEnumerator<TreeNode<T>> = new NodeB2TEnumerator(
NodeEnumeratorFactory.create_df_r2l_t2b_iter(node)
);
return iter;
}
// 创建深度优先、从右到左、从下到上的枚举
public static create_df_r2l_b2t_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
// 向上转型,自动(向下转型,需要as或<>手动)
let iter: IEnumerator<TreeNode<T>> = new NodeB2TEnumerator(
NodeEnumeratorFactory.create_df_l2r_t2b_iter(node)
);
return iter;
}
// 创建广度优先、从左到右、从下到上的枚举
public static create_bf_l2r_b2t_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
// 向上转型,自动(向下转型,需要as或<>手动)
let iter: IEnumerator<TreeNode<T>> = new NodeB2TEnumerator(
NodeEnumeratorFactory.create_bf_r2l_t2b_iter(node)
);
return iter;
}
// 创建广度优先、从右到左、从下到上的枚举
public static create_bf_r2l_b2t_iter<T>(
node: TreeNode<T> | undefined
): IEnumerator<TreeNode<T>> {
// 向上转型,自动(向下转型,需要as或<>手动)
let iter: IEnumerator<TreeNode<T>> = new NodeB2TEnumerator(
NodeEnumeratorFactory.create_bf_l2r_t2b_iter(node)
);
return iter;
}
}
本章参考如下:
《TypeScript 图形渲染实战——基于WebGL的3D架构与实现》