我们先看一张图,直观的感受一下树结构(了解的可以跳过这一part)
我们看最上面的一个节点,由它分出来几条边,每条边都有一个节点,然后继续向下延申,直到最后的子节点,直观印象上大概可以这么理解一棵树
树
规范一点的特征和定义应该是:
- 树(tree)是n个节点(node)组成的有限集合
- n=0,该集合为空,称之为空树(empty tree)
- n>0,该集合有一个根节点(root),以及m个( >=0 )子树,即每个互不相交的有限集
- 根节点与其子树的根节点用一条边(edge)相连
- 节点的度表示某个节点的子树的个数
- 度=0 的节点表示为叶子节点(leaf)
- 子节点一般使用children表示
- 父节点使用parent表示
了解以上这些,我们就可以着手构建一个树了
class treeNode {
value: number
children: treeNode[]
parent?: treeNode | null
constructor(value: number, children?: treeNode[], parent?: treeNode) {
this.value = value
this.children = children ?? []
this.parent = parent ?? null
}
add(node: treeNode) {
node.parent = this
this.children.push(node)
}
}
class tree {
root: treeNode
constructor(root: treeNode) {
this.root = root
}
}
const _tree = new tree(new treeNode(0))
const child_1 = new treeNode(10)
_tree.root.add(child_1)
_tree.root.add(new treeNode(11))
child_1.add(new treeNode(20))
child_1.add(new treeNode(21))
child_1.add(new treeNode(22))
我们在聊树结构的时候,经常会提到二叉树,然后不厌其烦的学习二叉树的相关算法和操作,为什么?
许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树特点是每个结点最多只能有两棵子树,且只有左右孩子节点
且使用二叉链表去存储数据,相对与子树的数组存储,可以节省很多内存空间
我们看一下一颗多叉树转为二叉树(一道典型的leetcode习题)的过程
二叉树
由👆我们知道了一颗二叉树的典型特征就是
- 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
👇是一颗满二叉树
根据定义我们先构建节点node对象,包含data, 还有left, right 两个子node对象
class treeNode<T> {
private data: T
private left?: treeNode<T>
private right?: treeNode<T>
constructor(data: T) {
this.data = data
this.left = undefined
this.right = undefined
}
get Data() {
return this.data
}
get Left() {
return this.left
}
set Left(value) {
this.left = value
}
get Right() {
return this.right
}
set Right(value) {
this.right = value
}
//判断是否叶子结点
isLeafNode() {
//没有左右孩子的结点即为叶子结点
return !this.Left && !this.Right
}
toString() {
return this.Data.toString()
}
}
我们看一下将数组转换为一颗二叉树的例子
class BinaryTree<T extends TBaseType> {
private _ListForCreateTree: Array<T>
private head: treeNode<T>
constructor(ListForCreateTree: Array<T>) {
this._ListForCreateTree = ListForCreateTree //用于构造二叉树的列表
this.head = new treeNode(ListForCreateTree[0]) //根节点
this.createTree(this.head, 0)
}
get Head() {
return this.head
}
// 构造一颗二叉树
private createTree(parent: treeNode<T>, index: number) {
let leftIndex = 2 * index + 1;
if (leftIndex < this._ListForCreateTree.length) {
// 添加左孩子
parent.Left = new treeNode(this._ListForCreateTree[leftIndex])
// 递归调用Add方法给左孩子添加孩子节点
this.createTree(parent.Left, leftIndex)
}
let rightIndex = 2 * index + 2
if (rightIndex < this._ListForCreateTree.length) {
// 添加右孩子
parent.Right = new treeNode(this._ListForCreateTree[rightIndex])
// 递归调用Add方法给右孩子添加孩子节点
this.createTree(parent.Right, rightIndex)
}
}
}
树的遍历
树的遍历有两种:深度优先(DFS) 和广度优先(BFS)
👇方两张图直观理解一下:
在关于二叉树的遍历上,DFS可以分为先序、中序、后序三种遍历
- 先序遍历(Preorder Traversal/VLR)
1)访问根节点;
2)访问当前节点的左子树;
3)若当前节点无左子树,则访问当前节点的右子树;
- 中序遍历(Inorder Traversal/LVR)
- 后序遍历(Postorder Traversal/LRV)
深度遍历的实现(ps:代码没有优化空间?):
depthFirstOrder(node?: treeNode<T>) {
node = node || this.Head;
let list: string[] = [];
console.log(node.toString());
list.push(node.toString())
if (node.Left) {
list = list.concat(this.depthFirstOrder(node.Left));
}
if (node.Right) {
list = list.concat(this.depthFirstOrder(node.Right));
}
console.log(list)
return list;
}
广度遍历:
levelOrder() {
const list = []
const queue = []
queue.push(this.Head) //利用队列先入先出的特性 把根结点压入队列
while (queue.length) {
console.log('queue', queue.toString());
let node = queue.shift() as treeNode<T> //出队
list.push(node.toString())
console.log(node.toString(), list.toString())
if (node.Left) {
queue.push(node.Left)
}
if (node.Right) {
queue.push(node.Right)
}
}
return list
}
以上聊了这么多 树结构和前端到底有什么关系?
DOM树
我们看一段vue2编译后的代码
var s = function() {
var t = this
, e = t.$createElement
, a = t._self._c || e;
return a("div", {
staticClass: "risk-index"
}, [a("risk-drop", {
staticClass: "risk-drop-rela",
attrs: {
"risk-level": t.rowData.projectRiskLevel,
"read-only": ""
}
}), t._v(" "), a("el-tooltip", {
attrs: {
effect: "dark",
content: t.rowData.projectName,
placement: "top"
}
}, [a("span", {
staticClass: "el-col-box-span",
on: {
click: t.jumpProjectHome
}
}, [t._v("\n " + t._s(t.rowData.projectName) + "\n ")])]), t._v(" "), a("risk-tag", {
attrs: {
"label-info-list": t.rowData.projectLabels
},
on: {
changeTagDire: t.changeTagDire
}
})], 1)
}
我们试着模拟一下简单的vdom构建
export type T_OBJ = {
[key: string]: any
}
export type T_KEY = string | number
export type T_ArrayNode = Array<IVNode | string | null>
export interface IVNode {
tagName: string
children: T_ArrayNode
key?: T_KEY
text?: string
count: number
props: T_OBJ
parentVNode?: IVNode
render(): HTMLElement | Text
}
export class Element implements IVNode {
tagName: string = 'div'
props: T_OBJ = {}
children: T_ArrayNode = []
key?: T_KEY
text?: string | undefined
count: number = 0
parentVNode?: IVNode
constructor(_tagName: string, props?: T_OBJ, children?: T_ArrayNode) {
this.tagName = _tagName
this.children = children || []
if (props?.text) {
this.text = props.text
delete props.text
}
this.props = props || {}
this.key = props?.key ?? undefined
let count = 0
for (let i = 0; i < this.children.length; i++) {
let c = this.children[i]
if (c instanceof Element) {
count += c.count
} else if (typeof c === 'string') {
this.children[i] = new Element('text', { text: c })
}
count++
}
this.count = count
}
render() {
if (this.tagName === 'text') {
return document.createTextNode(this.text!)
}
const el = document.createElement(this.tagName)
const props = this.props
for (let [key, value] of Object.entries(props)) {
setAttr(el, key, value)
}
this.children.forEach(c => {
let childEL = c instanceof Element ? c.render() : document.createComment('<!--null-->')
el.appendChild(childEL)
})
return el
}
}
// export FUNCTION rather than CLASS , avoid instantiation in constructor with FUNCTION(which actually defined the CLASS) called
export default function (_tagName: string): IVNode
export default function (_tagName: string, children: T_ArrayNode): IVNode
export default function (_tagName: string, props: T_OBJ): IVNode
export default function (_tagName: string, props: T_OBJ, children: T_ArrayNode): IVNode
export default function (_tagName: string, props?: T_OBJ, children?: T_ArrayNode): IVNode {
if (arguments.length === 2) {
const tmp = arguments[1]
if (Array.isArray(tmp)) {
return new Element(_tagName, {}, tmp)
} else {
return new Element(_tagName, tmp, [])
}
} else if (arguments.length === 1) {
return new Element(_tagName, {}, [])
}
return new Element(_tagName, props, children)
}
测试代码
import el from '../src/lib/element'
it('test create-element', () => {
const tree = el('div', { id: 'container' }, [
el('h1', { style: 'color: blue' }, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')]),
])
// 2. generate a real dom from virtual dom. `root` is a `div` element
var root = tree.render() as HTMLElement
console.log(root.outerHTML)
expect(true).toBe(true)
})
那么,树结构在日常开发中如何应用呢?
我们看下面的例子
树结构在实际开发中的应用
菜单栏
菜单栏内菜单切换时,选中的菜单要高亮,已高亮的菜单要清除样式
假设菜单的数据结构是这样的:
interface INavItem {
ID: number
title: string
href: string
isSel?: boolean
children?: INavItem[]
}
type INavList = INavItem []
首先我们要构建节点对象,最👆面我们知道,为查找方便,可以无脑添加parent属性
interface INavItem {
ID: number
title: string
href: string
isSel?: boolean
children?: INavItem[]
parent: INavItem
}
然后我们构建一颗树,树里面当然存在根对象
class sidebars {
private list: INavItem[]
private Root: INavItem
/**
* @description 树的构建
*/
constructor(list: INavItem[]) {
this.list = list
this.Root = { children: list } as INavItem
this.createTree()
}
private createTree() {
let i = 0
const func = (node: INavItem) => {
node.ID = i++; //唯一标识
const children = node.children
if (children) {
children.forEach(brother => {
brother.parent = node //添加指向双亲的指针域
func(brother)
})
}
}
func(this.Root)
}
}
我们使用深度搜索DFS获取选中对象
/**
* @description 查询isSel=true的唯一选项
*/
getSelectedNode() {
let tmp: INavItem | undefined
const func = (node: INavItem) => {
console.log(node.title);
if (node.isSel) tmp = node
const children = node.children
if (children) {
for (let i = 0, len = children.length; i < len && !tmp; i++) {
func(children[i])
}
}
}
func(this.Root)
return tmp
}
多层级的checkbox
- (不)勾选某一层级对象,如果其存在子对象数组,子对象数组跟随变化
- (不)勾选某一层级对象,根据其兄弟对象状态,更新父对象状态,并递归向上更新其祖先对象状态
一般接口返回的数据格式是这样的
const list = [{
label: '',
checked: false,
children: [{
label: '',
checked: false,
children:[]
}]
}]
同样的道理,我们要构建node, 生成一棵树
interface ICheckboxItem {
label: string;
checked: boolean;
children?: ICheckboxItem[];
level: number; // 结点的深度
parent: ICheckboxItem;
}
/** 构建一棵树 */
// 定义根节点
const root = ({children: fetchList} as unknown) as ICheckboxItem
// 深度递归添加parent和level
const createTree = () => {
let l = 0; // 游标
const func = (arr: ICheckboxItem[], parent: ICheckboxItem) => {
l++;
arr.forEach((x) => {
x.parent = parent;
x.level = l;
if (x.children) {
func(x.children, x);
}
});
l--;
};
func(root.value.children!, root.value);
};
createTree();
节点上下游对象状态都要跟随变化,我们可以着手构建两个DFS方法,先向下更新状态,再向上更新状态
const handleChange = (checkboxItem: ICheckboxItem) => {
checkboxItem.checked = !checkboxItem.checked;
const checked = checkboxItem.checked;
// 向下查询
const func = (o: ICheckboxItem) => {
o.children?.forEach((x) => func(x));
o.checked = checked;
};
// 向上回溯
const func2 = (o: ICheckboxItem) => {
if (o.parent) {
o.parent.checked = !o.parent.children!.some((x) => !x.checked);
func2(o.parent);
}
};
func(checkboxItem);
func2(checkboxItem);
};