代码随想录算法营DAY14 | 二叉树的递归遍历,二叉树的迭代遍历,二叉树的统一迭代
二叉树知识
链接: 二叉树理论基础篇
1.二叉树的种类:
- 满二叉树
- 完全二叉树
- 二叉搜索树
- 平衡二叉搜索树(C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn)
2.二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。那么链式存储方式就用指针, 顺序存储的方式就是用数组。用数组来存储二叉树如何遍历的呢?如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
3.二叉树的遍历方式
- 深度优先遍历(dfs)
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法) - 广度优先遍历(bfs)
层次遍历(迭代法)
4.二叉树的定义
//js定义
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
问题:
二叉搜索树和大顶堆,小顶堆有什么区别?优先级队列和…队列呢?
答:
144. 二叉树的前(中,后)序遍历【递归法】
题目链接: 144. 二叉树的前序遍历
思路:
递归三要素:
1.确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
这里函数的返回值是一个数组,参数只有一个根节点;终止条件是当当前节点为空就返回;单层递归逻辑(前序)就是中左右。
代码:
//注意js里面树节点的写法
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
//前序遍历函数
var preorderTraversal = function(root) {
let res = [];
const dfs=function(root){
if(root==null) return;
res.push(root.val);
dfs(root.left);
dfs(root.right);
}
dfs(root); //只使用一个参数 使用闭包进行存储结果
return res;
};
问题:
使用闭包存储结果是什么意思?
答:
144. 二叉树的前(中,后)序遍历【迭代法】
题目链接: 144. 二叉树的前序遍历
思路:
【前序】用栈去遍历二叉树的节点。
1.若根节点为空,则返回空数组,若不为空则入栈
2.先弹出栈顶元素,并把值放入数组(中节点处理),然后左右节点入栈,的逻辑是先右后左(这样下一个栈顶元素才是左节点,满足中左右的处理顺序)。
3.栈非空的时候继续处理,循环结束返回数组。
中序遍历,后序遍历用迭代法写不同
【中序】先一直向左遍历入栈,遇到空节点就弹出中节点并把值放入res中,然后再遍历右子树。
【后序】前序遍历是中左右,把写的顺序变一下变成中右左,然后再全部倒序变成左右中就是后续遍历了。
代码:
//前序遍历迭代法
// 入栈 右 -> 左
// 出栈 中 -> 左 -> 右
var preorderTraversal = function(root){
let res = [];
let stack = [];
if(root === null) return res;
stack.push(root);
while(stack.length){
let node = stack.pop();
res.push(node.val);
//千万注意这里是右节点先入栈,下一个循环才能先处理左节点
//一开始写反了orz
if(node.right) stack.push(node.right);
if(node.left) stack.push(node.left);
}
return res;
}
//中序遍历迭代法
var inorderTraversal = function(root,res=[]) {
const stack = [];//这里注意一开始定义的是空的,在循环里面才加入根节点。
if(root === null) return res;
let node = root;
while(stack.length||node){//当根节点为空的时候直接不循环
//一直向左遍历存储,如果遍历到空节点了就开始处理中节点,然后再遍历右节点
if(node){
stack.push(node);
node=node.left;
}
else{
node = stack.pop();
res.push(node.val);
node = node.right;
}
}
return res;
};
//后续遍历迭代法
var postorderTraversal = function(root){
let res = [];
let stack = [];
if(root === null) return res;
stack.push(root);
while(stack.length){
let node = stack.pop();
res.push(node.val);
if(node.left) stack.push(node.left);//这里和前序换了一下位置
if(node.right) stack.push(node.right);
}
return res.reverse();//返回倒序的数组
}
144. 二叉树的前(中,后)序遍历【统一迭代法】
题目链接: 144. 二叉树的前序遍历
思路:
我们以中序遍历为例,在二叉树:听说递归能做的,栈也能做! (opens new window)中提到说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
代码:
//中序统一迭代
var inorderTraversal = function(root){
let res =[];
const stack = [root];
if(root === null) return res;
while(stack.length){
let node = stack.pop();
if(node){
if(node.right)stack.push(node.right);//右入栈
stack.push(node);//中入栈
stack.push(null);
if(node.left)stack.push(node.left);//左入栈
}
else{
node=stack.pop();
res.push(node.val);
}
}
return res;
}
//前序和后序只需要换一下顺序,注意代码和遍历顺序是反着的
感想总结
注意迭代法的代码和遍历顺序是反着的!!需要从下到上读(因为栈的逻辑是先进后出)
1.递归:
比较好写,直接处理中,然后左右分别递归调用就行。
2.迭代法:
前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
后序可以通过中右左的遍历顺序,加上反转数组便捷的得到。
3.统一迭代法:
(标记法)
1.先把根节点入栈,每次取一个栈里的节点;
2.按照遍历顺序反着写代码入栈,在已经遍历但是没有处理的节点后面加一个null;
3.如果遇到null节点,弹出null节点前面的节点并且把值放进res。