二叉树的遍历 js实现
二叉树主要有两种遍历方式:一是深度优先遍历,即先往二叉树底部走,遇到叶子节点再返回;二是广度优先遍历,即对二叉树的每一层逐层遍历节点。
深度优先遍历
-
深度优先遍历中分为三个顺序的遍历:前序遍历、中序遍历、后序遍历。
-
这个顺序是以中间节点为参考的,前序遍历的顺序为中左右,中序遍历的顺序为左中右,后序遍历的顺序为左中右。
-
这是深度优先遍历,所以我们这里的遍历无论是哪一种顺序我们都是在深度优先的基础上实现的。例如前序遍历的中左右,“中”指的是“中间节点”,因为访问二叉树必须从根节点开始,所以无论哪种遍历第一个的中间节点都是根节点。“左”指的是左子树,“右”指的是“右子树”,每一次的遍历都需要到达叶子节点才返回重新进行新子树的遍历。
三种顺序的遍历原理相同,以前序遍历为例:
前序遍历的顺序是中左右,在此例中,节点遍历顺序为ABDFCF。我们需要注意的是每当中间节点遍历后,我们接下来访问的是中间节点的左节点,即A节点的左节点为B。此时,节点重置,B则成为了中间节点,接下来访问的是B节点的左节点即节点D,而D是叶子节点,所以下一访问节点是B节点的右节点E。同理,E节点也是叶子节点,那么整个“左”就遍历完了,所以接下来访问的节点是中间节点A的右节点C,而节点C的左节点为空,那么下一节点为节点F。
这里的节点重置是我自己对于节点遍历的理解,即每一次新节点的访问都会重新重新定义节点。
中序遍历:
后序遍历:
递归遍历
递归遍历是深度优先遍历实现的一种方式,使用递归,我们需要注意三方面:
- 首选需要确定递归的参数和返回值:我们在使用递归之前要确定递归过程中要使用的参数以及递归函数的返回值,根据返回值定义递归函数的类型(在js里无需)。
- 其次,确定递归的终止条件:确定递归的终止条件防止遇到栈溢出的错误,这一步至关重要,经常在使用递归时没有考虑清楚递归的终止条件出现错误。
- 最后,要确定单层递归的逻辑:确定每一层递归的逻辑,需要以及怎样处理信息,重复调用这层逻辑实现递归。
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
递归的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;
};
//二叉树的中序遍历 左中右 与前序遍历相同,位置换一下
var inorderTraversal = function(root) {
let res = [];
const dfs = function(root) {
if(root === null) return;
dfs(root.left);
res.push(root.val);
dfs(root.right);
}
dfs(root);
return res;
};
//二叉树的后序遍历 左右中
var postorderTraversal = function(root) {
let res = [];
const dfs = function(root) {
if(root === null) return;
dfs(root.left);
dfs(root.right);
res.push(root.val);
}
dfs(root);
return res;
};
迭代遍历
深度优先遍历的另一种方式是使用迭代法遍历二叉树,在这个过程中我们做了两个操作:处理元素即将节点值放入数组;访问二叉树,遍历节点。
通过栈实现二叉树的迭代遍历:
&&是JavaScript中的一个逻辑运算符,如果左操作数是真数,则返回右操作数,否则返回左操作数。
-
cur.right && stack.push(cur.right);//右 cur.left && stack.push(cur.left);//左 //表示如果存在cur的右节点或者左节点不为空,则将其推入栈
//前序遍历 中左右 入栈顺序为 右左中
var preorderTraversal = function(root) {
let res = [];//存放元素
if(root === null) return res;
let cur = null;
const stack = [root];//初始化栈
while(stack.length) {
cur = stack.pop();
res.push(cur.val);//中
//短路求值
cur.right && stack.push(cur.right);//右
cur.left && stack.push(cur.left);//左
}
return res;
};
//后序遍历 左右中 入栈顺序 中左右 将前序遍历反转reverse或者添加元素至数组时unshift
var preorderTraversal = function(root) {
let res = [];//存放元素
if(root === null) return res;
let cur = null;
const stack = [root];//初始化栈
while(stack.length) {
cur = stack.pop();
res.unshift(cur.val);//中
//短路求值
cur.left && stack.push(cur.left);//左
cur.right && stack.push(cur.right);//右
}
return res;
};
//中序遍历 左中右
var inorderTraversal = function(root) {
//中序遍历 左中右 左节点到叶子节点 然后弹出栈,中节点,然后右节点
let res = [], cur = root;
const stack = [];//中序遍历时,初始化栈,无数据
if(!root) return res;
while(stack.length || cur) {
if(cur) {//一路到左叶子节点
stack.push(cur);
cur = cur.left;
} else {//右节点
cur = stack.pop();
res.push(cur.val);
cur = cur.right;
}
};
return res;
};
二叉树的统一迭代法
使用统一风格的代码实现二叉树的迭代遍历:首先定义一个空栈stack,和一个空数组res,如果传入的根节点root不为空,则将其压入栈中。进入循环,当栈不为空时,取出栈顶元素node,如果该节点为空,则表示前一个节点没有左子节点或者右子节点,需要将前一个节点的值存入数组res中。如果当前节点node存在右子节点,则将其右子节点压入栈中。如果当前节点node存在左子节点,则将其左子节点压入栈中。**将当前节点node压入栈中,同时压入一个空节点,表示已经访问过当前节点,需要访问其子节点。**重复以上步骤,直到栈为空。
// 前序遍历:中左右
// 压栈顺序:右左中
var preorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
stack.push(node); // 中
stack.push(null);//空节点
};
return res;
};
// 中序遍历:左中右
// 压栈顺序:右中左
var inorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
if (node.right) stack.push(node.right); // 右
stack.push(node); // 中
stack.push(null);
if (node.left) stack.push(node.left); // 左
};
return res;
};
// 后续遍历:左右中
// 压栈顺序:中右左
var postorderTraversal = function(root, res = []) {
const stack = [];
if (root) stack.push(root);
while(stack.length) {
const node = stack.pop();
if(!node) {
res.push(stack.pop().val);
continue;
}
stack.push(node); // 中
stack.push(null);
if (node.right) stack.push(node.right); // 右
if (node.left) stack.push(node.left); // 左
};
return res;
};
广度优先遍历
层序遍历
广度优先遍历是指一层一层的逐层遍历二叉树节点,而这返回的是一个二维数组。
在这个逐层遍历的过程中我们需要注意的是:
-
通过队列先进先出的特性实现逐层遍历
-
如何控制每一层的节点都被遍历
代码实现中较难理解之处:
&&是JavaScript中的一个逻辑运算符,如果左操作数是真数,则返回右操作数,否则返回左操作数。
-
node.left && queue.push(node.left); node.right && queue.push(node.right);
在这种情况下,如果node.left或node.right为空或未定义,它们将是假的,相应的queue.push()函数调用将不会被执行,避免了错误。所以表达式node.left&& queue.push(node.left)和node.right && queue.push(node.right)分别在检查节点是否有左边或右边的子节点,如果有,就把它们推入队列。
-
//length是每一层节点的数量 for(let i = 0; i < length; i++){} //在这length是队列的长度,因为在循环体内,在处理队列时会入队出队产生变化
var levelOrder = function(root) {
//层序遍历 逐层从左节点到右节点 队列
let res = [], queue = [];
queue.push(root);//根节点
if(root === null) {
return res;
}
while(queue.length !== 0) {
let cur = [], length = queue.length;
//length是每一层节点的数量
for(let i = 0; i < length; i++) {
//入队列
let node = queue.shift();
//入数组
cur.push(node.val);
//下一层左右节点 短路求值
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
//二维数组
res.push(cur);
}
return res;
};