2021-09-23-二叉树的前,中,后序遍历所涉及的问题

遍历的不同实现方式

非递归

  • 非递归实现思路:借用辅助栈,全部左节点依次入栈

    • 前序是根,左,右,因此入栈时就记录节点,栈顶节点若有右子节点,则将它入栈,循环往复
    	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;
};
  • 而前序 + 后序的使用迭代完成,想不出来,先暂时放这了

本来呢,可以早点写完该篇博客的,但是中秋玩了几天,哈哈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值