本文接LeetCode Hot100刷题笔记·Java版(2)
目前处在第一轮学习阶段,学习重点在于解题逻辑的训练。在后续进行二轮复习时,会再次精进该文章,增添代码注释、细节解析、难点剖析等部分。尽请期待吧!
9. 图论
9.1 岛屿数量
class Solution {
public int numIslands(char[][] grid) {
int ans = 0;
//遍历每一个格子
for(int r=0;r<grid.length;r++){
for(int c=0;c<grid[0].length;c++){
//如果当前格子是陆地格子
if(grid[r][c]=='1'){
//从当前格子开始遍历上下左右的格子
dfs(grid,r,c);
//遍历结束,一片所有相邻的'1'的格子全部算完,说明这是一个岛屿
ans++;
}
}
}
return ans;
}
private void dfs(char[][] grid,int r, int c){
//边界条件,如果超出了边界,跳出
if(!inArea(grid,r,c)) return;
//如果格子值不为1,跳出
if(grid[r][c] != '1'){
return;
}
//如果格子值为2,表示该格子已经遍历过了,跳出
if(grid[r][c] == '2') return;
//到这,当前格子遍历过了,将其值设为'2'
grid[r][c] = '2';
//遍历当前格子的上下左右四个位置
dfs(grid,r-1,c); //上
dfs(grid,r+1,c); //下
dfs(grid,r,c-1); //左
dfs(grid,r,c+1); //右
}
private boolean inArea(char[][] grid,int r, int c){
return r>=0 && r<grid.length && c>=0 && c<grid[0].length;
}
}
代码逻辑:DFS
1. 参考二叉树,二叉树中每个节点深度遍历的对象为左子树和右子树两个节点;同样的,将岛屿问题的地图比作二叉树,岛屿问题需要便利的对象则是每个格子的上下左右四个格子。
2. 遍历结束的条件有三个:当格子超出边界;当格子不为1;当格子已经遍历过了。
9.2 腐烂的橘子
class Solution {
public int orangesRotting(int[][] grid) {
int freshOrange = 0;
Queue<int[]> queue = new LinkedList<>();
//遍历所有格子,记录新鲜橘子的数量,将腐烂橘子加入队列
for(int r=0;r<grid.length;r++){
for(int c=0;c<grid[0].length;c++){
if(grid[r][c] == 1){
freshOrange++;
}
if(grid[r][c] == 2){
queue.add(new int[]{r,c});
}
}
}
//如果没有新鲜橘子,说明全是腐烂橘子,所以不需要时间,time = 0;
if(freshOrange == 0) return 0;
int time = 0;
while(freshOrange>0 && !queue.isEmpty()){
//每处理一轮腐烂的橘子,时间加1min
time++;
int n = queue.size();
for(int i=0;i<n;i++){
//处理第i个腐烂的橘子
int[] curOrange = queue.poll();
int r = curOrange[0], c = curOrange[1];
//如果当前腐烂橘子上方的橘子在边界内,且为新鲜橘子,则腐烂它
if(r-1>=0 && grid[r-1][c] == 1){
grid[r-1][c] = 2;
freshOrange--;
//将被腐烂的这个橘子加到队列中,因为它还要继续腐烂其他橘子
queue.add(new int[]{r-1,c});
}
//如果当前腐烂橘子下方的橘子在边界内,且为新鲜橘子,则腐烂它
if(r+1<grid.length && grid[r+1][c] == 1){
grid[r+1][c] = 2;
freshOrange--;
queue.add(new int[]{r+1,c});
}
//如果当前腐烂橘子zuo方的橘子在边界内,且为新鲜橘子,则腐烂它
if(c-1>=0 && grid[r][c-1] == 1){
grid[r][c-1] = 2;
freshOrange--;
queue.add(new int[]{r,c-1});
}
//如果当前腐烂橘子上方的橘子在边界内,且为新鲜橘子,则腐烂它
if(c+1<grid[0].length && grid[r][c+1] == 1){
grid[r][c+1] = 2;
freshOrange--;
queue.add(new int[]{r,c+1});
}
}
}
if(freshOrange>0) return -1;
return time;
}
}
代码逻辑:BFS+队列
1. BFS层序遍历用于求最短路径的问题。例如这题,要求返回腐烂所有橘子的最短时间,即腐烂一层就要花1min,要腐烂多少层就要花多少min。
2. 将所有腐烂的橘子放进一个队列,对队列数量进行循环,处理每一个腐烂的橘子。
9.3 课程表
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//创建入度表
int[] indegrees = new int[numCourses];
//创建邻接表
List<List<Integer>> adjacency = new ArrayList<>();
//创建队列,用于存放入度为0的数
Queue<Integer> queue = new LinkedList<>();
//初始化邻接表,邻接表可以理解为每一条指向
//这里为每个课程都添加一个邻接表
for(int i=0;i<numCourses;i++){
adjacency.add(new ArrayList<>());
}
//添加入度和邻接表
for(int[] cp:prerequisites){
//入度cp[0],在indegrees中对应入度的位置标注出来,即+1
//这里可以理解为,创建出了cp[0]这个点,并在入度表中做记号
indegrees[cp[0]]++;
//这里可以理解为要形成一个指向类似于0-->1,也就是根据指向方向将1添加在0的后面,也就是cp[0]添加到cp[1]后面
adjacency.get(cp[1]).add(cp[0]);
}
//将入度为0的课程加到队列中
//入度为0的意思是当前课程的前序课程是0,也就是说当前可以学习这门课程
for(int i=0;i<numCourses;i++){
if(indegrees[i] == 0){
queue.add(i);
}
}
//处理入度为0的部分
//如果queue为空,且numCourese>0说明当前课程形成了环,是学不完的
while(!queue.isEmpty()){
//取出一个入度为0的课程,说明这个课程可以学完
int pre = queue.poll();
//学完 总课程数-1
numCourses--;
//遍历当前这个入度为0的所有指向
for(int cur : adjacency.get(pre)){
//如果入度表中这个指向的值 -1 等于零说明当前这个课程的前序课程为0
if(--indegrees[cur] == 0){
//将这个课程继续加入队列,用于后续循环
queue.add(cur);
}
}
}
return numCourses == 0;
}
}
代码逻辑:入度表BFS
1. 这题需要首先了解一下什么是入度,什么是邻接以及什么是有向无环图。
2. 在本题中我们需要达到的最终目的就是各个课程先后进行连接指向,如果形成了一个有向无环图则返回true。
3. 结合本题,我认为可以将入度理解为每创建的一个课程节点,而邻接表则表示形成一条这个课程节点的指向。
4. 代码逻辑是先将所有课程入度,并创建邻接表,即每个课程的指向关系。然后取出入度为0的课程,因为入度为0的课程表示这门课程的前序课程为0,这门课程可以直接学习。如果不存在入度为0的课程,说明整个课程指向形成了环,表示永远不可能学完。
9.4 实现Trie(前缀树)
//构造一个节点类型
class Node{
Node[] son = new Node[26];
boolean endFlag;
}
class Trie {
//引入节点类
private Node root;
public Trie() {
root = new Node();
}
public void insert(String word) {
//初始化当前节点为根节点
Node cur = root;
//遍历插进来字符串的每个字母
for(char c : word.toCharArray()){
// 将 'a' ~ 'z' 映射到 0 ~ 25
c -='a';
//如果当前位置为空,则在当前位置创建一个空节点
if(cur.son[c] == null){
cur.son[c] = new Node();
}
//将插进去的这个节点当作当前节点
cur = cur.son[c];
}
//一个单词插入完成,将最后节点标记为true,说明这个单词结束了
cur.endFlag = true;
}
public boolean search(String word) {
return find(word)==2;
}
public boolean startsWith(String prefix) {
return find(prefix)!=0;
}
private int find(String word){
Node cur = root;
//遍历字符串的每个字母
for(char c : word.toCharArray()){
// 将 'a' ~ 'z' 映射到 0 ~ 25
c -='a';
//如果当前位置为空,说明没有这个字母起始的单词,即找不到这个单词
if(cur.son[c] == null){
return 0;
}
cur = cur.son[c];
}
//尾节点是否是结束点?是返回2,找到了单词。不是返回1,为前缀
return cur.endFlag?2:1;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
代码逻辑:二叉树
根据该前缀树的描述,我们可以将这个前缀树看作一个类似于二叉树的二十六叉树。根节点可以有二十六个子节点,分别表示二十六个字母。
10. 回溯
10.1 全排列
class Solution {
//创建所有排列结果集
List<List<Integer>> res = new ArrayList<>();
//创建每一个排列
List<Integer> path = new ArrayList<>();
//创建used变量,用于标志当前数是否已经被使用过
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
//判空
if(nums.length == 0) return res;
//初始化used的长度为输入数组的长度,并且java会给每一个boolean类型赋默认值false
used = new boolean[nums.length];
//调用递归函数
helper(nums);
return res;
}
private void helper(int[] nums){
//当path的长度等于nums的长度时,说明一个排列已经形成,将这个排列加到结果集中
if(path.size() == nums.length){
//这里必须创建一个新的ArrayList是因为path是一个变量,如果直接加进去,后续path产生变化结果集中的值也会发生变化。因此这里相当于拷贝了一个path放入结果集,保证结果集中的path不会发生变化。
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
//如果当前位置的值被用过了,则跳出这次循环,继续下次循环
if(used[i]){
continue;
}
//将当前位置的标志设为true,表示当前值用过了
used[i] = true;
//将当前位置的值加入到path中
path.add(nums[i]);
//继续递归得到下一个值
helper(nums);
//回溯,将最后一个值去掉
path.removeLast();
//回溯,值被去掉了相应位置使用过的标志也应该返回false
used[i] = false;
}
}
}
代码逻辑:递归
一个一个位置进行排列,一个排列完成后回溯相应的次数再次排列。代码随想录中的这张图比较好理解:
10.2 子集
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backtracing(nums,0);
return res;
}
private void backtracing(int[] nums, int startIndex){
res.add(new ArrayList<>(path));
for(int i= startIndex;i<nums.length;i++){
path.add(nums[i]);
backtracing(nums,i+1);
path.removeLast();
}
}
}
代码逻辑:递归
这题与全排列不同点在于全排列是有序的,即[1,2,3]和[3,2,1]是不同的;而子集里是无序的,即[1,2,3]和[3,2,1]是一样的只用记录一个。因此这题里引入startIndex变量,用于记录当前值用过了,下次循环就值循环这个数后面的数。
根据10.1和10.2我们可以总结出一个回溯算法的模板题解:
回溯三部曲:递归函数参数、递归终止条件、确定单层遍历逻辑
void backtracing(递归参数){
if(递归终止条件){
加入结果集;
return;
}
for(选择本层集合中的元素){
处理节点:
backtracing(路径,选择列表); //递归
回溯,撤销处理结果
}
}
10.3 电话号码的字母组合
代码逻辑:
1. 确定回溯三部曲。
递归函数的参数:结果集res、字符串s、index表示遍历到第几个数了
递归终止条件:两个数都遍历完了就将s加入结果集,即index==digits.size()
单层遍历逻辑:取index指向的数,找到对应的字符集(如abc),for循环处理字符,例如处理a后,开始取index+1指向的数,处理def,然后回溯
class Solution {
List<String> res = new ArrayList<>();
StringBuilder s = new StringBuilder();;
String[] numString = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) return res;
backtracing(digits,0);
return res;
}
private void backtracing(String digits, int index){
if(index == digits.length()){
res.add(s.toString());
return;
}
//当前数字对应的字符串
String str = numString[digits.charAt(index)-'0'];
//遍历当前数字对应的字符串中每个字符
for(int i=0;i<str.length();i++){
s.append(str.charAt(i));
backtracing(digits,index+1);
s.deleteCharAt(s.length()-1);
}
}
}
10.4 组合总和
代码逻辑:
回溯三部曲:
递归函数参数:candidates、target、结果集res、path、总和sum、startIndex因为得到的组合是无序的所以需要这个参数
递归终止条件:sum>=target; 其中sun>target时直接return不收集结果。等于时收集结果并return。
单层遍历逻辑:每一层都可以取candidates中的n个数。一个一个遍历,加入path并计算已加入的数的总和。
根据回溯三部曲写完代码后发现当sum>target时,我们只是不收集结果但代码还会继续运行继续往后加。因此考虑优化代码,把candidates序列先排序成一个有序的序列,当sum>target时直接结束循环不往后加了,因为后面的数会越来越大肯定都不满足条件。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
backtracing(candidates,target,sum,0);
return res;
}
private void backtracing(int[] candidates, int target,int sum, int startIndex){
if(sum == target) {
res.add(new ArrayList<>(path));
return;
}
for(int i = startIndex; i<candidates.length;i++){
if(sum+candidates[i] >target) break;
path.add(candidates[i]);
sum +=candidates[i];
backtracing(candidates, target,sum, i);
path.removeLast();
sum -=candidates[i];
}
}
}
10.4 括号生成
10.4.1 DFS做法(非回溯)
代码逻辑:DFS
1. 左括号和右括号数量是一定的,那么我们考虑从括号总量中拿出一个括号,判断剩余没用过的左右括号数量。
2. 不难看出在这种方式下,当左括号数量大于0时,是可以取左括号的;右括号数量大于零也是可以取有括号的,但是有个前提,右括号数量是受左括号数量制约的,当左括号数量小于右括号的时候才可以取右括号,这样才符合提议。
3. 递归终止的条件是左右括号数量都为0。
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
//判空
if(n==0) return res;
dfs("",n,n);
return res;
}
private void dfs(String curStr, int left, int right){
//递归终止条件
if(left == 0 && right ==0){
res.add(curStr);
return;
}
//如果左括号数量大于右括号数量,则无法生成有效括号。这一步也称为剪枝
if(left>right){
return;
}
if(left>0){
//这里不能使用 curStr = curStr + "("; 或者curStr += "(";
//因为java中String类型是不可变的,可变的是StringBuilder
dfs(curStr + "(",left-1,right);
}
if(right>0){
dfs(curStr + ")",left,right-1);
}
}
}
10.4.2 回溯算法
代码逻辑:DFS(回溯)
根据回溯三部曲:
递归参数:res结果集、path每一种可能、left左括号数量、right右括号数量、startIndex
递归终止条件:左右括号数量为0
class Solution {
//结果集
List<String> res = new ArrayList<>();
StringBuilder str = new StringBuilder();
public List<String> generateParenthesis(int n) {
//判空
if(n==0) return res;
backtracing(str,n,n);
return res;
}
private void backtracing(StringBuilder str, int left, int right){
//递归终止条件
if(left == 0 && right ==0){
res.add(str.toString());
return;
}
//剪枝
if(left>right) return;
if(left>0){
str.append("(");
backtracing(str,left-1,right);
str.deleteCharAt(str.length()-1);
}
if(right>0){
str.append(")");
backtracing(str,left,right-1);
str.deleteCharAt(str.length()-1);
}
}
}
注意: 上面两种方法本质上是一种,都是dfs深度遍历的逻辑。至于为什么这一题不用回溯也可以完成是因为字符串的特性。
Java 和 Python 里 +
生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。
可以想象搜索遍历的问题其实就像是做实验,每一次实验都用新的实验材料,那么做完了就废弃了。但是如果只使用一份材料,在做完一次以后,一定需要将它恢复成原样(就是这里「回溯」的意思),才可以做下一次尝试。
这里使用了StringBuilder,
全程只使用一份变量去搜索的做法。
10.5 单词搜索
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(backtracing(board,words,i,j,0)){
return true;
}
}
}
return false;
}
//k表示单词中当前遍历的字母
private boolean backtracing(char[][] board, char[] word, int i, int j, int k){
if(i<0 || i>=board.length || j<0 || j>=board[0].length){
return false;
}
//剪枝,如果当前格子里的字母不是word[k],说明无论如何从这个格子出发也不会找到目标单词
if(board[i][j] != word[k]){
return false;
}
//递归终止条件
if(k == word.length-1){
return true;
}
//标记当前单元格已经访问过,避免重复访问
board[i][j] = '\0';
boolean res = backtracing(board,word,i-1,j,k+1)|| //遍历上
backtracing(board,word,i+1,j,k+1)|| //遍历下
backtracing(board,word,i,j-1,k+1)|| //遍历左
backtracing(board,word,i,j+1,k+1); //遍历右
//回溯
board[i][j] = word[k];
return res;
}
}
10.6 分割回文串
代码逻辑:根据树形图比较好理解,本质上是一个组合的问题,因此考虑引入startIndex来解决。依旧是根据回溯三部曲来构建代码结构即可。
class Solution {
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
String s;
public List<List<String>> partition(String s) {
this.s = s;
dfs(0);
return res;
}
private void dfs(int startIndex){
//递归终止条件
if(startIndex == s.length()){
res.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<s.length();i++){
//注意startIndex和i那个为左那个为右
if(isPalindrome(startIndex,i)){
//注意s.substring(startIndex,i+1)实际表示的是startIndex到i的字符
path.add(s.substring(startIndex,i+1));
dfs(i+1);
//回溯
path.remove(path.size()-1);
}
}
}
//判断当前字符串是否是回文字符串
private boolean isPalindrome(int left,int right){
while(left<right){
if(s.charAt(left++) != s.charAt(right--)){
return false;
}
}
return true;
}
}
10.7 N皇后
class Solution {
List<List<String>> ans = new ArrayList<>();
int n;
public List<List<String>> solveNQueens(int n) {
this.n = n;
int[] queens = new int[n];
boolean[] col = new boolean[n];
boolean[] diag1 = new boolean[2*n-1];
boolean[] diag2 = new boolean[2*n-1];
dfs(0,queens,col,diag1,diag2);
return ans;
}
private void dfs(int r, int[] queens,boolean[] col, boolean[] diag1, boolean[] diag2){
//递归终止条件
if(r==n){
List<String> board = new ArrayList<>();
for(int c:queens){
char[] row = new char[n];
Arrays.fill(row,'.');
row[c] = 'Q';
board.add(new String(row));
}
ans.add(board);
}
for(int c = 0;c<n;c++){
if(!col[c] && !diag1[r+c] && !diag2[r-c+n-1]){
queens[r] = c;
col[c] = diag1[r+c] =diag2[r-c+n-1] = true;
//递归
dfs(r+1,queens,col,diag1,diag2);
//回溯
col[c] = diag1[r+c] =diag2[r-c+n-1] = false;
}
}
}
}
11. 二分查找
11.1 搜索插入位置
代码逻辑:
首先找到这个数组的中点,因为数组是有序的,因此只需要判断目标值是在中心点的左边还是右边,如果在左边则将搜索的右边界移至中点前一个数,如果在右边则将搜索的左边界移至中点后一个数,通过这样不断地缩短范围最终可以找到目标数值。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while(left<=right){
int mid = (left+right)/2;
if(target>nums[mid]){
left = mid+1;
}else{
right = mid-1;
}
}
return left;
}
}
11.2 搜索二维矩阵
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int row = searchFirstCol(matrix,target);
if(row<0){
return false;
}
return searchrow(matrix,target,row);
}
private int searchFirstCol(int[][] matrix, int target){
int low = -1;
int high = matrix.length-1;
while(low<high){
int mid = (high-low+1)/2+low;
if(matrix[mid][0]<=target){
low = mid;
}else{
high = mid-1;
}
}
return low;
}
private boolean searchrow(int[][] matrix, int target, int row){
int low = 0;
int high = matrix[0].length-1;
while(low<=high){
int mid = (high-low)/2+low;
if(matrix[row][mid]<target){
low = mid+1;
}else if(matrix[row][mid]==target){
return true;
}else{
high = mid-1;
}
}
return false;
}
}
暂时断更,因为发现前面做过的题不复习已经忘得差不多了,目前正在从头开始巩固。