本周的 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]
表示 s
前 i
个字符和 p
前 j
个字符是否匹配。我们可以从后往前进行迭代计算。
- 情况 1 :
s[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]
无法匹配,说明整个字符串都无法匹配。
- 情况 2 :
p[j] == '*'
由于 *
与前一个的字符绑定,所以我们还要看前一个字符即 p[j-1]
。
如果 p[j-1] == s[i]
:说明至少可以匹配一次,那么此时只需要看 s
前 i-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:
输入:[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
)在减小,那么面积只会更小。
面试的计算公式为:
我们还注意到一点,假设当前的容器的高度为 h
,在以后的移动过程中,由于宽度在减小,要想面积更大,高度必须要比当前的高度 h
更大。所以我们增加一个变量 h
,用来动态表示移动过程中容器的高度,只有 heigth[left],height[right]
的较小值比 h
大 时,才计算面积并更新最大面积。
为了方便计算,我们将
h
初始化为-1
。
下面用示例 1 来展示这整个的过程。
- 一开始,
left = 0,right = 8
,此时的面积为 8,容器的高度为 1,由于左边更矮,我们移动left
- 此时,较小高度为 7,比
h
大,需要计算面积为 49。移动right
- 由于高度 3 比
h
小,所以不需要计算面积,继续移动右指针
- 较小高度为相等都为 8 且比
h
大,计算面试并比较更新,移动left
或right
,假设移动left
- 可以看到,再往中间移动,无论移动哪个指针,高度总是比
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:
输入: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;
}