前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >算法:二叉树遍历类题目

算法:二叉树遍历类题目

作者头像
Tim在路上
发布2022-03-23 14:18:50
2280
发布2022-03-23 14:18:50
举报
文章被收录于专栏:后台技术底层理解

算法:二叉树遍历类题目

树的遍历顺序是依赖于 节点的位置,前序遍历的顺序为 根左右,中序遍历的顺序为 左根右,后序遍历的顺序为 左右根。除此以外还存在层次遍历。

在树类算法中,很多题目是基于树的遍历和树的性质而产生的,熟悉树的遍历和性质是灵活应用树类题目的前提。

那么什么是树和二叉树?

树(tree)是n(n>=0)个结点的有穷集。n=0时称为空树。在任意一个非空树中:(1)每个元素称为结点(node);(2)仅有一个特定的结点被称为根结点或树根(root)。(3)当n>1时,其余结点可分为m(m≥0)个互不相交的集合T1,T2,……Tm,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作根的子树(subtree)。可见树(tree)的定义本身就是迭代递归的定义。

二叉树是树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

1. 前根遍历

1.1 前根的递归遍历

由树的定义可知,树天生具有可迭代的特性。

代码语言:javascript
复制
   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return res;
    }

1.2 前根的迭代遍历

递归一般可以通过栈来进行描述,所以可以通过栈实现前根的迭代遍历。

代码语言:javascript
复制
  public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
          return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while(cur != null || !stack.empty()) {
            if (cur != null) {
                res.add(cur.val);
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode node = stack.pop();
                if(node.right != null) {
                    cur = node.right;
                }
            }
        }
        return res;
    }

这种方式是使用先遍历左子树,如果左子树为NULL, 则回退到存在的右节点,再遍历其左子树。

代码语言:javascript
复制
  public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.add(root);
        while(!stack.empty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            // 先压入右节点,再压入左节点
            if (node.right != null) {
                stack.add(node.right);
            }
            if (node.left != null) {
                stack.add(node.left);
            }
        }
        return res;
    }

也可以采用在压栈时,先压入右节点,再压入左节点。这样在弹出时可以按照先序遍历的顺序从左到右进行弹出。

2. 中根遍历

2.1 中根的递归遍历

代码语言:javascript
复制
   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> inorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        inorderTraversal(root.left);
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        inorderTraversal(root.right);
        return res;
    }

2.2 中根的迭代遍历

代码语言:javascript
复制
public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
          return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while(cur != null || !stack.empty()) {
            if (cur != null) {
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode node = stack.pop();
                res.add(node.val);
                if(node.right != null) {
                    cur = node.right;
                }
            }
        }
        return res;
    }

中根遍历可以先压左节点,再压右节点。再回退弹出时将其放入列表。

3. 后根遍历

3.1 后根的递归遍历

代码语言:javascript
复制
   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        return res;
    }

3.2 后根的迭代遍历

代码语言:javascript
复制
public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if(root == null) return res;
        stack.push(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.pop();
            res.add(cur.val);
            if(cur.left!=null) stack.push(cur.left);
            if(cur.right!=null) stack.push(cur.right);
        }
        Collections.reverse(res);
        return res;
    }

可以采用先压左节点,再压右节点,最后再进行反转列表。另外如果使用的是LinkedList<>,则可以使用addFirst的方法。

代码语言:javascript
复制
public List<Integer> postorderTraversal(TreeNode root) {
        LinkedList<Integer> res = new LinkedList<>();
        Stack<TreeNode> stack = new Stack<>();
        if(root == null) return res;

        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            res.addFirst(cur.val);
            if(cur.left != null) stack.push(cur.left);
            if(cur.right != null) stack.push(cur.right);
        }
        return res;
    }

另外可以将每一个父节点后加一个NULL节点作为标识,在弹出时将其添加到结果List中。

