LeetCode刷题笔记(Java)---第61-80题

笔记导航

点击链接可跳转到所有刷题笔记的导航链接

61. 旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public static ListNode rotateRight(ListNode head, int k) {
        //特殊情况,直接返回head
        if (head == null || head.next == null || k == 0) return head;
        int number = 1;
        ListNode p = head;
        ListNode tail = null;
        //首先计算链表中有多少个节点
        while (p.next != null) {
            p = p.next;
            number++;
        }
        tail = p;//找到最后一个节点。
        if (k % number == 0)//若成立,说明不需要旋转。直接返回head。
            return head;
        //右移动k%number的结果和右移k的结果是一样的。
        //并计算除去右移k个的结果其他的节点的个数。
        k = number - k % number;
        p = head;
        number = 1;
        ListNode pre = null;
        //找到第k个节点。
        while (p.next != null && number != k) {
            p = p.next;
            number++;
            if (number == k) {
                pre = p;//pre是第k个节点
                p = p.next;//p是第k+1个节点。
                break;
            }
        }
        // pre不为空,则将第k个之后的链表挪到前面,前k个链表挪到后面。
        if (pre != null) {
            pre.next = null;
            tail.next = head;
            head = p;
        } 
        // 若pre为空,则说明第2个元素开始之后的节点挪到前面,第一个元素挪到后面。
        else {
            tail.next = head;
            head = head.next;
            tail.next.next = null;
        }
        //返回head
        return head;
    }
  • 分析

    1.首先k可能会大于链表的长度。所以先计算出链表的长度。右移 (k%链表的长度) 的结果 和 右移 k 的结果是一样的。
    看示例2 的第一步和第四步 结果是一样的。
    4%3 = 1
    2.链表右移k个,相当于从(k%链表的长度)的位置截断,(k%链表的长度)之后的链表和(k%链表的长度)之前的链表交换位置。例如 12345 k=3。相当于 123 和 45 交换位置。
    3.链表的交换位置只需要修改节点的指针即可。所以重点是找到3个指针,第一个是原始链表的最后一个节点,用指针tail表示。第二个是第(k%链表的长度)个节点和第(k%链表的长度+1)个节点。用于分割和交换前后链表。

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?
在这里插入图片描述

例如,上图是一个7 x 3 的网格。有多少可能的路径?

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public static int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        dp[0][0] = 1;
        //动态规划实现
        for (int i = 0; i < m ; i++) {
            for (int j = 0; j < n ; j++) {
                if (j == 0) dp[i][j] = 1;//第一行全为1,只有一种方式到达
                if (i == 0) dp[i][j] = 1;//第一列全为1,只有一种方式到达
                //其余位置可能的路径和该位置的上面和左边的位置有关系。
                else if (i != 0 && j!=0){
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];//动态转移方程。
                }
            }
        }
        return dp[m - 1][n - 1];
    }
  • 分析

    1.这一位置的状态和之前位置的状态有关,可以使用动态规划实现。
    2.因为只能向右边或者向下走,所以第一行的位置只有一种从起始点出发的路径。同理第一列的位置也只有一条路径可以到达。
    3.除了第一行和第一列。其余位置可达路径与其上方的位置和左方的位置有关。
    例如位置(2,3),它与位置(1,3)和位置(2,2)有关到达(1,3)的路径数量加上到达(2,2)的位置数量就等于从原点到达(2,3)的路径的数量。
    即可列出动态转移方程:
    dp[i][j] = dp[i-1][j] + dp[i][j-1]。(i>0,j>0)

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
在这里插入图片描述
网格中的障碍物和空位置分别用 1 和 0 来表示。

说明:m 和 n 的值均不超过 100。

