二叉树在前端的工程日常中不常见,但是多叉树组件——树形控件却经常使用到。所以掌握好二叉树的经典算法,其实多叉树也很容易举一反三,不仅有利于面试,对我们前端的日常开发也可以提供很多优化的思路。
二叉树看似复杂,其实其中蕴含了很多规律,只要掌握了二叉树的特点和规律,理解其中的套路,很多算法题其实都是大同小异。
树的基本知识
1. 树的结构
多叉树的结构
树其实可以看作是一棵棵“单元树”的集合,这个“单元树”由2个元素组成:根节点,子节点集合,而子节点同样也是这样一棵“单元树”:
interface TreeNode<T> {
value: T;
children?: Array<TreeNode<T>>;
}
二叉树的结构
而二叉树是树的一种特例,它的节点最多只会有两个子节点,分别把这两个子节点命名为left
、right
,而left
、right
同样也是这样一棵“单元二叉树”:
interface BinaryTreeNode<T> {
value: T;
left?: BinaryTreeNode<T>;
right?: BinaryTreeNode<T>;
}
2.树的深度优先遍历(DFS)
什么叫深度优先遍历呢?深度优先遍历要尽可能地搜索树的分支,简单来说,就是如果发现当前节点有子节点,那么继续往下遍历子节点,直到没有子节点为止,也就是叶子节点为止,然后再继续按照这种方式遍历另外一个分支。
因为树可以看作是一棵棵结构相同“单元树”的集合,我们可以采取递归的思想,将同样的算法应用到同样的结构上。
递归,是一种函数设计思路,就是在函数内部调用自己。
深度优先遍历的口诀是:
- 访问根节点
- 对根节点的子节点挨个进行深度优先遍历
const dfs = (root) => {
if(!rott) return;
// 访问根节点
console.log(root.value);
// 对子节点同样进行dfs
root?.children.forEach(dfs);
}
转换为二叉树:
const binaryDfs = (root) => {
if(!rott) return;
// 访问根节点
console.log(root.value);
// 对子节点同样进行dfs
binaryDfs(root.left);
binaryDfs(root.right);
}
3. 树的广度优先遍历(BFS)
什么是广度优先遍历呢?恰好与深度优先遍历相反,广度优先遍历要先访问离根节点最近的子节点,简单来说,就是按照树的高度,从上往下,遍历完同一层的所有节点后,再遍历下一层节点。
也就是说,当我们从根节点从上往下遍历节点时,先接触到的节点要先遍历,满足先进先出的特性,所以考虑使用队列辅助遍历。
广度优先遍历的口诀是:
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的子节点挨个入队
- 重复2,3步,直到队列为空
const bfs = (root) => {
const q = [root];
// 每一次while遍历其实就是一层节点的遍历
while(q.length) {
// 先进先出
const n = q.shift();
console.log(n.value);
// 子节点挨个入队,下一次while时就优先出队
n?.children.forEach(child => {
q.push(child);
});
}
}
转换为二叉树:
const binaryBfs = (root) => {
if(!rott) return;
const q = [root];
// 每一次while遍历其实就是一层节点的遍历
while(q.length) {
// 先进先出
const n = q.shift();
console.log(n.value);
// 子节点挨个入队,下一次while时就优先出队
if(n.left) {
q.push(n.left);
}
if(n.right) {
q.push(n.right);
}
}
}
(未完待续...)