【JS】二叉树先序、中序、后序(递归和非递归)
1二叉树的结构
定义一个简单的结点:
Node{
value:number,
left:Node,
right:Node
}
2递归遍历
参考学习:https://blog.csdn.net/xin9910/article/details/73476773
2.1伪代码
先序、中序、后序中的“先、中、后”描述的是“当前、左、右”三个结点中,当前结点(根结点)的遍历先后情况。
先序,说明先遍历根结点。
中序,说明先遍历左,再遍历根,最后右。根结点的遍历优先级位于三者的中间。
后序,则是先左,再右,最后根结点。
根结点的优先级虽然分了上面三种,但是左和右的优先级并没有分两种,而是左始终优先于右。
由于对称性,只要掌握了这三种遍历,右优先于左的话,也是很容易调整代码进行修改的。
先序:先遍历根,再遍历左,再遍历右。
preOrder(node:Node):
if(node){
visit(node);
preOrder(node.left)
preOrder(node.right)
}
中序:先遍历左,再遍历根,再遍历右。
midOrder(node:Node):
if(node){
midOrder(node.left)
visit(node);
midOrder(node.right)
}
后序:先遍历左,再遍历右,最后遍历根。
postOrder(node:Node):
if(node){
postOrder(node.left)
postOrder(node.right)
visit(node);
}
2.2JS代码测试
var node = {
value:0,
left:{
value:1,
left:{
value:3,
},
right:{
value:4
}
},
right:{
value:2,
left:{
value:5,
},
right:{
value:6
}
}
}
function preOrder(node){
if(node){
console.log(node.value);
preOrder(node.left)
preOrder(node.right)
}
}
preOrder(node)
console.log("-------");
function midOrder(node){
if(node){
midOrder(node.left)
console.log(node.value);
midOrder(node.right)
}
}
midOrder(node)
console.log("-------");
function postOrder(node){
if(node){
postOrder(node.left)
postOrder(node.right)
console.log(node.value);
}
}
postOrder(node)
console.log("-------");
3非递归遍历
参考学习:https://blog.csdn.net/weixin_42123213/article/details/112688508
3.1伪代码
由于树状结构,其遍历如果不递归,就需要用线性的容器将非线性的树转化为线性的,所以就可以用到数组、栈、队列。即使没有用到,也是需要用到线性化的方式。
假设使用了这样的容器,对于每个结点,放到容器时不代表立即访问,而是按照从容器中取出的顺序进行遍历。
3.1.1先序
先序是先根,再左,最后右。
分析根结点,如果使用队列,则根结点为了先从队列中出来,则需要第一个进入队列,然后左结点再进入,此时右结点如果立即进入了,那么就会排在左结点的子结点的前面,所以右结点不能进入。
右结点不进入的话,后面只能在根结点出来时,才能将右结点加入队列,因此当根结点出来时,左结点和它的孩子们必须全部已经遍历完毕了(a)。
左结点的孩子们也是一种根结点,其右孩子也是不能加入队列,所以只能先搞一个while,把根结点的左孩子的左孩子的...依次都加入到队列中。
但是这些左孩子们还会有自己的右孩子,由于最开始的根结点不能出列,所以无法回头将右孩子加入到队列中,所以
(a)就无法实现了,所以用队列恐不能实现无迭代的先序遍历。
(非严谨的个人推理,能说服自己了。)
然后换栈来分析一下。
首先,对于原始的根结点,一种是自己进栈前,右孩子先进栈,自己再进栈,自己出栈并访问,然后再左孩子进栈。
左孩子作为新的根结点进栈前,...(圆回来了)
但是问题在于,原始根结点的右孩子早早进栈了,那么它的右孩子将无法按上面的判断逻辑进栈,要么再去搞一些复杂的逻辑判断,要么就永远无法进栈。
另一种可以是自己进栈、出栈并访问,右孩子进栈,左孩子进栈....(发现左孩子的操作顺序和根结点的一样了,说明可以代码复用,找到规律了。同时,右孩子出栈时,也能接的上。)
push(root)
while(stack.size()>0):
cur = pop()
visit(cur)
if cur.right
push(cur.right)
if cur.left
push(cur.left)
初步结论:
- 根据自己不太严谨的推理,无递归的先序,可以使用栈,无法纯使用队列。
3.1.2后序
前序是:根-左-右,后序是左-右-根。
首先由于对称性,可以没有难度地调整前序为:根-右-左,这时就发现它和后序的顺序是完全颠倒的,所以就可以按照前序的对称模式(右优先于左)访问,然后将访问结果颠倒一下。
根-右-左的“前序”:
push(root)
while(stack.size()>0):
cur = pop()
visit(cur)
if cur.left
push(cur.left)
if cur.right
push(cur.right)
颠倒成后序:可以在visit(cur)
时将cur放到栈中,最后出栈顺序就是真正的访问顺序,也可以直接用arr.unshift从数组起始位置插入:
push(root)
var visitResult = []//表示后序访问的顺序
while(stack.size()>0):
cur = pop()
visitResult.unshift(cur)
if cur.left
push(cur.left)
if cur.right
push(cur.right)
3.1.3中序
中序是先左,再根,最后右。
首先分析一下,使用队列的可能性。
对于根结点,必须先把左孩子的支线全部走完,才遍历根,所以根早早地入队,很可能不行。
分析使用栈的情况。
一种想法是根结点入栈,左孩子不断入栈,直到左孩子为null,此时已经到叶子了,必须弹出一个访问。
访问完,也就访问完了左和根,就需要访问它的右孩子了,所以右孩子入栈。
然后需要把右孩子的左孩子支线都入栈,也就回到了最初的逻辑结构。
伪代码(错误的思考结果):
push(root)
while(stack.size()>0):
while(root.left){
push(root.left);
root=root.left
}
root = pop()
visit(root)
if(root.right)
push(root.right)
root=cur.right
但是随后发现,假如是一颗只有左子树的简洁的树,那么就会陷入死循环,主要是如果右子树为空,就无法更新root了。
换一种思路,
首先如果一开始就push根结点,那么进入while循环后,还是需要继续push,这样就很不整洁,所以可以将第一个push和while里面的push合并。
stack=[]
while(root || stack.length>0){
while(root){
push(root);
root=root.left
}
//这样就会把所有的左结点全部入栈。
//最终直到叶子的位置上,就停止了while循环,此时需要出栈来访问。
root = pop();
visit(root)
//右结点还没有访问呢,此时是否需要判断if(root.right)
//如果判断并且右结点没有,那么就无法更新root,所以应该无条件更新root为右结点
root = root.right;
//即使root.right为null,到了下一层循环,就不会添加左结点,而是继续pop上一层了。
}
- 另一种是右孩子入栈,根结点入栈,左孩子入栈。
3.2 JS代码测试
var node = {
value:0,
left:{
value:1,
left:{
value:3,
},
right:{
value:4
}
},
right:{
value:2,
left:{
value:5,
},
right:{
value:6
}
}
}
//先序
function pre_tranverse(root){
var stack = [root];
var visitResult = [];//从左到右为遍历顺序
while(stack.length > 0){
var cur = stack.pop();
visitResult.push(cur.value);
if(cur.right){
stack.push(cur.right);
}
if(cur.left){
stack.push(cur.left);
}
}
return visitResult;
}
//后序
function post_tranverse(root){
var stack = [root];
var visitResult = [];//从左到右为遍历顺序
while(stack.length > 0){
var cur = stack.pop();
visitResult.unshift(cur.value);
if(cur.left){
stack.push(cur.left);
}
if(cur.right){
stack.push(cur.right);
}
}
return visitResult;
}
console.log(pre_tranverse(node));
console.log(post_tranverse(node));
function mid_tranverse(root){
var stack =[];
var visitResult = [];//从左到右为遍历顺序
while(root || stack.length > 0){
while(root){
stack.push(root)
root = root.left
}
root = stack.pop();
visitResult.push(root.value)
root = root.right
}
return visitResult;
}
console.log(mid_tranverse(node));
先、后、中的输出结果:
(7) [0, 1, 3, 4, 2, 5, 6]
(7) [3, 4, 1, 5, 6, 2, 0]
(7) [3, 1, 4, 0, 5, 2, 6]