二叉树的遍历
这期视频我们看一下如何遍历二叉树。
遍历二叉树是指从根节点开始,访间树中所有的节点。它是其它一些算法的基础,例如深度优先搜索。
对于二叉树、每个节点有左右两个子节点,对于是先访问左子节点、还是当前节点,或是右子节点,可以把遍历分为前序中序和后序三种。接下来我们分别看一下这三种形式。
先看前序遍历,前序遍历会先访问当前节点的值,之后访问左子节点或右子节点,这里假定先访问左子节点,然后再访问右子节点,对于每个子节点都是做同样的操作。
中序遍历则是先访问左子节点,再访问当前节点,最后访问右子节点,对于每个子节点也是重复这个过程。
后序遍历则是先访问左子节点的值,再访问右子节点的值,最后访问当前节点的值。对于每个子节点也是同样的操作。
二叉树遍历的代码实现,最简单直观的方法就是使用递归。
对于前序遍历,我们先打印出当前节点的值,然后递归的调用自己,传递左节点,再调用自己传递右节点。当子节点为 null 时退出递归。
traversePreOrder(treeNode) {
if (!treeNode) {
return;
}
console.log(treeNode.value);
this.traversePreOrder(treeNode.left);
this.traversePreOrder(treeNode.right);
}
中序遍历只需要把打印当前节点的代码放到中间就可以了。
traverseInOrder(treeNode) {
if (!treeNode) {
return;
}
this.traverseInOrder(treeNode.left);
console.log(treeNode.value);
this.traverseInOrder(treeNode.right);
}
后序遍历则是放到末尾。
traversePostOrder(treeNode) {
if (!treeNode) {
return;
}
this.traversePostOrder(treeNode.left);
this.traversePostOrder(treeNode.right);
console.log(treeNode.value);
}
我们来看一下前序遍历的例子,把调用栈显示出来方便查看递归情况。
假如有这样一棵二叉树需要遍历。
tree.traversePreOrder(root);
traversePreOrder(treeNode) {
if (!treeNode) {
return;
}
console.log(treeNode.value);
this.traversePreOrder(treeNode.left);
this.traversePreOrder(treeNode.right);
}
代码:
- 调用 traversePreOrder() 遍历 root | [traversePreOrder(13)]
- 第一行先打印出根节点的值 13。| [traversePreOrder(13)]
- 接下来,执行到遍历左节点的方法 traversePreOrder(6),放入调用栈并执行,打印出 6,此时因为遍历左节点的方法还未 return,所以遍历右子节点的函数调用还不能执行 | 调用栈:[traversePreOrder(6), traversePreOrder(13)]。
- 接着,递归调用 traversePreOrder() 遍历 6 的左子节点,函数压入调用栈并执行,打印出 3 | 调用栈:[traversePreOrder(3), traversePreOrder(6), traversePreOrder(13) ]。
- 接着递归调用 traversePreOrder(treeNode.left) 遍历 3 的左子节点|调用栈:[traversePreOrder(null),traversePreOrder(3), traversePreOrder(6) , traversePreOrder(13)]
- 此时,执行 traversePreOrder(null),treeNode 为 null,函数执行到 if 后,直接 return 了|调用栈:[traversePreOrder(3), traversePreOrder(6) , traversePreOrder(13)]
- 那么接下来就从调用栈拿出最顶部的函数 traversePreOrder(3),继续执行,调用 traversePreOrder(treeNode.right) |调用栈:[traversePreOrder(null), traversePreOrder(3), traversePreOrder(6) , traversePreOrder(13)]
- treeNode.right 也是 null,所以函数直接返回 |调用栈:[ traversePreOrder(3), traversePreOrder(6) , traversePreOrder(13)]。
- 接着继续执行 traversePreOrder(3),后面没有代码了,所以 traversePreOrder(3) 函数返回。| [ traversePreOrder(6) , traversePreOrder(13)]
- 接着执行 traversePreOrder(6),调用它里边的 traversePreOrder(treeNode.right) | [traversePreOrder(9), traversePreOrder(6) , traversePreOrder(13)]。
- 执行打印出 9 | 调用栈 [traversePreOrder(9), traversePreOrder(6) , traversePreOrder(13)]。
- 接着调用 9 里的 traversePreOrder(treeNode.left) | [traversePreOrder(7), traversePreOrder(9), traversePreOrder(6) , traversePreOrder(13)]。
- 执行打印出 7。后面它没有子节点,所以它里边的 traversePreOrder(treeNode.left) 和 traversePreOrder(treeNode.right) 直接返回,这里就不演示,它本身到这里也执行结束了。| [traversePreOrder(9), traversePreOrder(6) , traversePreOrder(13)]。
- 又回到节点 9,它没有右节点,所以也执行结束并返回。| [traversePreOrder(6) , traversePreOrder(13)]。
- 到了 traversePreOrder(6),它的代码也执行完毕了,返回。| [ traversePreOrder(13)]
- 现在开始继续执行 13 根节点的 traversePreOrder(treeNode.right) 方法了,这里边的执行顺序和之前一样,我们简单过一下。
- 调用 traversePreOrder(20) 打印出 20 | [traversePreOrder(20), traversePreOrder(13)]。
- 遍历左节点并打印出 15。 | [traversePreOrder(15), traversePreOrder(20), traversePreOrder(13)]。
- 15 没有子节点直接返回,遍历 20 的右节点,打印出 28。| [traversePreOrder(28), traversePreOrder(20), traversePreOrder(13)]。
- 28 没有左子节点,遍历右子节点,打印出 32 并返回。| [traversePreOrder(32), traversePreOrder(20), traversePreOrder(13)]。
- 20 节点遍历完毕并返回。| [traversePreOrder(13)]。
- 13 根节点遍历完毕并返回 | []。
此时,前序遍历就完成了,结果是:
13 6 3 9 7 20 15 28 32
再看中序遍历:
tree.traverseInOrder(root);
traverseInOrder(treeNode) {
if (!treeNode) {
return;
}
this.traverseInOrder(treeNode.left);
console.log(treeNode.value);
this.traverseInOrder(treeNode.right);
}
这里简单过一下:
- 调用 traverseInOrder(root) 开始遍历根节点 13 | [traverseInOrder(13)]。
- 这里注意,traverseInOrder() 的第一行代码不再是 console.log(),而是直接调用 traverseInOrder(treeNode.left) 遍历左子节点。
- 调用 traverseInOrder(treeNode.left) 遍历 6。| [traverseInOrder(6), traverseInOrder(13)]。
- 调用节点 6 的 traverseInOrder(treeNode.left) ,遍历 3,| [traverseInOrder(3), traverseInOrder(6), traverseInOrder(13)]。
- 3 里边没有子节点,所以它调用 traverseInOrder(treeNode.left),直接返回,这里调用栈就不演示了。
- 执行 console.log 打印出 3,之后调用 traverseInOrder(treeNode.right) 也是直接返回,此时遍历 3 完成。| [traverseInOrder(6), traverseInOrder(13)]。
- 接着继续执行 traverseInOrder(6),打印出 6, 遍历它的右子节点 9。| [traverseInOrder(9), traverseInOrder(6), traverseInOrder(13)]。
- 遍历 9 的左子节点 7 | [traverseInOrder(7), traverseInOrder(9), traverseInOrder(6), traverseInOrder(13)]。
- 7 没有子节点,所以遍历左子节点直接返回,再打印出 7,遍历右子节点直接返回,遍历 7 节点完成。| [traverseInOrder(9), traverseInOrder(6), traverseInOrder(13)]。
- 继续执行 traverseInOrder(9), 打印出 9,9 没有右子节点,所以遍历完成。| [traverseInOrder(6), traverseInOrder(13)]。
- 6 遍历完成。| [traverseInOrder(13)]。
- 继续执行 traverseInOrder(13),打印出 13。
- 遍历它的右子节点 20 。 | [traverseInOrder(20), traverseInOrder(13)]。
- 遍历 20 的左子节点 15。| [traverseInOrder(15),traverseInOrder(20), traverseInOrder(13)]。
- 15 没有子节点,打印出 15,并返回。| [traverseInOrder(20), traverseInOrder(13)]。
- 继续执行 traverseInOrder(20),打印出 20,遍历右子节点 28:| [traverseInOrder(28),traverseInOrder(20), traverseInOrder(13)]。
- 28 没有左子节点,打印出 28,遍历右子节点 32 | [traverseInOrder(32), traverseInOrder(28),traverseInOrder(20), traverseInOrder(13)]。
- 打印出 32,后面所有的函数就都执行完毕并返回了。
中序遍历的结果就是:
3 6 7 9 13 15 20 28 32
再看后序遍历:
tree.traversePostOrder(root)
traversePostOrder(treeNode) {
if (!treeNode) {
return;
}
this.traversePostOrder(treeNode.left);
this.traversePostOrder(treeNode.right);
console.log(treeNode.value);
}
- 遍历根节点 13, | [traversePostOrder(13)]。
- 遍历 13 的 左子节点 6。| [traversePostOrder(6), traversePostOrder(13)]。
- 遍历 6 的左子节点 3。
- 3 没有子节点,打印出 3,并返回。
- 继续执行 traversePostOrder(6),遍历右子节点 9。
- 遍历 9 的左子节点 7.
- 7 没有子节点,打印出 7 并返回。
- 继续执行 traversePostOrder(9),9 没有右子节点,打印出 9 并返回。
- 继续执行 traversePostOrder(6),打印出 6,返回。
- 继续执行 traversePostOrder(13),遍历右子节点 20。
- 遍历 20 的左子节点 15。
- 15 没有子节点,打印出 15,并返回。
- 遍历 20 的右子节点,28。
- 28 没有左子节点,遍历 28 的右子节点 32.
- 32 没有子节点,打印出 32 之后返回。
- 继续执行 traversePostOrder(28),打印出 28,返回。
- 继续执行 traversePostOrder(20),打印出 20,返回。
- 继续执行 traversePostOrder(13),打印出 13,返回。
这样,后序遍历就完成了,结果是:
3 7 9 6 15 32 28 20 13
小结
好了,这个就是二叉树的前序、中序和后序遍历过程,利用递归,调整 console.log 也就是遍历节点相关的逻辑,就可以快速的实现三种次序的遍历过程。你学会了吗?如果有帮助请三连,想学更多有用的前端开发知识,请关注峰华前端工程师,感谢观看!
一系列的课程让你成为高级前端工程师。课程覆盖工作中所有常用的知识点和背后的使用逻辑,示例全部都为工作项目简化而来,学完即可直接上手开发!
即使你已经是高级前端工程师,在课程里也可能会发现新的知识点和技巧,让你的工作更加轻松!
《React 完全指南》课程,包含 React、React Router 和 Redux 详细介绍,所有示例改编自真实工作代码。点击查看详情。
《Vue 3.x 全家桶完全指南与实战》课程,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。
《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。
《JavaScript 基础语法详解》本人所著图书,包含 JavaScript 全面的语法知识和新特性, 可在京东、当当、淘宝等各大电商购买