示例 1:
在这里插入图片描述

  • 解答
    public static int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int row = obstacleGrid.length;//行数
        int column = obstacleGrid[0].length;//列数
        int[][] dp = new int[row][column];//dp数组
        if (obstacleGrid[0][0] != 1) dp[0][0] = 1;
        else return 0;
        //第一行的位置可达的路径最多只有一条
        for (int i = 1; i < column; i++) {
            //若遇到障碍,则障碍之后的位置不可达。
            if (obstacleGrid[0][i] == 1)
                break;
            dp[0][i] = 1;
        }
        //第一列的位置可达的路径最多只有一条
        for (int i = 1; i < row; i++) {
            //若遇到障碍,则障碍下方的位置不可达
            if (obstacleGrid[i][0] == 1)
                break;
            dp[i][0] = 1;
        }
        //其他的位置的路径和其上方的位置和左方的位置有关
        for (int i = 1; i < row; i++) {
            for (int j = 1; j < column; j++) {
                //若此处没障碍,那么根据动态方程得出路径数量。
                if(obstacleGrid[i][j] != 1){
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }
        return dp[row-1][column-1];
    }
  • 分析

    1.此题和上一题相比,仅多了一个障碍物。
    2.还是分成3类,第一行,第一列和其他位置。
    因为只能向下或者向右走,所以第一行的位置可达的路径最多只有一条。当遇到障碍,则障碍之后的位置都不可达。
    同理第一列的位置可达的路径最多只有一条。当遇到障碍,则障碍下方的位置都不可达。
    其余位置,会根据和上一题一样的动态转移方程。再加上判断此处有无障碍,来更新dp数组。
    最后返回数组右下角的值即为答案。

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:
在这里插入图片描述

  • 解答
    public int minPathSum(int[][] grid) {
        int[][] dp = new int[grid.length][grid[0].length];
        dp[0][0] = grid[0][0];
        //第一行每个位置的路径长度
        for (int i = 1; i < grid.length; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        //第一列每一个位置的路径长度
        for (int i = 1; i < grid[0].length; i++) {
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        }
        //其余位置的路径长度
        for (int i = 1; i < grid.length; i++) {
            for (int j = 1; j < grid[0].length; j++) {
                dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];//动态方程
            }
        }
        return dp[grid.length-1][grid[0].length-1];
    }
  • 分析

    1.因为每次只能向下或者向右走。所以第一行的每个位置的路径长度等于该位置的权值加上前一个位置的路径长度。
    即dp[i][0] = dp[i - 1][0] + grid[i][0];
    2.同理第一列的每一个位置的路径长度等于该位置的权值加上上一个位置的路径长度。
    即dp[0][i] = dp[0][i - 1] + grid[0][i];
    3.其余的位置的路径长度和自身位置的权值,以及和上方位置和左方位置的路径长度有关。
    即可列出动态方程:
    dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j]。
    选择上方和左方位置的路径最小的一个加上其位置的权值。即为该位置的路径长度。

65. 有效数字

验证给定的字符串是否可以解释为十进制数字。

例如:
在这里插入图片描述
说明: 我们有意将问题陈述地比较模糊。在实现代码之前,你应当事先思考所有可能的情况。这里给出一份可能存在于有效十进制数字中的字符列表:

  • 数字 0-9
  • 指数 - “e”
  • 正/负号 - “+”/"-"
  • 小数点 - “.”
    当然,在输入中,这些字符的上下文也很重要。
  • 解答
    public static boolean isNumber(String s) {
        s = s.trim();//去掉收尾括号
        if (s.length() == 0 || s.charAt(s.length() - 1) == 'e' || s.charAt(0) == 'e' || (s.length() == 1 && s.charAt(0) == '.'))//若s为空或者第一位为e或者最后一位为e或者当长度为一时仅有.则返回false
            return false;
        String[] strings = s.split("e");//根据科学记数法,将e的前后分开
        if (strings.length > 2)//若大于2,说明愿数字中有两个e不符合科学记数法规则。返回false
            return false;
        //将e左边的转成数组。
        char[] charsLeft = strings[0].toCharArray();
        //若其长度为1,且不是数字则返回false。
        if (charsLeft.length == 1 && (charsLeft[0] < 48 || charsLeft[0] > 57)) return false;
        char[] charsRight = null;
        //若存在e的右边,则将其记录下俩。
        if (strings.length == 2) {
            charsRight = strings[1].toCharArray();
            //同理若长度为1,且不是数字则返回false。
            if (charsRight.length == 1 && (charsRight[0] < 48 || charsRight[0] > 57)) return false;
        }
        int number = 0;//用于记录左侧出现小数点的个数。
        for (int i = 0; i < charsLeft.length; i++) {
        //第一个数字
            if (i == 0) {
                //若为小数点 计数+1
                if (charsLeft[i] == 46) {
                    number++;
                } 
                //若不是数字或正负号,则返回false。
                else if ((charsLeft[i] < 48 || charsLeft[i] > 57) && !(charsLeft[0] == 43 || charsLeft[0] == 45))
                    return false;
            } 
            //其余位,若不是数字
            else if (charsLeft[i] < 48 || charsLeft[i] > 57) {
                //若是小数点,计数+1
                if (charsLeft[i] == 46) {
                    number++;
                    //出现两个小数点,或小数点前不是数字则返回false。
                    if (number > 1 || (i + 1 == charsLeft.length && (charsLeft[i - 1] == 43 || charsLeft[i - 1] == 45)))
                        return false;
                } 
                //不是小数点也不是数字,返回false
                else return false;
            }
        }
        //若存在e的字母右侧部分。
        if (charsRight != null)
            for (int i = 0; i < charsRight.length; i++) {           
                // 若第一位是正负号,跳过
                if (i==0 && charsRight[i] == 43 || charsRight[i] == 45)
                    continue;
                //其余位置不是数字则返回false。
                if (charsRight[i] < 48 || charsRight[i] > 57)
                    return false;
            }
        // 上面判断都通过则返回true。
        return true;
    }
  • 分析

    1.一开始观察例子,可以发现如用科学记数法,那就会用e将一个字符串分割开来。然后分别判断前面和后面两个字符串即可。
    2.这是一道面向测试用例编程的题,每次提交根据结果修改代码就好了。

