本周的 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:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
- 示例 2:
输入: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 。
但是并不能返回 2,而应该返回 1 。因为如果 4 的父节点也是 4 的话,那么最长同值路径将会有交叉,如下图所示。
所以递归函数只能返回左右两侧的最长同值路径的较大值,也正是因为如此,所以需要一个全局变量 result 来记录更新实际的最长同值路径。
在计算当作子树的最长同值路径时,还要注意判断左右子节点的值是否和当前根节点相等。以左孩子为例,如果左孩子不为空并且和根节点相等,那么左侧的最长同值路径应该加 1;否则,左侧的最长同值路径应该为 0(路径断开,不连续) 。
然后就是更新最长同值路径 result = Math.max(result,left + right);
- 如果
left和right都不为 0 ,说明此时根节点和左右孩子相等,此时最长同值路径跨过左右子树,所以要和left+right比较 - 如果
left和right中有一个为 0 ,此时left+right正好就是左右一侧的最长同值路径长度 - 如果
left和right都为 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 所需要的最少硬币数,则动态规划的状态方程如下:
有了状态方程,代码很容易实现:
// 方法一 动态规划
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。