二叉树的递归和非递归遍历

575 阅读4分钟

二叉树的的定义:二叉树是一棵空树或者每个节点最多只能有两个子树的有序树,左边的称为左子树,右边的称为右子树。

二叉树遍历是二叉树的基础知识。

二叉树的递归遍历非常简单,主要在于理解和掌握二叉树的非递归遍历以及递归转非递归的方法。

前言

二叉树节点定义如下:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */

文中的代码均在leetcode提交通过。

前序遍历

递归方式

先序递归遍历时,先访问根节点,再先序递归遍历左子树,最后先序递归遍历右子树。递归就是套娃,不用管套娃的细节,只要套住子树的根节点就OK了。

在写递归方法时,第一步雷打不动先写循环退出的条件。

下面的写法中,保存访问过程的list是通过方法参数传递,亦可以把list作为类的成员变量。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
            return Collections.emptyList();
        }
        List<Integer> result = new ArrayList();
        preorderTraversal(root, result);
        return result;
    }
    
    public void preorderTraversal(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        
        result.add(root.val);
        preorderTraversal(root.left, result);
        preorderTraversal(root.right, result);
    }
}

非递归方式

先序是三种遍历方式的非递归写法中最简单的。

要注意的是:

  1. 根节点先入栈;
  2. 循环条件是栈空;
  3. 当前节点的右子节点先入栈,左子节点后入栈。
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
            return Collections.emptyList();
        }
        
        List<Integer> result = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.offerFirst(root);
        while (!stack.isEmpty()) {
            TreeNode t = stack.pollFirst();
            result.add(t.val);
            if (t.right != null) {
                stack.offerFirst(t.right);
            }
            if (t.left != null) {
                stack.offerFirst(t.left);
            }
        }
        return result;
    }
}

中序遍历

递归方式

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        if (Objects.isNull(root)) {
            return Collections.emptyList();
        }
        
        List<Integer> result = new ArrayList<>();
        inorderTraversal(root, result);
        return result;
    }
    
    public void inorderTraversal(TreeNode root, List<Integer> result) {
        if (Objects.isNull(root)) {
            return;
        }
        
        inorderTraversal(root.left, result); // (1)
        result.add(root.val); // (2)
        inorderTraversal(root.right, result); // (3)
    }
}

非递归方式

观察上面中序遍历的递归方法。(1)中将当前节点的左节点递归地入栈,即先将t.left迭代入栈到t = null,(1)执行结束即表示栈中弹出一帧,然后(2)中访问当前节点。最后当前节点变为t.right,以同样的方式处理t.right。

写法一:

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        if (Objects.isNull(root)) {
            return Collections.emptyList();
        }
        
        List<Integer> result = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode t = root;
        while (!stack.isEmpty() || t != null) {
            while (t != null) {
                stack.offerFirst(t);
                t = t.left;
            }
            t = stack.pollFirst();
            result.add(t.val);
            t = t.right;
        }
        
        return result;
    }
}

写法二: 方法的一次递归调用对应下面的一次while循环。

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        if (Objects.isNull(root)) {
            return Collections.emptyList();
        }
        
        List<Integer> result = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode t = root;
        while (!stack.isEmpty() || t != null) {
            if (t != null) {
                stack.offerFirst(t);
                t = t.left;
            } else {
                t = stack.pollFirst();
                result.add(t.val);
                t = t.right;
            }
        }
        return result;
    }
}

后序遍历

递归方式

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) {
            return Collections.emptyList();
        }
        List<Integer> result = new ArrayList<>();
        postorderTraversal(root, result);
        return result;
    }
    
    public void postorderTraversal(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        
        postorderTraversal(root.left, result);
        postorderTraversal(root.right, result);
        result.add(root.val);
    }
}

非递归方式

后序非递归遍历在中序的基础上进行修改,与中序不同的是,后序是先访问左子树,左子树遍历完成之后peek一眼根节点,然后又去访问右子树,访问完了右子树,最后再输出根节点的值。此时访问根节点时,为了不会再次进入右子树,需要用一个pre变量来保存上次访问的节点。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) {
            return Collections.emptyList();
        }
        
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode t = root;
        TreeNode pre = null;
        while (!stack.isEmpty() || t != null) {
            if (t != null) {
                stack.offerFirst(t);
                t = t.left;
            } else {
                t = stack.peekFirst();
                t = t.right;
                if (t != null && t != pre) {
                    stack.offerFirst(t);
                    t = t.left;
                } else {
                    t = stack.pollFirst();
                    res.add(t.val);
                    pre = t;
                    t = null;
                }
            }
        }
        return res;
    }
}

二叉树层序遍历

层序遍历即广度优先搜索,借助队列完成。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return Collections.emptyList();
        }
        List<List<Integer>> result = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        TreeNode last = root;
        TreeNode nlast = null;
        TreeNode t = null;
        List<Integer> list = new ArrayList<>();
        while (!queue.isEmpty()) {
            t = queue.poll();
            list.add(t.val);
            if (t.left != null) {
                queue.offer(t.left);
                nlast = t.left;
            }
            if (t.right != null) {
                queue.offer(t.right);
                nlast = t.right;
            }
            if (t == last) {
                result.add(list);
                list = new ArrayList<>();
                last = nlast;
            }
        }
        return result;
    }
}

运用递归解决问题

二叉树是递归定义的。递归是解决树相关问题最有效和最常用的方法之一。

通常,我们可以通过自顶向下(尾递归)或自底向上(头递归)的递归来解决问题。

自顶向下

自顶向下意味着在每个递归层级,我们首先访问节点来计算一些值,并在递归调用函数时将这些值传递到子节点上。所以自顶向下的解决方案可以被认为是一种先序遍历

特点:

  1. 在递归调用前先计算,将计算结果传入下一层;
  2. 计算结果通过方法入参传递。

递归函数f(root, param)是这样的:

1. if (root == null) return;
2. if (root.left == null && root.right == null) update and return;
3. f(root.left, leftParam);
4. f(root.right, rightParam);

param用来记录每个分支的值,当遍历到叶子节点时,根据param来更新最终的结果。

例如:求二叉树的最大深度。

自顶向下求二叉树深度

从根节点出发,将当前的高度param传递给两个子节点。当左右子节点都为null时说明到达了叶节点,若从根节点到此叶节点的这条路径的高度比当前高度值height大,则更新高度值。

class Solution {
    private int height = 0;
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        upToDown(root, 1);
        return height;
    }
    
    public void upToDown(TreeNode root, int param) {
        if (root == null) {
            return;
        }
        if (root.left == null && root.right == null) {
            height = Math.max(param, height);
        }
        upToDown(root.left, param + 1);
        upToDown(root.right, param + 1);
    }
}

自底向上

自底向上意味着在每个递归层级上,我们首先对所有子节点递归地调用函数,然后根据返回值和根节点本身的值得到答案。这个过程可以看作是后序遍历的一种。

特点:

  1. 在调用下一层时没有计算,直到下一层返回时才进行计算;
  2. 计算结果通过函数返回值传递。

递归函数f(root)是这样的:

1. if (root == null) return;
2. leftResult = f(root.left);
3. rightResult = f(root.right);
4. return result;
class Solution {
    public int maxDepth(TreeNode root) {
        return downToUp(root);
    }
    
    public int downToUp(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(downToUp(root.left), downToUp(root.right)) + 1;
    }
}