一、 树简介
1.1 树是什么?
- 一种
分层数据的抽象模型 - 前端工作中常见的树包括:DOM树、级联选择、树形控件...
- JS中没有树,但是可以用Object 和 Array 构建树
{
value: 'guangdong',
label: 'guangdong',
children: [
{
value: 'guangzhou',
label: 'guangzhou',
},{
value: 'shenzhen',
label: 'shenzhen',
}
]
}
1.2 树的常用操作
- 深度/广度优先遍历
- 先中后序遍历
二、 深度与广度优先遍历
2.1 什么是深度/广度优先遍历
- 深度邮件遍历:尽可能深的搜索树的分支。
- 广度优先遍历:先访问离跟节点最近的节点。
2.2 深度优先遍历(dfs)算法口诀
- 访问根节点
- 对根节点的children挨个进行深度优先遍历。
2.2.1 深度优先遍历(dfs)实现
const tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: [],
},
{
val: 'e',
children: [],
}
],
},
{
val: 'c',
children: [
{
val: 'f',
children: [],
},
{
val: 'g',
children: [],
}
],
}
],
};
const dfs = (root) => {
console.log(root.val);
// root.children.forEach(child=>{dfs(child)})
root.children.forEach(dfs);
};
dfs(tree);
2.3 广度优先遍历(bfs)算法口诀
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的children挨个入队
- 重复第二、第三步,知道队列为空
2.3.1 广度优先遍历(bfs)实现
const bfs = (root) => {
const q = [root];
while (q.length > 0) {
const n = q.shift();
console.log(n.val);
n.children.forEach(child => {
q.push(child);
});
}
};
bfs(tree);
三、 二叉树的先中后序遍历
3.1 二叉树是什么
- 树中每个节点最多只有两个子节点
- 在JS中通常使用Object 来模拟二叉树
{
val: 1,
left: {},
right: {}
}
3.11 创建一个二叉树(bt)
- 下面的代码展示会用到
// bt.js
const bt = {
val: 1,
left: {
val: 2,
left: {
val: 4,
left: null,
right: null,
},
right: {
val: 5,
left: null,
right: null,
},
},
right: {
val: 3,
left: {
val: 6,
left: null,
right: null,
},
right: {
val: 7,
left: null,
right: null,
},
},
};
module.exports = bt;
3.2 先序遍历(preorder)的算法口诀[根-左-右]
- 访问根节点
- 对根节点的
左子树进行先序遍历 - 对根节点的
右子树进行先序遍历
const bt = require('./bt');
const preorder = (root) => {
if (!root) { return; }
console.log(root.val);
preorder(root.left);
preorder(root.right);
};
preorder(bt);
3.3 中序遍历(inorder)的算法口诀[左-根-右]
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
const bt = require('./bt');
const inorder = (root) => {
if (!root) { return; }
inorder(root.left);
console.log(root.val);
inorder(root.right);
};
inorder(bt);
3.4 后序遍历(postorder)的算法口诀[左-右-根]
-
- 对根节点的左子树进行后序遍历
-
- 对根节点的右子树进行后序遍历
-
- 访问根节点
// postorder.js
const bt = require('./bt');
const postorder = (root) => {
if (!root) { return; }
postorder(root.left);
postorder(root.right);
console.log(root.val);
};
postorder(bt);
四、为何二叉树那么重要,而不是三叉树、四叉树呢?
4.1 如何让性能整体最优?
有序结构
- 数组:查找易,增删难
- 链表:增删易,查找难
将两者优点结合起来 —— 二叉搜索树 BST :查找易,增删易 —— 可使用二分算法
二叉搜索树 BST
- 左节点(包括其后代) <= 根节点
- 右节点(包括其后代) >= 根节点
4.2 高级二叉树
二叉搜索树 BST ,如果左右不平衡,也无法做到最优。
极端情况下,它就成了链表 —— 这不是我们想要的。
4.2.1 平衡二叉搜索树 BBST :要求树左右尽量平衡
- 树高度
h约等于logn - 查找、增删,时间复杂度都等于
O(logn)
4.2.2 红黑树:一种自动平衡的二叉树
- 节点分 红/黑 两种颜色,通过颜色转换来维持树的平衡
- 相比于普通平衡二叉树,它维持平衡的效率更高
4.2.3 B 树:物理上是多叉树,但逻辑上是一个 BST 。
用于高效 I/O ,如关系型数据库就用 B 树来组织数据结构。
五、堆有什么特点,和二叉树有什么关系
5.1 JS 执行时代码中的变量
- 值类型 - 存储到栈
- 引用类型 - 存储到堆
5.2 堆的特点:
- 节点的值,总是不大于(或不小于)其父节点的值
- 完全二叉树
堆,虽然逻辑上是二叉树,但实际上它使用数组来存储的。
// 上图是一个堆(从小到大),可以用数组表示
const heap = [-1, 10, 14, 25, 33, 81, 82, 99] // 忽略 0 节点
// 节点关系
const parentIndex = Math.floor(i / 2)
const leftIndex = 2 * i
const rightIndex = 2 * i + 1
5.3 堆的排序规则,没有 BST 那么严格,这就造成了
- 查询比 BST 慢
- 增删比 BST 快,维持平衡也更快
- 但整体复杂度都是
O(logn)级别,即树的高度
5.4 堆的应用场景
- 一般使用内存地址(栈中保存了)来查询,不会直接从根节点搜索
- 堆的物理结构是数组,所以查询复杂度就是
O(1)
5.5 总结
- 物理结构是数组(空间更小),逻辑结构是二叉树(操作更快)
- 适用于“堆栈”结构
5.6 答案
- 二叉树,可以充分利用二分法
- 二叉树可以同时规避数字和链表的缺点
- 引申到 BST BBST 等其他扩展结构
5.7 划重点
- 二分法的神奇力量
- 各个高级数据结构的存在价值、设计初衷
- 数据结构是基本功能