遍历的不同实现方式
- 二叉树的遍历问题有递归实现,非递归,颜色标记法模拟函数栈实现
非递归
-
非递归实现思路:借用辅助栈,全部左节点依次入栈
- 前序是根,左,右,因此入栈时就记录节点,栈顶节点若有右子节点,则将它入栈,循环往复
var preorderTraversal = function(root) { // 2. 非递归实现 let res = []; if (!root) return res; let stack = []; let node = root; while (stack.length > 0 || node) { // 先序遍历:先处理所有节点的左节点,然后用stack存储,依次出栈来处理所有右子树 while (node) { res.push(node.val); stack.push(node); node = node.left; } node = stack.pop(); node = node.right; } return res; };
- 中序是左,根,右,同理全部左节点依次入栈,出栈顶元素并记录在结果数组,若有右节点则入栈,重复
var inorderTraversal = function (root) { let stack = []; let node = root; while (stack.length > 0 || node) { while (node) { stack.push(node); node = node.left; } node = stack.pop(); res.push(node.val); node = node.right; } return res; }
- 后序是左,右,根,它的难点在于如何先处理右子树再处理它的根节点 ?
- 有右子节点的根节点且该右子节点还未访问过的,需要
重复入栈
- 无右子节点的根节点用prev记录它,防止重复访问右子树的根节点,node置为null防止重复访问该节点的左子树
var postorderTraversal = function(root) { let res = []; let stack = []; let node = root; let prev = null; while (stack.length > 0 || node) { while (node) { stack.push(node); node = node.left; } node = stack.pop(); if (node.right && node.right != prev) { stack.push(node); node = node.right; } else { res.push(node.val); prev = node; node = null; } } return res; }
- 有右子节点的根节点且该右子节点还未访问过的,需要
颜色标记法(V8垃圾回收器的标记最早之前也是用两色标记法)
- 颜色标记法,模拟递归的函数调用栈:像递归写法一样能有一个比较统一的模板,但是他需要将每个节点都重复加入依次,因此它实际的操作次数为2n,即时间复杂度为O(2n)
// 后序遍历,其它遍历调整栈压入的顺序即可,white代表节点还未访问过,gray代表节点已经访问过 var postorderTraversal = function(root) { if (!root) return []; let res = []; let stack = [{ color: "white", node: root }]; while (stack.length > 0) { let { color, node } = stack.pop(); if (!node) continue; if (color == "white") { // 左,右,根,栈的话倒序压入 stack.push({ color: "gray", node }); // gray代表已访问过该节点 stack.push({ color: "white", node: node.right }); stack.push({ color: "white", node: node.left }); } else { res.push(node.val); } } return res; }
递归
- 递归就不写了,于此题来说太简单了
二叉树与遍历的关系
- 由任意两个序列从而去推导出二叉树是如何构成的
/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */
- 前序遍历:[ 根节点,左子树的前序遍历,右子树的前序遍历 ]
- 左子树的前序遍历 = [ 根节点,左子树的前序遍历,右子树的前序遍历 ]
- 右子树的前序遍历 = [ 根节点,左子树的前序遍历,右子树的前序遍历 ]
- 中序遍历:[ 左子树的中序遍历,根节点,右子树的中序遍历 ],同理拆分
- 后序遍历:[ 左子树的后序遍历,右子树的后序遍历,根节点 ],同理拆分
而解决此类问题的关键是
如何划分开左右子树的区间
递归
-
基于上述分析用
递归
的方式比较方便,但由于JS的宿主环境没有实现尾递归优化,因此很有可能会爆栈,因此通过迭代
的方式实现也是需要掌握的(递归 <=> 迭代) -
基于上面的分析可以知道
- 每棵树的
前序
遍历序列最左边都是根节点
- 每棵树的
后序
遍历序列最右边都是根节点
- 每棵树的
中序
遍历序列可以用根节点划分开左右子树
- 每棵树的
-
①
前序 + 中序
,可以由前序知道根节点从而划分开中序的左右子树区间,并统计左或右的数量,从而划分开前序的左右子树区间1 - 递归构造左右子树顺序无要求
/** * @param {number[]} preorder * @param {number[]} inorder * @return {TreeNode} */ var buildTree = function(preorder, inorder) { function buildMyTree (preorder,inorder,preorderLeft,preorderRight,inorderLeft,inorderRight) { if (preorderLeft > preorderRight || inorderLeft > inorderRight) return null; // 找出每颗子树的根节点 let root = new TreeNode(preorder[preorderLeft]); // 每次划分后两个序列的长度必然一致,因为是对同一棵树 if (preorderLeft === preorderRight) { return root; } // 根节点在中序遍历中的索引 let rootIndexInorder = inoderIndex.get(preorder[preorderLeft]); // 通过中序遍历获取到左子树的个数 或 右子树的个数 let leftChildCount = rootIndexInorder - inorderLeft; // 此处不建议切割数组,因为又增加了时空 // 先序左子树的索引范围,中序左子树的索引范围 root.left = buildMyTree( preorder,inorder, preorderLeft + 1,preorderLeft + leftChildCount, inorderLeft,rootIndexInorder - 1 ); // 先序右子树的索引范围,中序右子树的索引范围 root.right = buildMyTree( preorder,inorder, preorderLeft + leftChildCount + 1,preorderRight, rootIndexInorder + 1,inorderRight ); return root; } let inoderIndex = new Map(); for (let i = 0; i < inorder.length; i ++) { inoderIndex.set(inorder[i],i); } let root = buildMyTree(preorder,inorder,0,preorder.length - 1,0,inorder.length - 1); return root; };
2 - 但可以利用前序遍历的特点,preIndex从0开始,每次递归+1,刚好对应了当前递归的树根节点(需要先递归构造左子树,再构造右子树)
/** * @param {number[]} preorder * @param {number[]} inorder * @return {TreeNode} */ var buildTree = function(preorder, inorder) { // 找每颗子树的根节点 function buildMyTree(inorderLeft,inorderRight) { if (inorderLeft > inorderRight) return null; let root = new TreeNode(preorder[preIndex ++]); if (inorderLeft === inorderRight) return root; let inorderRootIndex = inoderIndex.get(root.val); root.left = buildMyTree(inorderLeft,inorderRootIndex - 1); root.right = buildMyTree(inorderRootIndex + 1,inorderRight); return root; } let preIndex = 0; let inoderIndex = new Map(); for (let i = 0; i < inorder.length; i ++) { inoderIndex.set(inorder[i],i); } // return buildMyTree(preorder,inorder,0,preorder.length - 1,0,inorder.length - 1); return buildMyTree(0,inorder.length - 1); }
-
②
后序 + 中序
同理1 - 递归模板
var buildTree = function(inorder, postorder) { function buildMyTree(inorderLeft,inorderRight,postorderLeft,postorderRight) { if (inorderLeft > inorderRight || postorderLeft > postorderRight) return null; let root = new TreeNode(postorder[postorderRight]); // 每次划分后两个序列的长度必然一致,因为是对同一棵树 if (inorderLeft === inorderRight) { return root; } let inorderRootIndex = inorderIndex.get(root.val); let leftChildCount = inorderRootIndex - inorderLeft; root.left = buildMyTree( inorderLeft,inorderRootIndex - 1, postorderLeft,postorderLeft + leftChildCount - 1 ); root.right = buildMyTree( inorderRootIndex + 1,inorderRight, postorderLeft + leftChildCount,postorderRight - 1 ); return root; } let inorderIndex = new Map(); for (let i = 0; i < inorder.length; i ++) { inorderIndex.set(inorder[i],i); } return buildMyTree(0,inorder.length - 1,0,inorder.length - 1); }
2 - 同理利用后序的特点,postIndex从某尾开始,每次递归 -1,刚刚好对应了每次递归树的根节点(需要先构造右子树,再构造左子树)
var buildTree = function(preorder, inorder) { // 中序 + 后序,先构造右子树的话后序遍历序列倒数刚刚好对应每个根 function buildMyTree(inorderLeft,inorderRight) { if (inorderLeft > inorderRight) return null; let root = new TreeNode(postorder[postIndex]); postIndex --; // 每次划分后两个序列的长度必然一致,因为是对同一棵树 if (inorderLeft === inorderRight) return root; let inorderRootIndex = inorderIndex.get(root.val); root.right = buildMyTree(inorderRootIndex + 1,inorderRight); root.left = buildMyTree(inorderLeft,inorderRootIndex - 1); return root; } let postIndex = inorder.length - 1; let inorderIndex = new Map(); for (let i = 0; i < inorder.length; i ++) { inorderIndex.set(inorder[i],i); } // return buildMyTree(0,inorder.length - 1,0,inorder.length - 1); return buildMyTree(0,inorder.length - 1); }
-
③
前序 + 后序
,需要通过前序来确定左子树根节点索引
,从而划分开后续的左右子树区间,并统计数量,从而划分开前序的左右子树区间1 - 递归模板,因为没有了中序,所以不再是找根节点将其划分开,而是通过前序或后序的特点,
寻找当前递归树的左子树根节点或右子树根节点将其划分开
var constructFromPrePost = function(preorder, postorder) { // 1. 模板解,先构造左子树或右子树均可 function buildMyTree(preLeft,preRight,postLeft,postRight) { if (postLeft > postRight || preLeft > preRight) { return null; } // 根节点 let root = new TreeNode(preorder[preLeft ++]); // 区间长度为1,说明已无左右子树,直接返回根节点 if (preLeft === preRight) { return root; } // 当前树的左子树根节点 let leftRootIndex = postOrderIndex.get(preorder[preLeft]); let childCount = leftRootIndex - postLeft + 1; // preLeft + 1前序左子树起始索引 root.left = buildMyTree(preLeft + 1,preLeft + childCount,postLeft,leftRootIndex ); // postRight - 1后序右子树末尾索引 root.right = buildMyTree(preLeft + childCount + 1,preRight,leftRootIndex + 1,postRight - 1); return root; } let postOrderIndex = new Map(); for (let i = 0; i < postorder.length; i ++) { postOrderIndex.set(postorder[i],i); } return buildMyTree(0,preorder.length - 1,0,postorder.length - 1);
2.1 - 同理利用前序的特点,preIndex从0开始,每次递归+1,刚刚好对应左子树的根节点
var constructFromPrePost = function(preorder, postorder) { function buildMyTree(postLeft,postRight) { if (postLeft > postRight) { return null; } let root = new TreeNode(preorder[preIndex ++]); if (postLeft === postRight) return root; // 获取左子树根节点 let leftRootIndex = postOrderIndex.get(preorder[preIndex]); root.left = buildMyTree(postLeft,leftRootIndex); root.right = buildMyTree(leftRootIndex+ 1,postRight - 1); return root; } let preIndex = 0; let postOrderIndex = new Map(); for (let i = 0; i < postorder.length; i ++) { postOrderIndex.set(postorder[i],i); } return buildMyTree(0,postorder.length - 1); };
2.2 - 同理利用后序的特点,postIndex从length - 1开始,每次递归-1,刚刚好对应右子树的根节点
- 道理是相同,就不重复去实现了,
可参考后序 + 中序的实现方法2
- 道理是相同,就不重复去实现了,
迭代
- 虽然递归很香,但是递归会将函数上下文压入栈中,这样的话就有可能面临爆栈的风险,因此需要将
递归转为迭代
或者采用尾递归优化
(实际上就是将其转为循环执行,但V8并没有实现这个优化,仅讨论转迭代的方式) - 参考链接,
中序 + 后序
3
/ \
9 20
/ \ \
15 10 7
/ \
5 8
\
4
inorder = [15, 9, 10, 3, 20, 5, 7, 8, 4]
postorder = [15, 10, 9, 5, 4, 8, 7, 20, 3]
- 根节点入辅助栈,每一次循环如下
- 若栈顶节点不等于
inorder[ inorderIndex ]
,则构造右子树 - 否则,循环出栈且inorderIndex–,直至不相等(即找到当前树根节点),构造左子树
- 若栈顶节点不等于
var buildTree = function(inorder, postorder) {
// 递归 => 迭代
let stack = []; // 辅助栈
let root = new TreeNode(postorder[postorder.length - 1]);
stack.push(root);
// 根据后序特点:先构造右子树,找到还未构造的左子树根节点进行构造
let inorderIndex = inorder.length - 1;
for (let i = postorder.length - 2; i >= 0; i --) {
let postorderVal = postorder[i];
let node = stack[stack.length - 1];
if (node.val !== inorder[inorderIndex]) {
// 构造右子树
node.right = new TreeNode(postorderVal);
stack.push(node.right);
} else {
// 构造左子树:相同节点出栈(因为是已构造完的右子树),找到有左子树的根节点进行构造
while (stack.length > 0 && stack[stack.length - 1].val === inorder[inorderIndex]) {
node = stack.pop();
inorderIndex --;
}
node.left = new TreeNode(postorderVal);
stack.push(node.left);
}
}
return root;
};
前序
+中序
同理呀,只不过inorderIndex
从0开始,先构造右子树,再找到还未构造的左子树进行构造
var buildTree = function(inorder, postorder) {
let stack = [];
let root = new TreeNode(preorder[0]);
stack.push(root);
let inorderIndex = 0;
// 先构造左子树,再找到还未构造的右子树进行构造
for (let i = 1; i < preorder.length; i ++) {
let preorderVal = preorder[i];
let node = stack[stack.length - 1];
if (node.val !== inorder[inorderIndex]) {
// 先构造左子树
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
// 构造右子树
while (stack.length > 0 && stack[stack.length - 1].val === inorder[inorderIndex]) {
node = stack.pop();
inorderIndex ++;
}
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
};
- 而前序 + 后序的使用迭代完成,想不出来,先暂时放这了
本来呢,可以早点写完该篇博客的,但是中秋玩了几天,哈哈