首页 > 技术文章 > 🔥 LeetCode 热题 HOT 100(21-30)

winlsr 2021-06-30 20:02 原文

46. 全排列

思路:典型回溯法

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        boolean[] visited = new boolean[nums.length];

        dfs(nums, track, visited);
        return res;
    }

    private List<List<Integer>> res = new LinkedList<>();
    private void dfs(int[] nums, LinkedList<Integer> track, boolean[] visited) {
        //base case
        if (track.size() == nums.length) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            //剪枝
            if (visited[i]) continue;

            // 选择
            track.offerLast(nums[i]);
            visited[i] = true;

            dfs(nums, track, visited);

            //撤销选择
            track.pollLast();
            visited[i] = false;
        }
    }
}

48. 旋转图像

思路:(技巧题,记住就好)

  • 先沿对角线翻转
  • 然后每一行按中间元素翻转
class Solution {
    public void rotate(int[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;

        for (int i = 0; i < row; i++) {
            for (int j = i + 1; j < row; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }

        for (int i = 0; i < row; i++) {
            int left = 0;
            int right = col - 1;
            while (left < right) {
                int temp = matrix[i][left];
                matrix[i][left] = matrix[i][right];
                matrix[i][right] = temp;
                left++;
                right--;
            }
        }
    }
}

49. 字母异位词分组

思路:根据字母异位词定义可知:

  • 互为异位词的字符串根据字符排序后得到的字符串相同;
  • 互为异位词的字符串中各个字符的出现次数相同。

因此有两种方法:排序法、计数法。

排序法:

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();

        for (String str : strs) {
            char[] strArr = str.toCharArray();
            Arrays.sort(strArr);
            String orderStr = new String(strArr);
            List<String> list = map.getOrDefault(orderStr, new LinkedList<>());
            list.add(str);
            map.put(orderStr, list);
        }

        return new ArrayList<List<String>>(map.values());
    }
}

计数法:

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();

        for (String str : strs) {
            //统计str中字符出现次数
            int[] count = new int[26];
            for (int i = 0; i < str.length(); i++) {
                count[str.charAt(i) - 'a']++;
            }

            //拼接key
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < count.length; i++) {
                if (count[i] > 0) {
                    sb.append('a' + i);
                    sb.append(count[i]);
                }
            }
            String key = sb.toString();
            
            List<String> list = map.getOrDefault(key, new LinkedList<>());
            list.add(str);
            map.put(key, list);
        }

        return new ArrayList<List<String>>(map.values());
    }
}

53. 最大子序和

思路:动态规划

class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;

        //以当前位置结尾的子数组的最大和
        int[] dp = new int[len];

        //base case 
        dp[0] = nums[0];

        int max = dp[0];
        //转移方程:若dp[i - 1] 大于 0, dp[i] = dp[i - 1] + nums[i];否则 dp[i] = nums[i];
        for (int i = 1; i < len; i++) {
            if (dp[i - 1] > 0) {
                dp[i] = dp[i - 1] + nums[i];
            } else {
                dp[i] = nums[i];
            }
            max = Math.max(max, dp[i]);
        }

        return max;
    }
}

以上代码可以看出当前状态只与前一个状态相关,故可以将 间复杂度由O(n) 降到O(1):

class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;

        int sum = nums[0];
        int max = sum;

        for (int i = 1; i < len; i++) {
            if (sum > 0) {
                sum = sum + nums[i];
            } else {
                sum = nums[i];
            }
            max = Math.max(max, sum);
        }

        return max;
    }
}

55. 跳跃游戏

思路一:动态规划,推荐

class Solution {
    public boolean canJump(int[] nums) {
        int len = nums.length;
        //起始位置能否跳至当前位置
        boolean[] dp = new boolean[len];

        //base case
        dp[0] = true;

        //转移方程:i前面所有的点只要有一个能跳到当前结点就说明当前结点可达
        for (int i = 1; i < len; i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && (j + nums[j] >= i)) {
                    dp[i] = true;
                    break;
                }
            }
        }

        return dp[len - 1];
    }
}

思路二:贪心,时间复杂度更低,多数靠死记硬背。如果能用动态规划就尽量用动规,超时了再说