66. 加一

给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public int[] plusOne(int[] digits) {
        //从后往前遍历数组。
        for (int i = digits.length - 1; i >= 0; i--) {         
            //若当前位置数字+1等于10,就要进位,此位置变为0.
            if (digits[i] + 1 == 10 && i >= 0) {
                digits[i] = 0;
            }
            //否则,此位置数字+1,结束遍历。因为没有进位了。
            else {
                digits[i] += 1;
                break;
            }
        }
        //若第一位为0,则说明原先最高位为9,那么就要新数组,来得到新的数字。
        if(digits[0] == 0){
            int[] newDigits = new int[digits.length+1];
            newDigits[0] = 1;
            //最高位为1,其余位与digits数字中一样。
            for (int i = 1; i < digits.length+1; i++) {
                newDigits[i] = digits[i-1];
            }
            //返回新的数组。
            return newDigits;
        }
        //否则直接返回digits。
        else return digits;
    }
  • 分析

    1.数字+1 逢10进1。所以第一层for循环,从个位开始,若没有进位了则直接结束遍历即可。
    2.关注点在于最高位,因为若最高为9,并且有进位。那么原来的数组就无法表示这个新的数字,会越界。所以需要一个长度+1的新数组来表示这个新的数字。

67. 二进制求和

给定两个二进制字符串,返回他们的和(用二进制表示)。

输入为非空字符串且只包含数字 1 和 0。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public String addBinary(String a, String b) {
        StringBuilder res = new StringBuilder();
        int length1 = a.length();
        int length2 = b.length();
        int carry = 0;
        //两个字符串相同位都存在,则低位相加
        while (length1 > 0 && length2 > 0) {
            //相同位相加的结果。
            int number = Integer.parseInt("" + a.charAt(length1 - 1)) + Integer.parseInt("" + b.charAt(length2 - 1));
            //若前一次无进位
            if(carry ==0){
                if(number == 2){
                    res.insert(0, "0");
                    carry = 1;
                }else {
                    res.insert(0, number);
                }
            }
            //若前一次有进位
            else {
                if(number == 1) res.insert(0, "0");
                else if(number == 2) res.insert(0, "1");
                else {
                    res.insert(0, "1");
                    carry = 0;
                }
            }
            length1--;
            length2--;
        }
        //还有一个字符串有余下的位
        while (length1 > 0 || length2 > 0) {
            int number;
            if(length1>0)
                number = Integer.parseInt("" + a.charAt(length1 - 1)) + carry;
            else number = Integer.parseInt("" + b.charAt(length2 - 1)) + carry;
            if (number == 2) {
                res.insert(0, "0");
                carry = 1;
            } else {
                res.insert(0, number);
                carry = 0;
            }
            length1--;
            length2--;
        }
        //最后若还有进位,则高位补1
        if (carry == 1) {
            res.insert(0, "1");
        }
        return res.toString();
    }
  • 分析

    1.两个二进制字符串相加,则从低位开始相加,逢2进1。
    2.若不等长的话,长的那一个字符串余下的位置根据进位情况补在高位。
    3.最后若还存在进位,则在高位补1。

68. 文本左右对齐

给定一个单词数组和一个长度 maxWidth,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。

你应该使用“贪心算法”来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ’ ’ 填充,使得每行恰好有 maxWidth 个字符。

要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。

文本的最后一行应为左对齐,且单词之间不插入额外的空格。

说明:
在这里插入图片描述
示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述
示例 3:
在这里插入图片描述

  • 解答
