WebGL2.0从入门到精通-2、数据结构与算法(五、通用树的封装之代码篇)

五、通用树的封装

由于树及树结构的遍历涉及篇幅较多,这里拆分为两部分进行讲解,第一部分代码篇,第二部分理论篇。

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架构与实现》

猜你喜欢

转载自blog.csdn.net/yinweimumu/article/details/128795685
今日推荐