Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
哈喽,我是前端西瓜哥。
二叉树的前序遍历(Pre-Order Traversal),是二叉树的深度优先遍历的其中一种方式,指的是 先访问根节点,再访问子树的遍历方式。
递归实现
通常我们会使用递归的方式去实现前序遍历。实现如下:
function preOrder(root) {
if (root === null) return; // 递归结束条件
// 这里放使用 root 的业务代码
// 比如 console.log(root.val);
preOrder(root.left);
preOrder(root.right);
}
递归的优点是简单,只要局部正确了,那整体上也就正确了。
对于初学者来说比较难理解,因为它不符合我们习惯的思维模式。你不知道为什么这样写局部,整体就对了。你也不知道整体上具体发生了什么,因为里面全是套娃。
不过只要你写多了,就能拥有 “我写对了局部,整体虽然不是非常能推理出流程,但代码确实通过了测试用例” 的能力。
但也因为是递归实现,程序运行时会维护一个函数调用栈。在函数中调用其他函数时,临时保持当前函数的环境变量。
但程序提供的调用栈的大小是有限的,当二叉树非常深的时候,要保存的环境变量太多,就会导致栈溢出错误,让程序停止执行。
要解决栈溢出的问题,就需要将递归实现改为迭代实现。
迭代实现
代码实现为:
function preorderTraversal(root) {
const stack = [];
while (root || stack.length) {
while (root) {
// 这里放使用 root 的业务代码
// 比如 console.log(root.val);
stack.push(root);
root = root.left;
}
root = stack.pop();
root = root.right;
}
};
首先我们一路取左节点,将其入栈,直到没有左节点为止。这是模仿递归的 dfs(node.left)。
遍历入栈的同时,我们其实拿到了每一个 “根节点”,在这个地方写上我们的业务逻辑。
while (root) {
// 这里放使用 root 的业务代码
// 比如 console.log(root.val);
stack.push(root);
root = root.left;
}
遍历到节点没有左节点后,我们就需要通过出栈的方式,拿到最后一个节点,然后使用它的右节点。
出栈是为了将节点丢掉。因为这个节点的左节点已经访问过,并且即将访问右子树了,所以就不需要了。对比到递归实现,就是一个函数完全执行完后,销毁当前调用栈。
while (root || stack.length) {
while (root) {
// root 的左节点都入栈。
}
root = stack.pop();
root = root.right;
}
这个右节点如果存在,重新作为新的起点,重复上述的步骤,直到当栈为空且 root 为 null 才结束。
这里继续循环的条件是 while (root || stack.length),stack.length 还好理解,即是否还有节点的右子树待访问。
root 则比较晦涩,在我们获取右节点时,这个右节点还没有入栈,但栈此时可能已经为空了。因此 root 是否为 null 也要纳入循环判断。
结尾
二叉树前序遍历的迭代实现,思路上其实和递归类似,都是通过栈的方式记录好经过的节点,在访问根节点之后,先访问所有的左节点,再访问右节点。
递归实现更简单易懂,但迭代实现解决了栈溢出的问题。
我是啥都写写的前端西瓜哥,欢迎关注我。
文章首发于我的公众号:前端西瓜哥