每周Leetcode:102、107、547、687、322

572 阅读8分钟

本周的 5 道 Leetcode 分别是:

  • 102 二叉树的层序遍历
  • 107 二叉树的层次遍历 Ⅱ
  • 547 省份数量
  • 687 最长同值路径
  • 322 零钱兑换

102 二叉树的层序遍历

题目描述

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。(即逐层地,从左到右访问所有节点)。

  • 示例:
二叉树:[3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回其层序遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/bi…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 BFS

二叉树的层次遍历 实际上是 BFS 思想的应用,使用一个队列来实现。但是这道题的输出格式略有不同,同一层的元素要归为一类进行输出。

在原始层次遍历的代码基础上,我们增加两个变量:

  • levelNodes: 用来记录每层的节点数,默认为 1 即根节点
  • index: 表示结果集合的下标,也就是标识当前的元素是 index 层的元素(从 0 开始)

首先,如果根节点为空,直接返回一个空列表。在队首元素出队后,将 levelNodes 减 1,并将出队元素的值加入到结果集第 index 个位置上。

将左右孩子入队(或者为空)后,如果 levelNodes == 0 && !queue.isEmpty(),则表明第 index 层的元素已经遍历完成了,将 index 加 1,此时队列中元素个数就是下一层的元素个数。

在代码中,还需要注意到两个细节:

  • levelNodes == 0 && !queue.isEmpty() 这个判断条件不能去掉第二个条件,否则最后会多一个空集合
  • index++ 后,result 要新加一个元素 result.add(new ArrayList<>()),否则下次 result.get(index) 会报下标越界异常

代码如下:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        if(root == null){
            return new ArrayList<>();
        }
        // 记录每层的节点数
        int levelNodes = 1;
        // 标记当前遍历的是第几层,result结果集的下标
        int index = 0;
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> result = new ArrayList<>();
        result.add(new ArrayList<>());
        TreeNode node = null;
        queue.offer(root);
        while(!queue.isEmpty()){
            node = queue.poll();
            result.get(index).add(node.val);
            levelNodes--;
            if(node.left != null){
                queue.offer(node.left);
            }
            if(node.right != null){
                queue.offer(node.right);
            }
            // 注意这里的判断条件,如果不加 !queue.isEmpty() ,最后会多一个空子集
            if(levelNodes == 0 && !queue.isEmpty()){
                index++;
                result.add(new ArrayList<>());
                levelNodes = queue.size();
            }
        }
        return result;
    }
}

107 二叉树的层次遍历 Ⅱ

题目描述

给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

  • 例如:
给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回其自底向上的层序遍历为:

[
  [15,7],
  [9,20],
  [3]
]

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/bi…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 BFS

这道题的思路和上面的 102 二叉树的层次遍历 是一样的,都使用了 BFS 思想。但是这道题要求我们从最后一层开始输出,我们只需要将每一层的节点 头插入 到结果集合中就可以。

为了提高头插入的效率,结果集合使用 LinkedList,插入时间复杂度为 O(1)

代码如下:

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        if(root == null){
            return new LinkedList<>();
        }
        int levelNodes = 1;
        int index = 0;
        Queue<TreeNode> queue = new LinkedList<>();
        List<List<Integer>> result = new LinkedList<>();
        TreeNode node = null;
        result.add(new ArrayList<>());
        queue.add(root);
        while(!queue.isEmpty()){
            node = queue.poll();
            levelNodes--;
            // 当前层的节点总是存在首元素中
            result.get(0).add(node.val);
            if(node.left != null){
                queue.add(node.left);
            }
            if(node.right != null){
                queue.add(node.right);
            }
            if(levelNodes == 0 && !queue.isEmpty()){
                index++;
                // 头插入一个空元素,存储上一层的元素
                result.add(0,new ArrayList<>());
                levelNodes = queue.size();
            }
        }
        return result;
    }
}

547 省份数量

题目描述

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

  • 示例 1:

image.png

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
  • 示例 2:

image.png

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/nu…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 DFS

这道题可以理解为在找出图中连通分量的个数。每个连通分量构成一个省份。

DFS 的思想是遍历每一个城市,如果该城市 没有被访问过(即还没有组成省份),就去找是否有和该城市相通的其他城市,如果有,再对其他城市进行相同的操作(DFS 递归的过程),直到某个城市和其他所有未被访问的城市都不相通,这时访问过的城市构成了一个省份。