public static List<String> fullJustify(String[] words, int maxWidth) {
        List<String> res = new ArrayList<>();
        //特殊情况,字符串数组中只有一个。返回字符串并末尾补齐空格到指定最大长度maxWidth。
        if(words.length==1){
            for (int i = 0; i < maxWidth-words[0].length(); i++) {
                words[0] += " ";
            }
            res.add(words[0]);
            return res;
        }
        int length = 0;//用于记录当前字符串组合的长度
        int start = 0;//用于记录当前字符串组合开始的位置
        int end = 0;//用于记录当前字符串组合结束的位置。
        for (int i = 0; i < words.length; i++) {
            //若当前记录的字符串组合是小于最大长度的最长的组合。
            if (length + words[i].length() + i - start> maxWidth) {
                end = i - 1;
                //若组合中不止一个字符串,则调用combinationString,得到一个拼接后的字符串添加到答案中。
                if (end - start >= 1) {
                    res.add(combinationString(words, start, end, length, maxWidth));
                } 
                //若只有一个字符串,则将字符串并末尾补齐空格到指定最大长度maxWidth。添加到答案中。
                else {
                    for (int j = 0; j < maxWidth - length; j++) {
                        words[end] += " ";
                    }
                    res.add(words[end]);
                }
                start = i;//更新字符串组合的起始位置
                end = i;//更新字符串组合的结束位置
                length = words[i].length();//更新字符串组合的长度

            } 
            //若新的字符串加入字符串组合后,还是小于最大宽度。
            else {
                length += words[i].length();//更新字符串组合的长度
                end++;//更新字符串组合的结束位置。
            }
        }
        //遍历结束后,若发现length不等于0,则说明还有字符串没有加入组合中。
        if (length != 0) {
            //若存在end大于start
            if (end > start) {
                StringBuilder s = new StringBuilder();
                //最后一行向左靠齐,字符串之间间隔一个空格
                for (int i = start; i <= end; i++) {
                    if(i!=end)
                        s.append(words[i] + " ");
                    else s.append(words[i]);
                }
                //末尾补齐空格到最大宽度。
                for (int i = 0; i < maxWidth - length - end + start; i++) {
                    s.append(" ");
                }
                //加入答案中。
                res.add(s.toString());
            } 
            //若仅有一个字符串。
            else {
                //末尾补齐空格到最大宽度
                for (int i = 0; i < maxWidth - length; i++) {
                    words[end] += " ";
                }
                //加入答案中。
                res.add(words[end]);
            }

        }
        return res;
    }

    public static String combinationString(String[] words, int start, int end, int length, int maxWidth) {
        StringBuilder res = new StringBuilder();
        int blank = maxWidth - length;//需要填充的空格数量
        int eachBlank = blank / (end - start);//平均每个单词之间需要添加的空格
        int remainingBlank = blank - eachBlank * (end - start);//无法平均的情况,剩余的空格数量
        for (int i = start; i <= end; i++) {
            //加入第一个字符串
            if (i == start) {
                res.append(words[i]);
            } 
            //添加需要填充的空格
            else {
                for (int j = 0; j < eachBlank; j++) {
                    res.append(" ");
                }
                //若有剩余空格,则补一个,总剩余空格数减一。
                if (remainingBlank != 0) {
                    res.append(" ");
                    remainingBlank--;
                }
                res.append(words[i]);
            }
        }
        return res.toString();
    }
  • 分析

    1.遍历字符串数组,找寻小于最大长度的字符串组合。
    2.找寻字符串组合的时候,要计算上最少的空格。比如2个字符串之间需要一个空格。不然会出现字符串连接在一起的情况。
    3.根据找到的字符串组合。判断其字符串长度和最大长度的差,可以得到需要填充的空格数量。
    4.空格数量先平分,若剩余不可平均分配的空格按照从左向右的顺序填在字符串之间。
    例如剩余空格2.平均空格分配1,字符串avc,asd,ewq,dasd。
    那么等到的字符串组合就是avc+2个空格+asd+2个空格+ewq+1个空格+dasd。
    5.最后一行的靠左对齐。字符串之间一个空格,末尾空格补齐长度。

