树 Tree
树结构被广泛使用.
如: 计算机文件系统, 组织架构, 多级分类等等
程序定义
ts
class Tree<T> {
data: T
children?: Tree<T>[]
isLeaf = true
constructor(data: T) {
this.data = data
}
insert(data: T) {
this.isLeaf = false
const child = new Tree(data)
if (this.children) {
this.children.push(child)
} else {
this.children = [child]
}
return child
}
}
const tree = new Tree(1)
const child1 = tree.insert(2)
child1.insert(3)
tree.insert(4)
特征
- 除了根节点没有父节点外, 树的每个节点都有一个且只有一个父节点
- 树的子节点和其后代节点组成子树(因此树的递归操作很常见)
- 没有子节点的节点被称为叶子节点
- 树的深度为根节点到所有子节点中的最长路径
二叉树
顾名思义, 二叉树就是最多有两个分叉的树.
二叉树的最典型应用就是用于搜索(二叉搜索树), 其机制类似于二分查找, 如果我们规定二叉树的数据满足左节点的数据一定小于 右节点和父节点, 且右节点的数据大于父节点和左节点, 那么每次查询时我们总可以过滤掉一半的内容。例如从100个结果查询一个值,最多只需要7次即可(< 2^7)。
ts
class TreeNode<T> {
value: T
left: TreeNode<T> | null
right: TreeNode<T> | null
constructor(value: T) {
this.value = value
this.left = null
this.right = null
}
}
class BinaryTree<T> {
root: TreeNode<T> | null
constructor() {
this.root = null
}
insert(value: T) {
const newNode = new TreeNode(value)
if (this.root === null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
insertNode(node: TreeNode<T>, newNode: TreeNode<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)
}
}
}
search(value: T): boolean {
return this.searchNode(this.root, value)
}
searchNode(node: TreeNode<T> | null, value: T): boolean {
if (node === null) {
return false
}
if (value < node.value) {
return this.searchNode(node.left, value)
} else if (value > node.value) {
return this.searchNode(node.right, value)
} else {
return true
}
}
}
// 示例用法
const binaryTree = new BinaryTree<number>()
binaryTree.insert(8)
binaryTree.insert(3)
binaryTree.insert(10)
binaryTree.insert(1)
binaryTree.insert(6)
console.log(binaryTree.search(6)) // 输出: true
console.log(binaryTree.search(5)) // 输出: false
此为还有平衡二叉树(AVL树)。是一个特殊的二叉树,不过它要求每个节点的左右子树的高度差不能超过1。
因为当你按从大到小插入树节点时,会出现左子树的深度会一直增加的情况,这个时候插入或者删除往往都是二叉搜索树的最坏查找结果,因此让左右树的高度平衡就是其中的关键。
平衡通常是通过一个旋转操作来完成的,通俗点来说就是节点再排序,就是将左子树种较大的部分放入右子树的左节点,或者将右子树种的较小部分放入左子树的右节点,使得最终结果满足左子树高度差最多为1,且左子树总是小于右子树。
ts
class TreeNode<T> {
value: T
left: TreeNode<T> | null
right: TreeNode<T> | null
/** 子树高度 */
height: number
constructor(value: T) {
this.value = value
this.left = null
this.right = null
this.height = 1
}
}
class AVLTree<T> {
root: TreeNode<T> | null
constructor() {
this.root = null
}
insert(value: T) {
this.root = this.insertNode(this.root, value)
}
insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> {
if (node === null) {
return new TreeNode(value)
}
// 这里来和普通二叉树一致
if (value < node.value) {
node.left = this.insertNode(node.left, value)
} else if (value > node.value) {
node.right = this.insertNode(node.right, value)
} else {
return node // 不允许插入重复的值
}
// 子树高度
node.height =
Math.max(this.getHeight(node.left), this.getHeight(node.right)) + 1
// 比较左右子树的高度差
const balanceFactor = this.getBalanceFactor(node)
// 左旋转
if (balanceFactor > 1 && value < node.left!.value) {
return this.rotateRight(node)
}
// 右旋转
if (balanceFactor < -1 && value > node.right!.value) {
return this.rotateLeft(node)
}
// 左右旋转
if (balanceFactor > 1 && value > node.left!.value) {
node.left = this.rotateLeft(node.left!)
return this.rotateRight(node)
}
// 右左旋转
if (balanceFactor < -1 && value < node.right!.value) {
node.right = this.rotateRight(node.right!)
return this.rotateLeft(node)
}
return node
}
getHeight(node: TreeNode<T> | null): number {
return node ? node.height : 0
}
getBalanceFactor(node: TreeNode<T> | null): number {
return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0
}
rotateLeft(z: TreeNode<T>): TreeNode<T> {
const y = z.right!
const T2 = y.left
y.left = z
z.right = T2
z.height = Math.max(this.getHeight(z.left), this.getHeight(z.right)) + 1
y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1
return y
}
rotateRight(z: TreeNode<T>): TreeNode<T> {
const y = z.left!
const T3 = y.right
y.right = z
z.left = T3
z.height = Math.max(this.getHeight(z.left), this.getHeight(z.right)) + 1
y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1
return y
}
}
trie 前缀树
前缀树最常用的情景在于服务端url的命中算法中。
ts
// 比较以下以下几个url
const url1 = '/a/b/c'
const url2 = '/a/c'
const url3 = '/a/d'
const url4 = '/a/e'
const url5 = '/b/c'
// 假设要匹配URL4,最坏的情况下要匹配4次
// 改一下数据结构
const urlTrie = {
a: {
b: {
c: null
},
c: null,
d: null,
e: null
},
b: {
c: null
}
}
// 当我们要命中/a/e时只需要把 /a/e用/切割成['a', 'e']两个部分再去urlTrie中去查找当碰到null时则代表命中
// 测试仅需要两次访问即可命中
// 当路由越多,这种优势越明显