每周 Leetcode : 10、11、74、692、169

154 阅读7分钟

本周的 5 道 Leetcode 分别是:

  • 10 正则表达式匹配
  • 11 盛水最多的容器
  • 74 搜索二维矩阵
  • 692 前 k 个高频词
  • 169 多数元素

10 正则表达式匹配

题目描述

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。'.' 匹配任意单个字符,'*' 匹配零个或多个前面的那一个元素。所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。

  • 示例 1:
输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
  • 示例 2:
输入:s = "aa" p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
  • 示例 3:
输入:s = "ab" p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
  • 示例 4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
  • 示例 5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false
  • 提示:
    • 0 <= s.length <= 20
    • 0 <= p.length <= 30
    • s 可能为空,且只包含从 a-z 的小写字母。
    • p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *
    • 保证每次出现字符 * 时,前面都匹配到有效的字符

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/re…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

dp[i][j] 表示 si 个字符和 pj 个字符是否匹配。我们可以从后往前进行迭代计算。

  • 情况 1s[i] == p[j] || p[j] == '.'

这种情况说明 s[i]p[j] 单个字符匹配。此时只需要比较它们前面的部分是否匹配即可。状态转移方程为:

// s[i] == p[j] || p[j] == '.'
dp[i][j] = dp[i-1][j-1]

如果 s[i]p[j] 无法匹配,说明整个字符串都无法匹配。

  • 情况 2p[j] == '*'

由于 * 与前一个的字符绑定,所以我们还要看前一个字符即 p[j-1]

如果 p[j-1] == s[i] :说明至少可以匹配一次,那么此时只需要看 si-1 个字符是否和 p 匹配。状态转移方程为:

// p[j] == '*' && p[j-1] == s[i]
dp[i][j] = dp[i-1][j]

如果 p[j-1] == '.' : 状态转移方程为:

// p[j] == '*' && p[j-1] == '.'
dp[i][j] = dp[i][j-2]

如果 p[j-1] != s[i] :说明此时的 p[j-1]* 组合无法匹配任意字符即匹配 0 次,我们可以认为这个组合没有出现过。状态转移方程为:

// p[j] == '*' && p[j-1] != s[i]
dp[i][j] = dp[i][j-2]

根据以上的各种情况,就可以求出最终的结果:dp[s.length][p.length]

从后往前计算,必须要设置相应的边界条件:dp[0][0] = true 即空字符串可以匹配。由于 dp[0][0] 表示的是空字符串,所以我们的 dp 数组实际上多了一行一列,dp[1][1] 表示 s[0]p[0] 是否匹配。

我们可以将 p[j] == '*' && p[j-1] == s[i]p[j] == '*' && p[j-1] == s[i] 看作一种情况,它们都是能够匹配成功的。判断两个字符是否匹配的方法封装为 isMath()

// 解法一 动态规划
public boolean isMatch(String s, String p) {
    int sl = s.length();
    int pl = p.length();
    boolean[][] dp = new boolean[sl + 1][pl + 1];
    dp[0][0] = true;
    for(int i = 0; i <= sl; ++i){
        for(int j = 1; j <= pl; ++j){
            if(p.charAt(j - 1) != '*'){
                if(isMatch(s,p,i,j)){
                    dp[i][j] = dp[i-1][j-1];
                }
            }else {
                if(isMatch(s,p,i,j-1)){
                    dp[i][j] = dp[i-1][j] || dp[i][j-2];
                }else {
                    dp[i][j] = dp[i][j-2];
                }
            }
        }
    }
    return dp[sl][pl];
}

public boolean isMatch(String s, String p, int si,int pj){
    if(si == 0){
        return false;
    }
    if(p.charAt(pj-1) == '.'){
        return true;
    }
    return s.charAt(si-1) == p.charAt(pj-1);
}

11 盛水最多的容器

题目描述