首先,我们需要一个数组 int[] visited 来表示城市是否被访问过,如果城市 i 被访问了,要将 visited[i] 修改为 1

  • visited[i] == 1 表示第 i 个城市被访问过
  • visited[i] == 0 表示第 i 个城市未被访问

DFS 递归函数定义如下:

/**
* DFS 递归函数
* @param isConnected : nxn 矩阵
* @param city : 当前访问的城市
* @param cities : 城市总数
* @param visited : 访问数组
*/
public void dfs(int[][] isConnected,int city,int cities,int[] visited){
    // 依次寻找和 city 相连且未被访问的城市
    for(int i = 0; i < cities; i++){
        if(isConnected[city][i] == 1 && visited[i] == 0){
            // 访问过城市 i 记得修改访问数组
            visited[i] = 1;
            // 对城市 i 递归
            dfs(isConnected,i,cities,visited);
        }
    }
}

有了递归函数,我们遍历城市,对未访问的城市调用递归操作,并记录省份数量。

// 解法一 DFS
public int findCircleNum(int[][] isConnected) {
    int result = 0;
    int cities = isConnected.length;
    int[] visited = new int[cities];
    for(int i = 0; i < cities; i++){
        if(visited[i] == 0){
            visited[i] = 1;
            dfs(isConnected,i,cities,visited);
            result++;
        }
    }
    return result;
}

解法二 BFS

BFS 一般会结合 队列 来做,这道题也不例外。

基本思路和 解法一 相同,也需要一个访问数组,遍历城市,如果未被访问,则加入队列,如果队列不为空,寻找和队首城市相连且未被访问的城市,并加入队列。

直到队列为空,此时说明找到了一个省份。

// 解法二 BFS
public int findCircleNum(int[][] isConnected) {
    Queue<Integer> queue = new LinkedList<>();
    int cities = isConnected.length;
    int[] visited = new int[cities];
    int temp = 0;
    int result = 0;
    for(int i = 0; i < cities; i++){
        if(visited[i] == 0){
            queue.offer(i);
            while(!queue.isEmpty()){
                temp = queue.poll();
                visited[temp] = 1;
                for(int j = 0; j < cities; j++){
                    if(isConnected[temp][j] == 1 && visited[j] == 0){
                        queue.offer(j);
                    }
                }
            }
            result++;
        }  
    }
    return result;
}

解法三 并查集

并查集类的基本模板已经在 200 岛屿数量 中给出。

一开始,所有的城市的祖先都是自己,遍历 isConnected 矩阵,如果城市 i,j 相连,将它们合并,即将它们的祖先设置为同一个祖先。遍历完成后,同一个省份的所有城市的祖先都一样。

最后并查集中 root[i] = i 的城市个数就是最终的省份数量。

并查集类如下,并且将统计结果的函数 result() 定义在其中:

// 并查集类
class UnionFind{
    private int[] root;

    public UnionFind(int[][] isConnected){
        root = new int[isConnected.length];
        for(int i = 0; i < root.length; i++){
            root[i] = i;
        }
    }

    public void union(int x,int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            root[rootX] = rootY;
        }
    }

    public int find(int x){
        if(x == root[x]){
            return x;
        }
        root[x] = find(root[x]);
        return root[x];
    }

    public int result(){
        int result = 0;
        for(int i = 0; i < root.length; i++){
            if(root[i] == i){
                result++;
            }
        }
        return result;
    }
}

遍历 isConnected 矩阵,如果城市相连就合并:

// 解法三 并查集
public int findCircleNum(int[][] isConnected) {
    UnionFind uf = new UnionFind(isConnected);
    int result = 0;
    for(int i = 0; i < isConnected.length; i++){
        for(int j = i+1; j < isConnected[0].length; j++){
            if(isConnected[i][j] == 1){
                uf.union(i,j);
            }
        }
    }
    return uf.result();
}

由于 isConnected 表示的是一个无向图,所以它一定是 对称矩阵,我们只需要遍历上三角(或下三角)的元素即可。

687 最长同值路径

题目描述

给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以 经过也可以不经过 根节点。

注意:两个节点之间的路径长度由它们之间的 边数 表示。

  • 示例 1:
输入:
              5
             / \
            4   5
           / \   \
          1   1   5
输出: 2
  • 示例 2:
输入:
              1
             / \
            4   5
           / \   \
          4   4   5
输出: 2

注意: 给定的二叉树不超过10000个结点。 树的高度不超过1000。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/lo…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 二叉树递归

这个问题可以使用递归来解决,二叉树本身也是一种递归结构。

