算法:二叉树遍历类题目

2022-03-23 14:18:50 浏览数 (1)

算法:二叉树遍历类题目

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

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

那么什么是树和二叉树?

树(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作为临时存储值的地方。

0 人点赞