代码语言:javascript
复制
public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.add(root);
    while(!stack.empty()) {
        TreeNode node = stack.pop();
        if (node != null) {
            // 将每一个父节点重新压入, 这里相当于进行了两遍的压栈
            stack.push(node);
            stack.push(null);
            if (node.right != null) {
                stack.add(node.right);
            }
            if (node.left != null) {
                stack.add(node.left);
            }
        } else {
            // 将父节点进行添加
            TreeNode r = stack.pop();
            res.add(r.val);
        }
    }
    return res;
}

4. 层次遍历

4.1 基于递归的层次遍历

代码语言:javascript
复制
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    levelHelper(res, root, 0);
    return res;
}

private void levelHelper(List<List<Integer>> res, TreeNode root, int level) {
    if (root == null)
        return;
    // level表示的是层数,如果level >= list.size(),说明到下一层了,所以
    // 要先把下一层的list初始化,防止下面add的时候出现空指针异常
    if (level >= res.size()) {
        res.add(new ArrayList<>());
    }
    // level表示的是第几层,这里访问到第几层,我们就把数据加入到第几层
    res.get(level).add(root.val);
    // 当前节点访问完之后,再使用递归的方式分别访问当前节点的左右子节点
    levelHelper(res, root.left, level + 1);
    levelHelper(res, root.right, level + 1);
}

传入参数为结果list, 遍历节点root, 和遍历的层数level。如果level大于等于res列表中的。

4.2 基于迭代的层次遍历

代码语言:javascript
复制
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) {
        return result;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        List<Integer> list = new ArrayList<>();
        for (int i=0;i < size; i++) {
            TreeNode node = queue.poll();
            list.add(node.val);
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
        result.add(list);
    }
    return result;
}

使用queue的先进先出的性质,进行层次遍历。

基于树的递归应用问题

由树的定义可知,树的定义是递归的,所以可以通过递归去解决一些类树的问题。使用递归解决问题一般有两种思路:1. 自顶向下。2. 自底向上。

  • 自顶向下意味着在每一个递归层级,先方法当前节点应用计算一些值。
  • 自底向上意味着先进行递归遍历,利用递归的回溯的原理,在递归的回溯过程中应用计算。
  1. 二叉树的最大深度

自顶向下:

代码语言:javascript
复制
int maxDepth = 0;
public int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    callDepth(root, 1);
    return maxDepth;
}

private void callDepth(TreeNode root, int level) {
    // 处理特殊null节点,返回特定值。
    if (root == null) {
        return;
    }
    // 如果当前节点满足条件,更新结果值
    if (root.left == null && root.right == null) {
        maxDepth = Math.max(maxDepth, level);
    }
    // 左右子树进行递归
    callDepth(root.left, level + 1);
    callDepth(root.right, level + 1);
}

自底向上:

代码语言:javascript
复制
public int maxDepth(TreeNode root) {
  // 处理特殊的null节点,返回特定值
  if (root == null) {
      return 0;
  }
  // 处理特定值作为递归出口值
  if (root.left == null && root.right == null) {
      return 1;
  }
  // 左右子树进行递归
  return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
  1. 对称的二叉树

自顶向下:

代码语言:javascript
复制
public boolean isSymmetric(TreeNode root) {
   if (root == null) {
       return false;
   }
   return isSymmetricCore(root.left, root.right);
}

private boolean isSymmetricCore(TreeNode left, TreeNode right) {
    if (left == null && right == null) {
        return true;
    }
    if (left == null || right == null || left.val != right.val) {
        return false;
    }
    return isSymmetricCore(left.left, right.right) && isSymmetricCore(left.right, right.left);
}
  1. 路径总和

自低向上

代码语言:javascript
复制
public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) {
        return false;
    }
    if (root.left == null && root.right == null) {
        return targetSum == root.val;
    }
    return hasPathSum(root.left, targetSum - root.val) ||
    hasPathSum(root.right, targetSum - root.val);
}
  1. 由中序和后序遍历构造二叉树

自顶向下:

代码语言:javascript
复制
public TreeNode buildTree(int[] inorder, int[] postorder) {
    if (postorder.length == 0) {
        return null;
    }
    int len = postorder.length;
    return buildTreeCore(inorder, 0, len - 1, postorder, 0, len - 1);
}

