【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上一层了。
}
  1. 另一种是右孩子入栈,根结点入栈,左孩子入栈。

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]
posted @ 2022-06-11 13:16  greencode  阅读(304)  评论(0编辑  收藏  举报