二叉树的前、中、后序递归和非递归遍历(力扣第144、145、94题)
二叉树的遍历分为三种,即先序、中序、后序,这里的先中后都以根节点的访问顺序为基准,根据二叉树的结构特点,进行这三种遍历的方式可以采用递归的方式,也可以采用非递归的方式,采用递归的方式时三种遍历的程序只需改变访问根节点的顺序即可,十分的简单。非递归的方式,主要是借助于栈,最要的不同也是访问结点的顺序不同。现在专门对二叉树的遍历进行一个总结,为自己的编程能力打好一个基础。
1、二叉树的先序遍历
(1)递归
private List<Integer> resList; public List<Integer> preorderTraversal(TreeNode root) { resList = new ArrayList<>(); preTraversal(root); return resList; } private void preTraversal(TreeNode root){ if (root == null){ return; } resList.add(root.val); preTraversal(root.left); preTraversal(root.right); }
(2)非递归
递归的实现本身在底层也是借助于栈,所以我们进行非递归先序遍历的时候,也是借助于栈,栈的特点就是后进先出,所以我们要根据栈的进出特点以及先序遍历的顺序要求进行程序逻辑的编写。
先序遍历,是先访问根节点然后访问左子结点和右子结点,即root->left->right,那么就可以在初始化的时候,将根节点先入栈,然后迭代的时候,每一次迭代的开始,先出栈,表示先访问根结点,然后将当前这个根节点右孩子结点和左孩子结点入栈(栈的特点是后进先出,所以先入栈右孩子,后入栈左孩子),然后继续迭代访问,之所以每一次迭代的开始先将栈顶元素弹出,就是因为这是先序遍历,先访问根结点,栈顶元素就是以它为根节点的子树的根结点。
public List<Integer> preorderTraversal2(TreeNode root) { List<Integer> reslist = new ArrayList<>(); if (root == null){ return reslist; } Stack<TreeNode> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()){ TreeNode curNode = stack.pop(); reslist.add(curNode.val); if (curNode.right != null){ stack.push(curNode.right); } if (curNode.left != null){ stack.push(curNode.left); } } return reslist; }
2、二叉树的中序遍历
(1)递归遍历
List<Integer> resList; public List<Integer> inorderTraversal(TreeNode root) { resList = new ArrayList<>(); if (root == null) { return resList; } inOrderTraver(root); return resList; } private void inOrderTraver(TreeNode root) { if (root == null) { return; } inOrderTraver(root.left); resList.add(root.val); inOrderTraver(root.right); }
(2)非递归遍历
中序非递归遍历也是借助于栈,中序遍历的顺序是:left->root->right,根据这一特点,我们知道最先访问的一定是整棵树的最左边的叶子节点,然后是最左边叶子结点的父亲结点和这个父亲结点的右孩子节点,访问完这个最小子树才会向上,所以我们迭代的时候,应该先一直向左,当到达最左边的叶子结点的时候,它是没有左孩子的,也就不能再向左了,所以那就停止向左搜索,将栈顶元素弹出,此时这个栈顶元素就是最左边的叶子节点,我们可以认为它是以它为根节点的子树的根结点,那么既然没有左孩子,就直接访问它,然后向其右孩子进行搜索,如果没有那就继续弹出栈顶元素,进行下一步的访问,,,,我们之所以在弹出栈顶元素之后只向右搜索是因为,进行弹出栈顶元素的操作是在不能继续向左搜索时才会发生的,所以当选择弹出栈顶元素的时候,一定是向左已经走完了,那么就该访问根节点了,然后就只能向右搜索了。。。
public List<Integer> inorderTraversal2(TreeNode root) { List<Integer> resList = new ArrayList<>(); if (root == null) { return resList; } Stack<TreeNode> stack = new Stack<>(); TreeNode p = root; while (p != null || !stack.isEmpty()) { if (p != null){ stack.push(p); p = p.left; }else { p = stack.pop(); resList.add(p.val); p = p.right; } } return resList; }
3、二叉树的后续遍历
(1)递归遍历
List<Integer> resList; public List<Integer> postorderTraversal(TreeNode root) { resList = new ArrayList<>(); if (root == null) { return resList; } postOrderTraver(root); return resList; } private void postOrderTraver(TreeNode root) { if (root == null) { return; } inOrderTraver(root.left); inOrderTraver(root.right); resList.add(root.val); }
(2)非递归遍历
后续遍历的顺序是:left->right->root,后续非递归采取的迭代方法也是,先一路向左走,依次进栈,然后当左边走完之后,栈顶元素此时一定是最左边的叶子节点,此时不能轻易出栈,因为接下来要有两种选择:一种是如果栈顶元素有右孩子那么需要先访问右孩子,一种是如果栈顶元素没有右孩子那么就直接出栈访问这个栈顶元素。同时还应该设置一个标记变量记录上一次访问的节点,这是因为,当我们访问完左孩子的时候,会去获取这个左孩子的父亲节点(不出栈),然后再通过这个父亲结点,向其右孩子进行搜索,这个父亲节点的右子树的所有节点的访问顺序一定是在它前面的,当右子树的所有节点访问完毕的时候(它们经历了入栈和出栈,入栈前栈顶元素一定是这个父亲结点,出站完毕后,栈顶元素还是这个父亲结点),该访问这个父亲结点了,那么访问前也是会先只获取它的值,然后判断是否可以向右走,但是这个向右已经已经走过了,为了不再向右走而是出栈,应该标记上一次访问的节点,判断这个父亲节点的右孩子是否和标记的上一次访问的节点是一样的,如果一样那么就说明它的右子树已经访问过了,就该访问父亲结点了。之所以标记的是上一次访问的节点是因为,访问父亲结点的时候,如果它有右孩子,其上一次访问的节点一定是它的右孩子。
public List<Integer> postorderTraversal(TreeNode root) { List<Integer> reslist = new ArrayList<>(); if (root == null){ return reslist; } Stack<TreeNode> stack = new Stack<>(); TreeNode p = root; TreeNode mark = null; while (p != null || !stack.isEmpty()){ if (p != null){ stack.push(p); p = p.left; }else { p = stack.peek(); if (p.right != null && p.right != mark){ p = p.right; }else { stack.pop(); reslist.add(p.val); mark = p; p = null; } } } return reslist; }