在递归的过程中,需要不断比较是否有更大的同值路径,因此定义一个全局变量 result = 0 来表示最终返回的最长同值路径的长度。

定义递归函数 int longestPath(TreeNode) ,该函数返回 当前子树左右两侧最长同值路径长度的较大值。下面给出了递归函数的实现:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    private int result = 0;
    public int longestUnivaluePath(TreeNode root) {
        longestPath(root);
        return result;
    }

    public int longestPath(TreeNode root){
        // 1. 如果根节点为空,返回 0
        if(root == null){
            return 0;
        }
        // 2. 首先获得左右子树两侧最长同值路径长度
        int left = longestPath(root.left);
        int right = longestPath(root.right);
        // 3. 分别计算当前子树两侧的最长同值路径长度
        left =  root.left != null && root.left.val == root.val? left + 1 : 0;
        right = root.right != null && root.right.val == root.val ? right + 1 : 0;
        // 4. 更新最长同值路径长度
        result = Math.max(result,left + right);
        return Math.max(left,right);
    }
}

下面详细分析一下代码实现的细节。

首先要明确一点,树中的一条路径是逻辑上的线性关系,不能有交叉,简单来说要能 一笔画

递归函数返回的路径长度并不是真正的子树的最长同值路径,而是包含根结点在内的左右两侧的最长同值路径的较大值。换句话说,递归函数返回的最长同值路径长度不包含跨越左右子树的这种情况,因为这样无法和父节点组成一条路径。

比如下面的二叉树,当递归到第一个 4 时,当前子树的最长同值路径其实是 2 。 image.png

但是并不能返回 2,而应该返回 1 。因为如果 4 的父节点也是 4 的话,那么最长同值路径将会有交叉,如下图所示。 image.png

所以递归函数只能返回左右两侧的最长同值路径的较大值,也正是因为如此,所以需要一个全局变量 result 来记录更新实际的最长同值路径。

在计算当作子树的最长同值路径时,还要注意判断左右子节点的值是否和当前根节点相等。以左孩子为例,如果左孩子不为空并且和根节点相等,那么左侧的最长同值路径应该加 1;否则,左侧的最长同值路径应该为 0(路径断开,不连续) 。

然后就是更新最长同值路径 result = Math.max(result,left + right);

  • 如果 leftright 都不为 0 ,说明此时根节点和左右孩子相等,此时最长同值路径跨过左右子树,所以要和 left+right 比较
  • 如果 leftright 中有一个为 0 ,此时 left+right 正好就是左右一侧的最长同值路径长度
  • 如果 leftright 都为 0 ,说明左右孩子都不等于根节点,就不存在最长同值路径

322 零钱兑换

题目描述

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

  • 示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
  • 示例 2:
输入:coins = [2], amount = 3
输出:-1
  • 示例 3:
输入:coins = [1], amount = 0
输出:0
  • 示例 4:
输入:coins = [1], amount = 1
输出:1
  • 示例 5:
输入:coins = [1], amount = 2
输出:2
  • 提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/co…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

如果选择了硬币 i (有 coins.length 个选择) ,那么硬币个数要增加 1,同时再继续挑选硬币,使得达到 amout - coins[i] 面值的硬币个数最少,这是该问题的最优子结构。

dp[i] 表示组成金额为 i 所需要的最少硬币数,则动态规划的状态方程如下:

dp[i]=minjcoins(dp[ij]+1)dp[i] = min_{j \in coins}(dp[i - j] + 1)

有了状态方程,代码很容易实现:

// 方法一 动态规划
public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    for(int i = 0; i < dp.length; i++){
        dp[i] = amount + 1;
    }
    dp[0] = 0;
    for(int i = 1; i <= amount; i++){
        for(int j = 0; j < coins.length; j++){
            if(i - coins[j] >= 0){
                dp[i] = Math.min(dp[i],dp[i-coins[j]] + 1);
            }
        }
    }
    return dp[amount] == amount + 1 ? -1 : dp[amount];
}

需要注意的是dp 数组的长度为 amount + 1,并且 dp[0] = 0

初始化时,dp 数组默认都填充为 amount + 1,这是为了在比较较小值时更方便,也可以填充为 Integer.MAX_VALUE

如果在计算过程种,i - coins[j] < 0,即硬币面值要比当前金额更大,说明无法组合,这种情况不做任何操作,如果所有的硬币都比金额大,那么这时 dp[i] 就会保持为 amount + 1,因此可以判断没有任何金额 i 的组合,返回 -1