69. x 的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public static int mySqrt(int x) {
        if (x <= 1) return x;
        long r = x;
        while (r > x / r) {
            r = (r + x / r) / 2;
        }
        return (int)r;
    }
  • 分析

    1.利用牛顿迭代法
    在这里插入图片描述
    更新平方根

    2.用long 因为第一次更新可能会越界超过int的范围
    r + x / r会超出int的上限。

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public int climbStairs(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1;
        if (n == 1) return dp[0];
        dp[1] = 1;
        for (int i = 2; i < n+1; i++) {
            dp[i] =dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
  • 分析:

    1.观察前4个数:
    一节台阶,一种方法
    二节台阶,两种方法
    三节台阶,三种方法
    四节台阶,五种方法
    1,2,3,5
    前面再加一个1,得1,1,2,3,5
    满足斐波那契数列
    即dp[i] =dp[i-1]+dp[i-2];
    2.所以根据斐波数列的规则,可以直接得出答案。

71. 简化路径

以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。

在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (…) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。更多信息请参阅:Linux / Unix中的绝对路径 vs 相对路径

请注意,返回的规范路径必须始终以斜杠 / 开头,并且两个目录名之间必须只有一个斜杠 /。最后一个目录名(如果存在)不能以 / 结尾。此外,规范路径必须是表示绝对路径的最短字符串。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述
示例 3:
在这里插入图片描述
示例 4:
在这里插入图片描述
示例 5:
在这里插入图片描述
示例 6:
在这里插入图片描述

  • 解答
    public static String simplifyPath(String path) {
        StringBuilder res = new StringBuilder();
        String[] strings = path.split("\\/");//以“/”划分字符串。
        Stack<String> stack = new Stack<>();//利用栈
        //遍历划分后的字符串数组。
        for (int i = 0; i < strings.length; i++) {
           //若当前栈不为空,且字符串是“.."则出栈,表示返回上一目录
           if(strings[i].equals("..")&&!stack.isEmpty())stack.pop();
           //若当前字符串不等于“”或“..”或“."则进栈,表示进入目录
            else if(!strings[i].equals("")&&!strings[i].equals("..")&&!strings[i].equals("."))stack.push(strings[i]);
        }
        //栈内元素出栈顺序取反,即可得到完整路径。取反操作,就是每次出栈的字符串添加在当前得到的路径前面
        while (!stack.isEmpty()){
            res.insert(0,"/"+stack.pop());
        }
        //若当前路径为空,则返回“/”
        if(res.length()==0)res.append("/");
        //否则返回得到的路径
        return res.toString();
    }
  • 分析

    1.将字符串按照“/”进行分割。可以得到各个目录级别的操作。
    2.其中有2个特殊的字符串。

      * “..”表示返回上一目录,则说明和前面一个进入某个目录的操作抵消。
      例如/a/../
      按照"/"划分可以得到“”,"a",".."。空字符串不用管它,“a"表示进入目录a,".."表示抵消上一步操作,即返回到a的父目录。
          
      * “."表示当前目录,则说明不用任何操作,忽略它。
    

    3.根据上面的规律,发现用栈可以很容易的实现。
    进栈表示进入某个目录,出栈表示返回到上一目录。
    4.遍历划分后的字符串数组,遇到字母组成的字符串表示进栈操作。
    遇到“…“当前栈顶出栈。
    5.最后即可得到规范路径。
    6.注:以“/”进行划分字符串需要这样表示”\/”。

72. 编辑距离

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public int minDistance(String word1, String word2) {
        if (word1.length() == 0 || word2.length() == 0) return word1.length()+word2.length();//若有一个字符串为空,则返回word1.length()+word2.length()
        int m = word1.length();
        int n = word2.length();
        //dp数组用来记录word1的第i个字符之前和word2第j个字符之前的最少操作次数。
        int[][] dp = new int[m+1][n+1];
        //word2无字符,dp[i][0]表示将word1的第i个字符之前的字符串修改为空字符串的次数
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i;
        }
        // word1无字符,dp[0][j]表示将空字符串修改为word2的第j个字符之前的字符串,需要修改的次数
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j;
        }
        // dp[i][j]表示word1前i个字符的字符串,修改为word2前j个字符的字符串需要修改的次数。
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 若word1的第i个字符和word2的第j个字符相同
                // 则dp[i][j]=dp[i-1][j-1];
                if(word1.charAt(i-1)==word2.charAt(j-1))
                    dp[i][j]=dp[i-1][j-1];
                // 若不相等,则根据三种情况,选择修改次数最少的一种。在此基础上次数加1.
                else dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1]);
            }
        }
        return dp[m][n];
    }
  • 分析

    1.将复杂问题拆成简单的问题,在解决子问题的基础上扩展子问题。最后得到复杂问题的解。
    2.题目要求:得到将字符串word1修改为word2需要的最少修改次数。
    那么可以把问题简化为求解word1的第i个字符前的字符串修改为word2的第j个字符前的字符串,需要修改的次数。

    首先可以先求的,从空字符串修改为word2的第j个字符前的字符串,需要修改的次数。即dp[0][j] = j;
    同理,从word1的前i个字符串修改为空字符串需要的修改次数。
    即[i][0] = i;

    接着就是判断word1的第i个字符前的字符串修改为word2的第j个字符前的字符串,需要修改的次数。
    与dp[i][j]有关联的,就是dp[i-1][j-1],dp[i][j-1],dp[i-1][j]三种。
    若word1的第i个字符和word2的第j个字符相同。那么次数需要修改的次数和dp[i-1][j-1]相同。因为两个字符相同,这里不需要修改。
    即 dp[i][j] = dp[i-1][j-1]
    若不相同,则需要修改这一位。那此时的修改次数是多少呢?
    因为选择的是最少修改次数,所以选择dp[i-1][j],dp[i][j],dp[i][j-1]中最小的一个。在最小的那一个的基础上修改word1的第i位,即次数加1.就可以得到dp[i][j]。
    即 dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1])

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述
进阶:

  • 一个直接的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。

  • 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。

  • 你能想出一个常数空间的解决方案吗?

  • 解答

    //方法1 空间复杂度O(m+n)
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int[] row = new int[m];//用于记录某一行是否有0
        int[] column = new int[n];//用于记录某一列是否有0
        //遍历matrix,找到0的地方,记录下坐标。
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if(matrix[i][j] == 0 ){
                    row[i] = 1;
                    column[j] = 1;
                }
            }
        }
        //遍历row数组,记录为1的对应的行数,修改matrix矩阵这一行为0
        for (int i = 0; i < m; i++) {
            if(row[i] == 1){
                for (int j = 0; j < n; j++) {
                    matrix[i][j] = 0;
                }
            }
        }
        //遍历column数组,记录为1的对应的列,修改matrix矩阵这一列为0.
        for (int i = 0; i < n; i++) {
            if(column[i] == 1){
                for (int j = 0; j < m; j++) {
                    matrix[j][i] = 0;
                }
            }
        }
    }
    //方法2,空间复杂度仅O(2)
    public void setZeroes2(int[][] matrix) {
        boolean rowFlag = false;
        //判断首行是否有0
        for (int i = 0; i < matrix[0].length; i++) {
            if (matrix[0][i] == 0) {
                rowFlag = true;
                break;
            }
        }
        
        boolean colFlag = false;
        //判断首列是否有0
        for (int i = 0; i < matrix.length; i++) {
            if (matrix[i][0] == 0) {
                colFlag = true;
                break;
            }
        }
        //遍历matrix数组,若有0,则将对应的第一行相同列的位置修改为0
        //第一列相同行的位置修改为0
        for (int i = 1; i < matrix.length; i++) {
            for (int j = 1; j < matrix[0].length; j++) {
                if (matrix[i][j] == 0){
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                }
            }
        }
        
        //遍历matrix第一行
        for (int i = 1; i < matrix[0].length; i++) {
            //若有0,则将对应的列修改为0
            if (matrix[0][i] == 0) {
                for (int j = 0; j < matrix.length; j++) {
                    matrix[j][i] = 0;
                }
            }
        }
        
        //遍历matrix第一列
        for (int i = 1; i < matrix.length; i++) {
            //若有0,则将对应的行修改为0
            if (matrix[i][0] == 0) {
                for (int j = 0; j < matrix[0].length; j++) {
                    matrix[i][j] = 0;
                }
            }
        }
        //若第一行一开始就有0,则将第一行全部置0
        if (rowFlag){
            for (int i = 0; i < matrix[0].length; i++) {
                matrix[0][i] = 0;
            }
        }
        //若第一列一开始就有0,则将第一行全部置0
        if (colFlag){
            for (int i = 0; i < matrix.length; i++) {
                matrix[i][0] = 0;
            }
        }
    }
  • 分析

    1.方法一就是利用两个额外的数组,来记录某一行或某一列是否有0存在
    2.方法二首先判断第一行或第一列是否有0,并记录下来。
    然后利用原始的第一行和第一列来记录某一行和某一列是否有0存在。起始就是相当于方法一,只是不需要额外的两个数组的空间。
    遍历完数组后,就可以得到修改后的第一行和第一列。根据以此来判断某行某列是否要全部修改为0.
    最后再判断根据一开始记录的两个标志,来判断是否修改第一行和第一列。

  • 提交结果

