LeetCode Hot100刷题笔记·Java版(4)

本文接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;
    }
}

暂时断更,因为发现前面做过的题不复习已经忘得差不多了,目前正在从头开始巩固。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值