class Solution {
    public boolean canJump(int[] nums) {
        int len = nums.length;
        //目前从起点能调到的最远位置
        int maxFar = 0;

        for (int i = 0; i <= maxFar; i++) {
            if (nums[i] + i > maxFar) {
                maxFar = nums[i] + i;
            }
            if (maxFar >= (len - 1)) {
                return true;
            }
        }

        return false;
    }
}

56. 合并区间

class Solution {
    public int[][] merge(int[][] intervals) {
        int len = intervals.length;
        //按照 左 边界排序
        Arrays.sort(intervals, (arr1, arr2) -> arr1[0] - arr2[0]);
        int[][] res = new int[len][2];

        //i指向新区间的第一个小区间,k为合并后的新区间个数
        int i = 0, k = 0;
        while (i < len) {
                int left = intervals[i][0];
                //合并区间的过程就是修改right的过程
                int right = intervals[i][1];

                //寻找可以合并的区间并完成合并
                int j = i + 1;
                while (j < len && right >= intervals[j][0]) {
                    right = Math.max(right, intervals[j][1]);
                    j++;
                }

                //存储新区间的范围
                res[k][0] = left;
                res[k][1] = right;
                k++;

                i = j; 
        }

        return Arrays.copyOf(res, k);
    }
}

62. 不同路径

思路:动态规划

  • 状态:从起点到当前结点的不同路径数。

  • 转移方程:起点到当前点的不同路径数dp[i][j]等于:起点到当前结点相邻左结点dp[i][j - 1]和相邻上结点dp[i - 1][j]不同路径数之和。

  • base case:第0行dp[0][x]和0列dp[x][0]都为1,前者只能通过其相邻左节点到达,后者只能通过相邻上结点到达。

class Solution {
    public int uniquePaths(int m, int n) {
        //状态
        int dp[][] = new int[m][n];

        //base case
        for (int i = 0; i < n; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; i++) {
            dp[i][0] = 1;
        }

        //转移方程
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
            }
        }

        return dp[m - 1][n - 1];
    }
}

64. 最小路径和

思路:动态规划,和上一题相似。

  • 状态:起点到当前结点的最小路径和

  • 转移方程:起点到当前结点最小路径和dp[i][j]等于:min(起点到其相邻左结点最小路径和dp[i][j - 1],起点到其相邻上结点最小路径和dp[i - 1][j]) + 当前结点值grid[i][j]

  • base case: dp[0][0] = grid[0][0]; 第一行dp[0][x]都为其相邻左结点dp[0][x -1] + 自身结点值grid[0][x], x >= 1;第一列dp[x][0]都为其相邻上结点dp[x - 1][0] + 自身结点值grid[x][0], x >= 1

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        //从左上角到i, j的最短路径和
        int[][] dp = new int[m][n];

        //base case
        dp[0][0] = grid[0][0];
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int i = 1; i < n; i++) {
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        }

        //转移方程
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp [i][j - 1]) + grid[i][j];
            }
        }

        return dp[m - 1][n - 1];
    }
}

70. 爬楼梯

思路:动态规划

  • 状态:从第0个台阶跳到当前台阶的不同方式

  • 转移方程:第0个台阶到当前台阶的不同方式dp[i]等于:第0个台阶到当前台阶下面两个台阶的不同方式之和(dp[i - 1] + dp[i - 2])

  • base case: dp[0] = dp[1] = 1

class Solution {
    public int climbStairs(int n) {
        //状态:从第0个台阶跳到当前台阶的不同方式
        int[] dp = new int[n + 1];

        //base case
        dp[0] = 1;
        dp[1] = 1;
        
        //转移方程
        for (int i = 2; i < n + 1; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n];
    }
}

72. 编辑距离

思路:动态规划

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();

        //word1 前m个字符和 word2 前n个字符之间的编辑距离,z
        int[][] dp = new int[m + 1][n + 1];

        //base case
        dp[0][0] = 0;
        for (int i = 1; i < m + 1; i++) {
            dp[i][0] = i;
        }
        for (int i = 1; i < n + 1; i++) {
            dp[0][i] = i;
        }

        //状态转移
        for (int i = 1; i < m + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                //最后一个字符相等
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                //不等则分别进行 替换、删除word1中最后一个字符,或在word1最后插入一个字符,取代价最小的一个
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
                }
            }
        }

        return dp[m][n];
    }
}

推荐阅读