private TreeNode buildTreeCore(int[] inorder, int s1, int e1, int[] postorder, int s2, int e2) {
    if (s2 > e2) {
        return null;
    }
    int rootVal = postorder[e2];
    TreeNode root = new TreeNode(rootVal);
    // 算出当前根节点到s1的距离
    int mid = 0;
    while (inorder[s1 + mid] != rootVal) mid++;
    root.left = buildTreeCore(inorder, s1, s1 + mid - 1, postorder, s2, s2 + mid - 1);
    root.right = buildTreeCore(inorder, s1 + mid + 1, e1, postorder, s2 + mid, e2 - 1);
    return root;
}
  1. 由前序和中序遍历生成二叉树
代码语言:javascript
复制
public TreeNode buildTree(int[] preorder, int[] inorder) {
    if (preorder.length == 0) {
        return null;
    }
    int len = preorder.length;
    return buildTreeCore(preorder, 0, len - 1, inorder, 0, len - 1);
}

private TreeNode buildTreeCore(int[] preorder, int s1, int e1, int[] inorder, int s2, int e2) {
    if (s1 > e1) {
        return null;
    }
    int rootVal = preorder[s1];
    TreeNode root = new TreeNode(rootVal);
    if (s1 == e1) {
        return root;
    }
    // 算出当前根节点到s1的距离
    int mid = 0;
    while (inorder[s2 + mid] != rootVal) mid++;
    root.left = buildTreeCore(preorder, s1 + 1, s1 + mid, inorder, s2 , s2 + mid -1);
    root.right = buildTreeCore(preorder, s1 + mid + 1, e1, inorder, s2 + mid + 1, e2);
    return root;
}

由上面的题目可以看出,大部分的算法题目都可以通过这两种方法实现。但是两种方式并不能适应于所有的题目。如果题目可以在当前的任意节点就可以判断出返回的结果,则适合使用自顶向下(增加剪枝效果)。如果题目必须遍历到叶子节点后才能计算出中间值,则需要使用自底向上。当然如果是根据叶子节点的值的状态,或者在遍历过程中并不需要更新结果值,那么这两种方式其实是很混淆的。

基于树的迭代应用问题

基于迭代进行处理,一般围绕层次遍历,先序,后序遍历或中序的迭代遍历进行展开的。

  1. 二叉树的最大深度
代码语言:javascript
复制
public int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    int count = 0;
    while (!queue.isEmpty()) {
        int size = queue.size();
        while (size -- > 0) {
            TreeNode node = queue.poll();
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        count ++;
    }
    return maxDepth;
}

使用层次遍历的方式实现获取二叉树的深度的算法。

  1. 镜像二叉树
代码语言:javascript
复制
public boolean isSymmetric2(TreeNode root) {
    if (root == null) {
        return false;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root.left);
    queue.add(root.right);
    while (!queue.isEmpty()) {
        TreeNode left = queue.poll();
        TreeNode right = queue.poll();

        if (left == null && right == null) {
            continue;
        }
        if (left == null || right == null || left.val != right.val) {
            return false;
        }

        queue.add(left.left);
        queue.add(right.right);
        queue.add(left.right);
        queue.add(right.left);
    }
    return false;
}

只判断为false的情况,为true的情况下进行跳过。

  1. 路径总和
代码语言:javascript
复制
public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.empty()) {
            TreeNode node = stack.pop();
            if (node.left == null && node.right == null) {
                if (targetSum == node.val) return true;
            }
            if (node.left != null) {
                node.left.val = node.left.val + node.val;
                stack.push(node.left);
            }
            if (node.right != null) {
                node.right.val = node.right.val + node.val;
                stack.push(node.right);
            }
        }
        return false;
    }

由于只使用一次,所以可以使用TreeNode 的val作为临时存储值的地方。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022.02.12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 算法:二叉树遍历类题目
    • 1. 前根遍历
      • 2. 中根遍历
        • 3. 后根遍历
          • 4. 层次遍历
            • 基于树的递归应用问题
              • 基于树的迭代应用问题
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档