给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai)(i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器。

  • 示例 1:

image.png

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/co…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 双指针法

我们用左右指针 left,right 分别表示容器的两个侧边所在的位置,初始情况下,left = 0,right = length - 1

当左右指针没有相遇即 left < right 时,我们总是移动较矮的边的索引(left++right--)。这是因为容器的高度是较矮的那条边的长度,如果移动较高的边的索引,高度不会发生变化,而宽度(right-left)在减小,那么面积只会更小。

面试的计算公式为:

area=Math.min(height[left],height[right])(rightleft)area = Math.min(height[left],height[right]) * (right - left)

我们还注意到一点,假设当前的容器的高度为 h,在以后的移动过程中,由于宽度在减小,要想面积更大,高度必须要比当前的高度 h 更大。所以我们增加一个变量 h,用来动态表示移动过程中容器的高度,只有 heigth[left],height[right] 的较小值比 h 时,才计算面积并更新最大面积。

为了方便计算,我们将 h 初始化为 -1

下面用示例 1 来展示这整个的过程。

  • 一开始,left = 0,right = 8,此时的面积为 8,容器的高度为 1,由于左边更矮,我们移动 left

image.png

  • 此时,较小高度为 7,比 h 大,需要计算面积为 49。移动 right

image.png

  • 由于高度 3 比 h 小,所以不需要计算面积,继续移动右指针

image.png

  • 较小高度为相等都为 8 且比 h 大,计算面试并比较更新,移动 leftright,假设移动 left

image.png

  • 可以看到,再往中间移动,无论移动哪个指针,高度总是比 h 小,所以都不会更新面积。直到 left >= right 结束,最终的面积为 49。

代码实现如下:

// 解法一 双指针法
public int maxArea(int[] height) {
    int left = 0;
    int right = height.length - 1;
    int result = 0;
    int h = -1; 
    int min = 0; // 用来表示较矮的高度值
    while(left < right){
        if(h < (min = Math.min(height[left],height[right]))){
            h = min;
            result = Math.max(result,h*(right - left));
        }
        if(height[left] < height[right]){
            left++;
        }else{
            right--;
        }
    }
    return result;
}

74 搜索二维矩阵

题目描述

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

  1. 每行中的整数从左到右按升序排列。
  2. 每行的第一个整数大于前一行的最后一个整数。
  • 示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
  • 示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
  • 提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 100
-104 <= matrix[i][j], target <= 104

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/se…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 二分查找

按照题目的意思,如果将矩阵的每一行依次拼接成一个数组,该数组是升序的。因此可以使用 二分查找

但是我们要将线性的索引值映射为矩阵中对应的元素,映射函数如下:

/**
* 将矩阵每一行按顺序排成线性结构,矩阵维数为 m*n,那么 0 <= index <= m*n - 1
* 根据给定的索引返回矩阵中对应的元素
*/
private int mapMatrix(int[][] matrix, int index){
    return matrix[index/matrix[0].length][index%matrix[0].length];
}

二分查找的实现如下:

class Solution {
    /** 方法 1 二分查找法 */
    public boolean searchMatrix(int[][] matrix, int target) {
        int left = 0;
        int right = matrix.length * matrix[0].length - 1;
        while(left <= right){
            int mid = left + (right - left)/2;
            int midValue = mapMatrix(matrix,mid);
            if(midValue == target){
                return true;
            }else if(midValue > target){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return false;
    }
}

解法二 按行查找

定义矩阵的行列数为 row,col。从第 row 行,第 i(i=0) 列开始和 target 比较:

  • 如果 target 更大,说明应该在第本行查找,将 i++row 不变
  • 如果 target 更小,说明应该从上一行开始继续查找,此时 row--i 不变
  • 如果和 target 相等,返回 true

循环结束的条件为 row > 0 && i < col。代码实现如下:

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int row = matrix.length, col = matrix[0].length;
        int i = 0;
        while (row > 0 && i < col) {
            if (target > matrix[row - 1][i]) {
                i++;
            } else if (target < matrix[row - 1][i]) {
                row--;
            } else {
                return true;
            }
        }
        return false;
    }
}

692 前 k 个高频词

题目描述

给一非空的单词列表,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

  • 示例 1:
输入: ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次. 注意,按字母顺序 "i" 在 "love" 之前。
  • 示例 2:
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词, 出现次数依次为 4, 3, 2 和 1 次。
  • 注意:
假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
输入的单词均由小写字母组成。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/to…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 哈希表 + 排序

为了得到每个单词出现的频率,首先应该用 HashMap 来统计每个单词出现的次数。然后按照每个单词出现的次数降序排序,取前 k 个单词即可。

// 解法一 哈希表排序
public List<String> topKFrequent(String[] words, int k) {
    HashMap<String,Integer> counts = new HashMap<>();
    for(int i = 0; i < words.length; i++){
        counts.put(words[i],counts.getOrDefault(words[i],0) + 1);
    }
    List<String> list = new ArrayList<>(counts.keySet());
    list.sort((a,b) -> {
        // 单词出现的次数相同,按照单词默认的排序实现
        if(counts.get(a) == counts.get(b)){
            return a.compareTo(b);
        }else {
            return counts.get(b) - counts.get(a);
        }
    });
    return list.subList(0,k);
}

解法二 哈希表 + 优先队列(堆)

同样也要使用 HashMap 统计单词出现的次数。排序可以构造一个大顶堆来实现。Java 中提供了优先队列的实现 PriorityQueue ,默认是一个小顶堆的实现。构造函数中传递一个比较器来规定元素插入的顺序。

然后将单词添加到队列中,取前 k 个出队的单词即可。

// 解法二 哈希表 + 堆
public List<String> topKFrequent(String[] words, int k) {
    HashMap<String,Integer> counts = new HashMap<>();
    for(int i = 0; i < words.length; i++){
        counts.put(words[i],counts.getOrDefault(words[i],0) + 1);
    }
    PriorityQueue<String> queue = new PriorityQueue<>((a,b) -> {
        if(counts.get(a) == counts.get(b)){
            return a.compareTo(b);
        }
        return counts.get(b) - counts.get(a);
    });
    for(String s : counts.keySet()){
        queue.offer(s);
    }
    List<String> result = new ArrayList<>();
    for(int i = 0; i < k; ++i){
        result.add(queue.poll());
    }
    return result;
}

169 多数元素

题目描述

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋  的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

  • 示例 1:
输入:[3,2,3]
输出:3
  • 示例 2:
输入:[2,2,1,1,1,2,2]
输出:2

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ma…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 哈希表计数

使用 HashMap 对数组元素进行计数,当发现某个元素的计数值过半时,返回即可。

我们在计数的同时,判断是否过半,但是要注意数组只有一个元素的情况,因此将它先拿出来判断。

// 解法一 HashMap
public int majorityElement(int[] nums) {
    // 如果数组只有一个元素,直接返回
    if(nums.length == 1){
        return nums[0];
    }
    int lower = nums.length / 2;
    HashMap<Integer,Integer> map = new HashMap<>();
    for(int num : nums){
        if(!map.containsKey(num)){
            map.put(num,1);
        }else{
            map.put(num,map.get(num)+1);
            if(map.get(num) > lower){
                return num;
            }
        }
    }
    return 0;
}

解法二 分治法

利用二分思想,将数组分为左右两部分,分别计算左右两侧的多数元素,如果它们相等,则多数元素就是这个元素。如果它们不相等,多数元素应该为出现次数更多的那个多数元素。

// 解法二 分治法
public int majorityElement(int[] nums) {
    return majorityElement(nums,0,nums.length - 1);
}

// 返回 left ~ right 范围内的多数元素
public int majorityElement(int[] nums, int left, int right){
    // 只有一个元素,多数元素就是本身
    if(left == right){
        return nums[left];
    }
    // 二分
    int mid = left + (right - left)/2;
    int leftMajorityEle = majorityElement(nums,left,mid);
    int rightMajorityEle = majorityElement(nums,mid + 1,right);

    if(leftMajorityEle == rightMajorityEle){
        return leftMajorityEle;
    }

    // 左右多数元素不等,分别计算它们在左右两侧出现的次数
    int leftCounts = counts(nums,leftMajorityEle,left,mid);
    int rightCounts = counts(nums,rightMajorityEle,mid + 1,right);

    // 返回出现次数更多的
    return leftCounts > rightCounts ? leftMajorityEle : rightMajorityEle;
}

// 计算 left ~ right 范围内,target 出现的次数
private int counts(int[] nums,int target,int left,int right){
    int counts = 0;
    for(int i = left; i <= right; i++){
        if(nums[i] == target){
            counts++;
        }
    }
    return counts;
}