持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
二叉树定义
首先,这是一个树状结构,也就是说,从根节点开始,可能有最多两个子节点,形状如下:
每个节点的子节点,有左右之分。
例如,图中染色的节点,没有左子节点,但是拥有右子节点。
遍历二叉树
一般二叉树只会给一个根节点,我们如果要访问到所有的节点,就必须采用某种遍历手段:
我们给出一个具体的树:
前序遍历(先序遍历)
先访问根节点,然后依次访问左子节点,右子节点,所以又简称根左右。
如果按照这种方法,前面的树遍历顺序如下:
一,二,四,三,五,六
递归实现前序遍历
递归的方式是非常自然的,代码也非常容易阅读。
这里我们将结果放入一个数组中,便于观察和理解:
var Result = [];
function scan(node) {
if (!node) {return;}
Result.push(node.value);
scan(node.left);
scan(node.right);
}
scan(root);
循环实现前序遍历
如果不让用递归,那么循环也是可以的,无非就是要自己实现一个栈结构,好存储一些中间状态。
- 初始化的时候,将根节点放入栈中。
- 然后遍历栈,从栈上取一个节点,将节点的右子节点放入栈中
- 然后将节点的左子节点放入栈中
- 此时,栈非空,接着进行上面的操作,循环即可
之所以,先放入节点的右子节点,因为栈是先进后出的,先放右边的,是因为我们先要遍历到左边。
我们看几个图,来理解一下:
根据上面的几个步骤,写出代码如下:
function scan(node) {
let Result = [];
let Stack; // 此处Stack是一个数据结构的栈
Stack.push(node);
while(!Stack.empty()) { // 栈如果非空
let _node = Stack.pop();
Result.push(_node.value);
if (_node.right) {Stack.push(_node.right);}
if (_node.left) {Stack.push(_node.left);}
}
return Result;
}
上述代码里,我们依赖于一个数据结构栈,这个东西用js很容易实现,就不赘述。
中序遍历
按照前面的说法,中序就是指,先访问左子节点,然后访问根节点,然后访问右子节点,简称左根右。
还是看这个图:
左根右的遍历结果就是:
二,四,一,五,三,六
递归实现中序遍历
递归的思路依然简单:
let Result = [];
function scan(node) {
if (!node) {return;}
scan(node.left);
Result.push(node.value);
scan(node.right);
}
循环实现中序遍历
思路还是一样,要用一个栈存储中间状态。
观察一下上面的scan函数,每调用一次,都是将一个元素push到函数栈上,我们依循上面函数的轨迹,理清思路:
这个栈的打开过程就是一直在沿着左子节点往下遍历的过程,一旦左子节点为空了,我们就会pop栈顶元素,然后再去处理这个元素的右子节点。
所以我们的循环里,一定有这种逻辑,就是一直不停将左子节点push到栈的过程。 一旦左子节点为空,那么就立即对栈进行pop,然后存入结果数组,我们来举一个例子:
我们可以用一个表格来记录程序执行状态,表格有三列,分别是:
cur_node,stack,result
初始情况,:
程序的遍历过程应该是这样:
根据上面的遍历过程,我们写出如下代码:
function scan(node) {
let res = [];
let stack;
let cur_node = root;
while(true)
{
if (cur_node) {
stack.push(cur_node);
cur_node = cur_node.left;
}
else
{
if (stack.empty())
{
break;
}
let t_node = stack.top();
stack.pop();
res.push_back(t_node.val);
if(t_node.right)
{
cur_node = t_node.right;
}
}
}
return res;
}
后序遍历
将根节点最后访问,先访问左子节点,再访问右子节点,简称左右根。
递归实现后序遍历
极其简单,直接放代码:
let Result = [];
function scan(node) {
if(!node) {return;}
scan(node.left);
scan(node.right);
Result.push(node.value);
}
循环实现后序遍历
我们仔细观察上面的递归算法,还是按照递归的逻辑,来拟合循环算法。
我们发现依然是不停的沿着左子节点往栈上push节点,但是上面的递归算法暗含了一个顺序,当某个节点的左子节点完全处理完毕之后,才会去处理这个节点的右子节点,且在这两个逻辑之间,栈顶元素就是当前节点,我们在循环算法里需要包含这种信息。上面的递归中,这种执行顺序是由代码直接写好的。
但是在循环里,这种执行顺序需要额外的变量来判断,我们这里新建一个额外的变量,用来存储,上一次在栈上pop出来的节点。
直接看如下代码:
function scan(node) {
let Result = [];
if (!node) {return;} // 特判
let Stack;
Stack.push(node);
let prev_pop = null; // 上一次pop出来的节点
while(!Stack.empty()) {
let t_node = Stack.top();
if (t_node.left && t_node.left!=prev_pop && t_node.right != prev_pop) {
Stack.push(t_node.left);
}else if (t_node.right && t_node.right!=prev_pop) {
Stack.push(t_node.right);
}else{
Result.push(t_node.value);
Stack.pop();
prev_pop = t_node;
}
}
return res;
}