方法1:
在这里插入图片描述

方法2:
在这里插入图片描述

74. 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

  • 每行中的整数从左到右按升序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述

  • 解答
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) return false;//若矩阵为空返回false
        int m = matrix.length;
        int n = matrix[0].length;
        int left = 0;
        int right = m - 1;
        int mid = 0;
        //根据第一列进行二分查找,判断target可能在的行数
        while (left <= right) {
            mid = (left + right) / 2;
            if (matrix[mid][0] > target)
                right = mid - 1;
            else if (matrix[mid][0] < target)
                left = mid + 1;
            else return true;//若找到则返回true
        }
        //得到target可能在的行数
        int row = target > matrix[mid][0] ? mid : mid - 1;
        if (row < 0) return false;
        left = 0;
        right = n - 1;
        //在row这一行进行二分查找
        while (left <= right) {
            mid = (left + right) / 2;
            if (matrix[row][mid] > target)
                right = mid - 1;
            else if (matrix[row][mid] < target)
                left = mid + 1;
            else return true;
        }
        return false;
    }
  • 分析

    1.因为给定的矩阵是有序的,每一行的第一个数字大于上一行最后一个数字。所以可以先根据第一列进行二分查找。寻找target可能在的行
    2.若找到目标行,则在这一行内进行二分查找。即可得到答案。

  • 提交结果
    在这里插入图片描述

75. 颜色分类

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

注意:
不能使用代码库中的排序函数来解决这道题。

示例:
在这里插入图片描述
进阶:

