2021 05 13
本周整理的 5 道 Leetcode 分别为 53-最大子序和、78-子集、77-组合、46-全排列、200-岛屿数量。
53 最大子序和
题目描述
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
- 示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
复制代码
- 示例 2:
输入:nums = [1]
输出:1
复制代码
- 示例 3:
输入:nums = [0]
输出:0
复制代码
- 示例 4:
输入:nums = [-1]
输出:-1
复制代码
- 示例 5:
输入:nums = [-100000]
输出:-100000
复制代码
- 提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
复制代码
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ma…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一 暴力法
暴力法就是依次求每一个连续子数组的和,然后取最大值。用两个指针 i,j
来表示连续子数组的首尾,每次将 j
向后移动一位,求和,如果 j
移到了最后,将 i
向后移动一位,表示继续计算以下一个字符为首的所有连续子数组的和。
public int maxSubArray(int[] nums){
int result = Integer.MIN_VALUE;
int sum = 0;
for(int i = 0; i < nums.length; ++i){
// 每次子数组的首元素改变后要重新开始求和
sum = 0;
for(int j = i; j < nums.length;++j){
sum += nums[j];
result = Math.max(result,sum);
}
}
return result;
}
复制代码
解法二 动态规划
假设用 p[i]
表示以 nums[i]
结尾的最大连续子序列的和,那么对于 p[i+1]
来说,要么取 nums[i+1]
本身,要么和 p[i]
相加,即:p[i+1] = max(nums[i+1],nums[i+1] + p[i])
。
需要注意的是,p[i]
只能求出每次以 nums[i]
结尾的最大连续子序和,一共有 nums.length
个这样的和,最终应该返回这些和的最大值,我们也可以在计算的过程种,动态更新最终的结结果。
// 动态规划
public int maxSubArray(int[] nums) {
int result = nums[0];
int sum = nums[0];
for(int i = 1; i < nums.length; ++i){
sum = Math.max(sum + nums[i],nums[i]);
result = Math.max(result,sum);
}
return result;
}
复制代码
解法三 分治法
对于给定的数组,如果将数组从中间位置 mid
分为左右两部分,那么最大子序和可能有三种情况,最后只需要返回这三种情况的 较大值 即可。
- 情况 1 :最大子序和在左半边
比如 [1,4,-1,2,-2]
的最大子序和在左边,为 [1,4]
。
- 情况 2 :最大子序和在右半边
比如 [2,-2,-1,4,1]
的最大子序和在右边,为 [4,1]
。
- 情况 3 :最大子序和跨越了左右两边
比如 [-2,2,4,1,-1]
的最大子序和横跨左右两边,为 [2,4,1]
。
定义一个递归函数 int maxSubArray(int[] nums,int left,int right)
,计算数组中 [left,right]
范围内的最大子序和。递归的终止条件为 left == right
。
对于前两种情况,可以直接递归调用得到左右两侧的最大子序和。对于第 3 种情况,并不符合递归的规则,我们需要额外进行计算,下面重点看一下第 3 种情况。
第 3 种情况下,最大连续子序和以下两部分的和:
- 左测以
mid
结尾的最大连续子序和 - 右侧以
mid + 1
开头的最大连续子序和
例如 [-2,2,4,1,-3,4]
,左侧以 mid
结尾的最大连续子序和为 6;右侧以 mid+1
开头的最大连续子序和为 2。所以跨越左右两边的最大连续子序和为 8。
需要注意的是 ,第三种情况并不是左右两侧的最大连续子序和相加。这是因为一侧的最大连续子序和不一定和另一侧的是相连的,比如上图种,右侧的最大连续子序其实是 [4]
,但是它不和左侧的 [2,4]
相邻。
代码实现如下:
// 主方法
public int maxSubArray(int[] nums){
return maxSubArray(nums,0,nums.length - 1);
}
public int maxSubArray(int[] nums,int left,int right){
// 递归终止条件
if(left == right){
return nums[left];
}
int mid = left + (right - left)/2;
int leftMax = maxSubArray(nums,left,mid);
int rightMax = maxSubArray(nums,mid + 1,right);
int midMax = midMax(nums,left,mid,right);
return max(leftMax,rightMax,midMax);
}
// 求解第 3 种情况
private int midMax(int[] nums,int left,int mid,int right){
int leftMax = nums[mid];
int rightMax = nums[mid + 1];
int sum = nums[mid];
int i = 0;
// 从 mid - 1 开始向前遍历
for(i = mid - 1; i >= left; --i){
sum += nums[i];
leftMax = leftMax > sum ? leftMax : sum;
}
sum = nums[mid+1];
// 从 mid + 2 开始向后遍历
for(i = mid + 2; i <= right; ++i){
sum += nums[i];
rightMax = rightMax > sum ? rightMax : sum;
}
return leftMax + rightMax;
}
private int max(int a,int b,int c){
return a > b ? (a > c ? a : c) : (b > c ? b : c);
}
复制代码
78 子集
题目描述
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
- 示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
复制代码
- 示例 2:
输入:nums = [0]
输出:[[],[0]]
复制代码
- 提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
复制代码
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/su…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一 DFS
DFS (深度优先搜索)和 回溯法 都利用了递归思想,我们首先以 nums=[1,2,3]
为例,画出递归树。
由于不计算重复的集合(比如 [1,2]
和 [2,1]
是重复的),所以我们每次可以从当前元素的下一个元素开始遍历,这也是递归树由于剪枝而不完整的原因。
DFS 从递归树的根节点出发,沿着一条路径走到底,再向上返回。所以 DFS 遍历出来的子集的顺序是 [],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]
。
DFS 递归函数如下,其中:
results
表示最终的结果集start
表示当前路径从数组的哪个位置开始遍历subset
表示当前路径表示的子集
public void dfs(int[] nums,List<List<Integer>> results,int start,List<Integer> subset){
// 复制一份 subset,防止引用传递
results.add(new ArrayList<>(subset));
// 递归终止的条件,剪枝条件
if(start == nums.length){
return;
}
for(int i = start; i < nums.length; ++i){
// 沿着当前路径一直前进,遇到一个节点就加入到子集中
subset.add(nums[i]);
dfs(nums,results,i+1,subset);
// 返回上一层后,需要将当前层加入的子集元素移除
subset.remove(subset.size() - 1);
}
}
复制代码
调用递归函数,并返回结果:
public List<List<Integer>> subsets(int[] nums){
List<List<Integer>> results = new ArrayList<>();
dfs(nums,results,0,new ArrayList<Integer>());
return results;
}
复制代码
解法二 回溯法
回溯法按照子集的长度进行遍历,除了空子集外,子集的长度可以是 1 ~ nums.length
。如果遍历递归的过程中,发现子集的长度等于当前的遍历的长度,则向上一层回溯。
回溯法遍历产生子集的顺序为 [],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]
。
回溯递归函数的设计如下,其中:
len
表示当前子集的长度start
表示从哪个位置的元素开始遍历,下一次回溯start + 1
保证了不出现重复集合
// 回溯递归函数
public void backtracking(int[] nums,List<List<Integer>> results,int len,int start,List<Integer> subset){
// 递归终止条件
if(len == subset.size()){
results.add(new ArrayList<>(subset));
return ;
}
for(int i = start; i < nums.length; ++i){
subset.add(nums[i]);
backtracking(nums,results,len,i+1,subset);
subset.remove(subset.size() - 1);
}
}
复制代码
按照可能的长度进行递归回溯调用:
public List<List<Integer>> subsets(int[] nums){
List<List<Integer>> results = new ArrayList<>();
results.add(new ArrayList<>());
for(int i = 1; i <= nums.length; ++i){
backtracking(nums,results,i,0,new ArrayList<Integer>());
}
return results;
}
复制代码
解法三 扩展法
扩展法的思想是遍历数组,将 当前元素 分别加入到之前的每个子集中,形成新的子集,然后将之前的子集和新的子集合并为最终的子集集合。
比如 nums=[1,2,3]
,一开始的子集为 []
。
- 遍历到
1
,加入已有子集,得到[1]
,和之前的子集合并,子集为[],[1]
- 遍历到
2
,加入已有子集,得到[2],[1,2]
,和之前的子集合并,子集为[],[1],[2],[1,2]
- 遍历到
3
,加入已有子集,得到[3],[1,3],[2,3],[1,2,3]
,和之前的合并,子集为[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]
扩展法实现如下:
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> results = new ArrayList<>();
results.add(new ArrayList<>());
for(int i = 0; i < nums.length; ++i){
// 拷贝已有的子集集合
List<List<Integer>> copied = new ArrayList<>();
for(List<Integer> list : results){
ArrayList<Integer> temp = new ArrayList<>(list);
copied.add(temp);
}
// 分别加入当前元素,并加入 results 中
for(List<Integer> list : copied){
list.add(nums[i]);
results.add(list);
}
}
return results;
}
复制代码
77 组合
题目描述
给定两个整数 n
和 k
,返回 1 ... n
中所有可能的 k
个数的组合。
- 示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
复制代码
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/co…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一 回溯法
回溯法的思想和实现逻辑都是类似的,有了上面 78 题的经验,同样可以画出这道题的递归树:
其中,绿色节点部分是 n=4,k=2
的递归树。递归函数 combine
的定义如下,递归的终止条件是 组合的大小和 k 相等 。
// 回溯递归函数
void combine(List<List<Integer>> results,int n,int k,int start,List<Integer> comb){
if(comb.size() == k){
// results.add(comb) 会导致引用传递,不可取
results.add(new ArrayList<>(comb));
return;
}
for(int i = start; i <= n;i++){
comb.add(i);
combine(results,n,k,i+1,comb);
// 回溯到上一层,需要将组合的最后一个元素移除
comb.remove(comb.size() - 1);
}
}
// 主函数
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> results = new ArrayList();
combine(results,n,k,1,new ArrayList<>());
return results;
}
复制代码
遇到递归的问题,除了递归树外,我一般也会画下面这种的简单的框图来加深对递归过程的理解,黑色框表示递归函数的调用,语句顺序从上到下执行。
46 全排列
题目描述
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
- 示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
复制代码
- 示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
复制代码
- 示例 3:
输入:nums = [1]
输出:[[1]]
复制代码
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/pe…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一 回溯法
这道题同样可以使用回溯法来解决,对于 nums=[1,2,3]
,画出它对应的递归树:
递归树每条路径构成了一种排列,每个元素都会出现在排列中,但是出现的顺序不同。所以当发现一条路径中,出现了重复的元素,就进行剪枝。
这道题的递归函数相比上面几题参数更加简单,递归的终止条件就是 当前排列的大小和数组长度相等 即所有元素都在排列中。
// 回溯递归函数
public void permute(List<List<Integer>> results,int[] nums, List<Integer> ans){
if(ans.size() == nums.length){
results.add(new ArrayList<>(ans));
return;
}
for(int i = 0; i < nums.length; i++){
// 剪枝,如果排列中已经出现了该元素,舍去这条路径
if(!ans.contains(nums[i])){
ans.add(nums[i]);
permute(results,nums,ans);
ans.remove(ans.size() - 1);
}
}
}
复制代码
调用递归函数求解:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> results = new ArrayList<>();
permute(results,nums,new ArrayList<>());
return results;
}
复制代码
200 岛屿数量
题目描述
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由 水平方向和/或竖直方向上 相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
- 示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
复制代码
- 示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
复制代码
- 提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0' 或 '1'
复制代码
来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/nu…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一 BFS
一个岛屿的周围要么是边界,要么是 '0'
。遍历二维数组,发现是陆地时,就变成 '0'
,如果周围还有陆地,也变成 '0'
,这样当发现周围都没有陆地时,一个完整的岛屿就找到了,并且由于都变成了水域,也不会和之后的岛屿发生冲突。直到遍历结束。
将陆地周围的陆地变成岛屿,直到周围没有陆地的过程,可以使用 BFS
广度优先搜索,一般 BFS
都会和 队列 一起使用。
遍历二维数组的过程中,如果遇到 '1'
,则进行如下操作:
- 岛屿数量加 1
- 将当前元素加入队列中(元素的行号和列号)
- 将当前元素置为
'0'
,即变成水域 - 遍历队列,直到队列为空
- 取出队首元素,如果四周(上下左右)有陆地,则将周围的元素加入队列并改为
'0'
。
- 取出队首元素,如果四周(上下左右)有陆地,则将周围的元素加入队列并改为
实现代码如下:
// 方法 1 BFS 思想
public int numIslands(char[][] grid) {
Queue<Pos> queue = new LinkedList<>();
int result = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if('1' == grid[i][j]){
result++;
queue.offer(new Pos(i,j));
grid[i][j] = '0';
while(!queue.isEmpty()){
Pos pos = queue.poll();
if(pos.x - 1 >= 0 && grid[pos.x - 1][pos.y] == '1'){
queue.add(new Pos(pos.x - 1, pos.y));
grid[pos.x - 1][pos.y] = '0';
}
if(pos.x + 1 < grid.length && grid[pos.x + 1][pos.y] == '1'){
queue.add(new Pos(pos.x + 1, pos.y));
grid[pos.x + 1][pos.y] = '0';
}
if(pos.y - 1 >= 0 && grid[pos.x][pos.y - 1] == '1'){
queue.add(new Pos(pos.x, pos.y - 1));
grid[pos.x][pos.y - 1] = '0';
}
if(pos.y + 1 < grid[0].length && grid[pos.x][pos.y + 1] == '1'){
queue.add(new Pos(pos.x, pos.y + 1));
grid[pos.x][pos.y + 1] = '0';
}
}
}
}
}
return result;
}
复制代码
总结一下,这道题的思路就遇到一个陆地时,一定是某个岛屿的一部分(所以岛屿数量加 1),向四周扩展,直到周围全是水域,说明这些扩展的元素构成了原来的一个岛屿。
解法二 DFS
在扩展的过程中,也可以使用 DFS 思想,递归调用。递归函数 dfs
的作用是将当前元素置为 '0'
,并对其四周的方向分别进行递归扩展,递归的 终止条件 是四周是边界或者四周本来就是水域。
// dfs 递归函数
void dfs(char[][] grid, int row, int col){
if(row < 0 || row > grid.length - 1 || col < 0 || col > grid[0].length - 1 || grid[row][col] == '0'){
return;
}
grid[row][col] = '0';
dfs(grid,row - 1,col);
dfs(grid,row + 1,col);
dfs(grid,row,col - 1);
dfs(grid,row,col + 1);
}
复制代码
遍历二维数组,遇到陆地就调用 dfs
就可以解决了。
// 方法 2 DFS 思想
public int numIslands(char[][] grid) {
int result = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if('1' ==grid[i][j]){
result++;
dfs(grid,i,j);
}
}
}
return result;
}
复制代码
解法三 并查集
这道题也可以使用 并查集 UnionFind
来解决,这种方法相对复杂,但可以很好的理解并查集思想。
我们可以将岛屿的二维数组映射为一个一维数组 root
,root
的长度为 row*col
(其中,row,col
分别是二维数组的行列),分别对应二维数组的每个元素。
root[i]
表示第 i
个元素的祖先的索引,如果 i==root[i]
,则表示第 i
个元素的祖先就是 root[i]
。例如 root = [1,2,2]
,第 0
个元素祖的位置为 root[0] = 1
,而第 1
个元素祖先的位置为 root[1] = 2
,而 2 == root[2]
,说明数组中的三个元素都有同一个祖先 2
。
再回到岛屿的问题上,如果部分陆地元素上都有同一个祖先,那么就可以认为它们构成一个岛屿。
我们将 root
定义在并查集类中,进行相关操作。UnionFind.class
的定义可以认为是一个模板,其中有一个 find(x)
和 union(x,y)
方法,分别对应查询和合并操作。
// 并查集模板类
class UnionFind{
public UnionFind(char[][] grid){}
public int find(int x){}
public void union(int x, int y){}
}
复制代码
在岛屿问题中,并查集中需要定义两个成员:
root[]
: 数组,映射二维数组,并记录元素的祖先count
: 整数,默认为二维数组的元素个数,表示 合并的次数
在构造函数中,要初始化 root,count
,默认 root[i] = i
,即自己是自己的祖先。
// UnionFind.class 的构造函数
public UnionFind(char[][] grid){
int eles = grid.length * grid[0].length;
count = eles;
root = new int[eles];
for(int i = 0; i < root.length; i++){
root[i] = i;
}
}
复制代码
find(x)
的作用是递归找到第 x
个元素的祖先。
/** 递归找到 x 的祖先 */
public int find(int x){
if(x == root[x]){
return x;
}
root[x] = find(root[x]);
return root[x];
}
复制代码
union(x,y)
的作用是将 x,y
同化合并,让它们的祖先一样。
public void union(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX != rootY){
root[rootX] = rootY;
count--;
}
}
复制代码
同时,UnionFind.class
还应该提供一个 getCount()
函数获取 count
。(或者你可以让 count
的权限为 public
)。
看完了并查集的代码,下面就是要利用并查集来解决岛屿数量的问题。
首先定义一个变量 waters
表示 0
的数量,遍历二维数组:
- 如果是水域,
waters++
- 如果是陆地,当前元素分别和上下左右的四个元素进行
union
操作 - 最后岛屿的数量是
unionFind.count - waters
为什么岛屿的数量是
count - waters
?
我们假设二维数组的元素个数为 N,水域元素有 waters 个。而一个有 n 个元素的岛屿需要合并 n – 1 次。也就是说,最终岛屿的数量 = N – waters – 合并次数,即count - waters
。
// 方法 3 并查集
public int numIslands(char[][] grid) {
int waters = 0;
UnionFind uf = new UnionFind(grid);
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == '0'){
waters++;
}else {
operateDir(uf,grid,i,j,-1,0);
operateDir(uf,grid,i,j,1,0);
operateDir(uf,grid,i,j,0,-1);
operateDir(uf,grid,i,j,0,1);
}
}
}
return uf.getCount() - waters;
}
// 对某个方向进行同化合并操作
private void operateDir(UnionFind uf,char[][] grid,int i,int j,int xDir,int yDir){
int x = i + xDir;
int y = j + yDir;
if(x >= 0 && x < grid.length && y >= 0 && y < grid[0].length && grid[x][y] == '1'){
uf.union(x*grid[0].length + y,i*grid[0].length+j);
}
}
复制代码