* 一个直观的解决方案是使用计数排序的两趟扫描算法。
首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
* 你能想出一个仅使用常数空间的一趟扫描算法吗?
  • 解答
    //方法1
    public static void sortColors(int[] nums) {
        int head = 0;
        int tail = nums.length - 1;
        while (nums[head] == 0 && head < nums.length - 1) head++;//前面是0的不用管它
        while (nums[tail] == 2 && tail > 1) tail--;//后面是2的不用管它
        //从head到tail的位置遍历数组
        for (int i = head; i <= tail; i++) {
            //若当前的位置是2,则将其与tail所指向的位置交换
            if(nums[i] == 2){
                nums[i] = nums[tail];
                nums[tail] = 2;
                //若tail原来指向的是0,则0要和head所指向的交换
                if(nums[i] == 0){
                    nums[i] = nums[head];
                    nums[head] = 0;
                    //求改head指针
                    while (nums[head] == 0 && head < nums.length - 1) head++;
                }
                //修改tail指针
                while (nums[tail] == 2 && tail > 1) tail--;
            }
            //若当前位置是0,则将其与head所指向的位置交换。因为i之前不可能会有2,所以不用像上面再判断一次交换的数字是否为2.
            else if(nums[i] == 0 && i>head){
                nums[i] = nums[head];
                nums[head] = 0;
                //修改head指针
                while (nums[head] == 0 && head < nums.length - 1) head++;
            }
        }
    }

    //方法2 利用三指针
    public void sortColors2(int[] nums) {
        int redPosition, whitePosition, bluePosition;
        redPosition = whitePosition = bluePosition = 0;
        for(int temp = 0;temp<nums.length;temp++){
            switch (nums[temp]){
                case 0://若遇到0,则修改每个指针指向的位置,并后移
                    nums[bluePosition++] = 2;
                    nums[whitePosition++] = 1;
                    nums[redPosition++] = 0;
                    break;
                case 1://若遇到1,则修改12指针指向的位置,并后移
                    nums[bluePosition++] = 2;
                    nums[whitePosition++] = 1;
                    break;
                case 2://若遇到2,则修改2指针指向的位置,并后移
                    nums[bluePosition++] = 2;
                    break;
            }
        }
    }
    
  • 分析

    1.方法1主要就是判断数组中数字0和2的位置,将0放到前面,2放到后面。
    2.方法2比较巧妙,运用了三指针。具体的过程可以举个简单的例子,比如2,1,0,2,1。跟着步骤走一遍就清楚了。

  • 提交结果

方法1
在这里插入图片描述
方法2
在这里插入图片描述

76. 最小覆盖子串

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。

示例:
在这里插入图片描述
说明:

  • 如果 S 中不存这样的子串,则返回空字符串 “”。
  • 如果 S 中存在这样的子串,我们保证它是唯一的答案。
  • 解答
    public String minWindow(String s, String t) {
        if (t.length() == 0 || s.length() < t.length()) return "";
        //记录最短子串的开始位置和长度
        int start = 0, minLen = Integer.MAX_VALUE;
        int left = 0, right = 0;
        int macth = 0;//用于记录匹配的个数
        //定义t的各个元素hash存储
        Map<Character, Integer> tMap = new HashMap<>();
        // 记录t中各个字母出现的次数
        for(int i=0;i<t.length();i++)
            tMap.put(t.charAt(i), tMap.getOrDefault(t.charAt(i), 0) + 1);
        //滑动窗口中t中包含的字母出现的次数
        Map<Character, Integer> windows = new HashMap<>();
        
        while (right < s.length()) {
            //窗口中下标为right的字母在t中出现,则记录下个数
            if (tMap.containsKey(s.charAt(right))) {
                windows.put(s.charAt(right), windows.getOrDefault(s.charAt(right), 0) + 1);
                //当个数一致的时候,说明匹配上
                if ((int)tMap.get(s.charAt(right)) == (int)windows.get(s.charAt(right)))
                    macth++;
            }
            right++;
            while (macth == tMap.size()) {
                if (right - left < minLen) {
                    start = left;//滑动窗口的起始位置
                    minLen = right - left;//长度
                }
                //缩小区间
                if (tMap.containsKey(s.charAt(left))) {
                    windows.put(s.charAt(left), windows.getOrDefault(s.charAt(left), 0) - 1);
                    if ((int)tMap.get(s.charAt(left)) > (int)windows.get(s.charAt(left)))
                        macth--;
                }
                left++;
            }
        }
        return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
    }
  • 分析

    1.利用滑动窗口加字母计数来实现。
    2.因为匹配窗口内的字母和字符串t需要的时间长,有的测试用例会时间超时,所以使用计数来实现,只要比较t中出现字母的个数和滑动窗口内相同字母出现的个数一致,则说明匹配。
    3.一开始滑动窗口是移动的右边的区间,所以当匹配后,可以缩小左区间。

  • 提交结果
    在这里插入图片描述

77. 组合

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:
在这里插入图片描述

  • 解答
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(res, n, k, new ArrayList<>(), 1);
        return res;
    }
    //回溯法
    public void backtrack(List<List<Integer>> res, int n, int k, List<Integer> tmp, int number) {
        if (tmp.size() == k)
            res.add(tmp);
        for (int i = number; i <= n; i++) {
            tmp.add(i);
            backtrack(res, n, k, tmp, ++number);
            tmp.remove((Integer) i);
        }
    }
  • 分析

    1.看到组合的题目,一下就想到了回溯法。
    2.经典的回溯方法就可实现。因为组合不可重复,所以注意每一轮迭代的起始的添加数字。

  • 提交结果
    在这里插入图片描述

78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。
示例:
在这里插入图片描述

  • 解答
//方法一:
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(res,nums,new ArrayList<>(),0);
        return res;
    }
        
        //回溯法
    public void backtrack(List<List<Integer>> res ,int[] nums,List<Integer> tmp,int number){
        res.add(new ArrayList<>(tmp));
        for (int i = number; i < nums.length; i++) {
            tmp.add(nums[i]);
            backtrack(res,nums,tmp,++number);
            tmp.remove(tmp.size()-1);
        }
    }
//方法二:
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<>());
        for (int i = 0; i < nums.length; i++) {
           int all = res.size();
            for (int j = 0; j < all; j++) {//在已找到的组合的基础上,加上新的数字,得到新的组合加入答案中。
                List<Integer> tmp = new ArrayList<>(res.get(j));
                tmp.add(nums[i]);
                res.add(tmp);
            }
        }
        return res;
    }
  • 分析

    1.方法一,写了很多次的回溯法。
    2.方法二,遍历数组。每当一个新的数字出现,遍历所有已找到的组合,在已找到组合的基础上添加新数字,得到新的组合添加到答案中。

  • 提交结果
    方法一:
    在这里插入图片描述
    方法二:
    在这里插入图片描述

79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:
在这里插入图片描述
提示:

  • board 和 word 中只包含大写和小写英文字母。
  • 1 <= board.length <= 200
  • 1 <= board[i].length <= 200
  • 1 <= word.length <= 10^3
    public boolean exist(char[][] board, String word) {
        //遍历board中的每一个字母
        for (int i = 0; i < board.length; i++){
            for (int j = 0; j < board[0].length; j++) {
                //搜索,看是否有能组成word的组合
                if (search(board, word, i, j, 0)) {
                    return true;
                }
            }
        }
        return  false;
    }

    boolean search(char[][] board, String word, int i, int j, int k) {
        if (k >= word.length()) return true;//当k大于word长度,说明找到了符合的组合。
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != word.charAt(k)) return false;//超出board边界表示没找到符合的组合
        board[i][j] += 256;//+256表示使用了这个位置的字母
        //4个方向搜索 用或计算,有一个true 则为true
        boolean result = search(board, word, i - 1, j, k + 1) || search(board, word, i + 1, j, k + 1)
                || search(board, word, i, j - 1, k + 1) || search(board, word, i, j + 1, k + 1);
        //回溯
        board[i][j] -= 256;
        return result;
    }
  • 分析

    1.遍历board中的每一个字母,作为搜索的起点。
    2.数组复用,在board数组中修改原来的字母,即标记该字母已被使用,不需要额外开辟一个空间来记录字母是否被使用
    3.用递归+回溯即可找到组合
    4.搜索的方向有4个,上下左右,四个方向的结果做或运算即可得到是否可以找到组合。

  • 提交结果
    在这里插入图片描述

80. 删除排序数组中的重复项 II

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

示例 1:
在这里插入图片描述
示例 2:
在这里插入图片描述
说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:
在这里插入图片描述

  • 解答
public int removeDuplicates(int[] nums) {
        int number = 1;//标记每个数字出现的次数
        int j = 1;//修改位
        int n = 0;//多余的数字的个数
        for (int i = 1; i < nums.length; i++) {
            //当前数字和之前一致,则number++;
            if (nums[i] == nums[i - 1]) number++;
            //否则 number设置为1
            else number = 1;
            //j位的数字修改为i位的数字
            nums[j] = nums[i];
            //若重复的数字等于3
            if (number == 3) {
                j = i - n;//记录下此时的修改位,说明这一位置是需要修改的
                n++;//多余的数字个数+1
            } 
            //若重复的数字不足与3,那么修改位+1,用于后面的数字前移
            else if (number < 3) j++;
            // 若重复数字大于3,则多余的数字个数+1
            else n++;
        }
        //j的位置即为修改后数组的长度
        return j;
    }
  • 分析

    1.因为需要原地修改数组,所以需要一个标志位j,用来记录需要修改的位置。
    2.遍历数组,一开始遍历的下标和j是一样的,表示不需要修改。当出现重复数字的时候,就需要一个额外的计数number来记录一个数字重复的次数。当number等于3的时候,此时就要修改标示位j,标示位j与当前遍历的位置,以及已找到的多余数字的个数有关。
    3.需要一个计数位n来记录多余的数字的个数。
    4.可以发现j之前的数字都已符合题目要求,所以最后返回j即为新的长度

  • 提交结果
    在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值