持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
4. 寻找两个正序数组的中位数 *
思路
(递归) O(log(n+m))
找出两个正序数组的中位数等价于找出两个正序数组中的第k小数。如果两个数组的大小分别为n和m ,那么第 k = (n + m)/2 小数就是我们要求的中位数。
如何寻找第k小的元素?
过程如下:
1、考虑一般情况,我们在 nums1和nums2数组中各取前k/2个元素
我们默认nums1数组比nums2数组的有效长度小 。nums1数组的有效长度从i开始,nums2数组的有效长度从j开始,其中[i,si - 1]是nums1数组的前k / 2个元素,[j, sj - 1]是nums2数组的前k / 2个元素。
2、接下来我们去比较nums1[si - 1]和nums2[sj - 1]的大小。
- 如果
nums1[si - 1] > nums2[sj - 1],则说明nums1中取的元素过多,nums2中取的元素过少。因此nums2中的前k/2个元素一定都小于等于第k小数,即nums2[j,sj-1]中元素。我们可以舍去这部分元素,在剩下的区间内去找第k - k / 2小的元素,也就是说第k小一定在[i,n]与[sj,m]中。 - 如果
nums1[si - 1] <= nums2[sj - 1],同理可说明nums2中的前k/2个元素一定都小于等于第k小数,即nums1[i,si-1]中元素。我们可以舍去这部分元素,在剩下的区间内去找第k - k / 2小的元素,也就是说第k小一定在[si,n]与[j,m]中。
3、递归过程2,每次可将问题的规模减少一半,最后剩下的一个数就是我们要找的第k小数。
递归边界:
- 当
nums1数组为空时,我们直接返回nums2数组的第k小数。 - 当
k == 1时,且两个数组均不为空,我们返回两个数组首元素的最小值,即min(nums1[i], nums2[j])。
奇偶分析:
- 当两个数组元素个数的总和
total为偶数时,找到第total / 2小left和第total / 2 + 1小right,结果是(left + right / 2.0)。 - 当
total为奇数时,找到第total / 2 + 1小,即为结果。
时间复杂度分析: k=(m+n)/2,且每次递归 k 的规模都减少一半,因此时间复杂度是O(log(m+n)).
c++代码
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int tot = nums1.size() + nums2.size();
if(tot % 2 == 0){
int left = find(nums1, 0, nums2, 0, tot / 2);
int right =find(nums1, 0, nums2, 0, tot / 2 + 1);
return (left + right) / 2.0;
}else{
return find(nums1, 0, nums2, 0, tot / 2 + 1);
}
}
int find(vector<int>& nums1,int i, vector<int>& nums2, int j, int k){
if(nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);
if(k == 1){
//当第一个数组已经用完
if(i == nums1.size()) return nums2[j];
else return min(nums1[i], nums2[j]);
}
//当nums1数组为空时,我们直接返回nums2数组的第k小数。
if (nums1.size() == i) return nums2[j + k - 1];
int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;
if(nums1[si - 1] > nums2[sj - 1]){
return find(nums1, i, nums2, sj, k - (sj - j));
}else{
return find(nums1, si, nums2, j, k - (si - i));
}
}
};
10. 正则表达式匹配 *
思路
(动态规划) O(nm)
状态表示: f[i][j] 表示字符串s 的前 i个字符和字符串 p 的前j 个字符能否匹配。
状态计算:
根据p[j] 是什么来划分集合:
-
1、
p[j] != '*',即p[j]是字符, 看p[j]和s[i]的关系。如果p[j] == s[i],则需判断s的前i - 1个字母 能否和p的前j -1个字母匹配 ,即f[i][j] == f[i - 1][j - 1],不匹配 , 无法转移。 -
2
P[j]是匹配符:- 如果
p[j] == '.',则p[j]和s[j]匹配 ,则需判断s的前i - 1个字母能否和p的前j -1个字母匹配 ,即f[i][j] == f[i - 1][j - 1]。 p[j] == '*',得看p[j - 1]和s[i]的关系。如果不匹配,即p[j - 1] != s[i],那么'*'匹配0个p[j - 1],则需判断s的前i个字母 能否和p的前j - 2个字母匹配 ,即f[i][j] == f[i][j - 2]。如果匹配,即p[j - 1] == s[i] || p[j - 1] == '.',则需判断s的前i - 1个字母能否和p的前j个字母匹配 ,即f[i][j] == f[i - 1][j])。
- 如果
总结:
f[i][j] == f[i - 1][j - 1], 前提条件为p[j] == s[i] || p[j] == '.'
f[i][j] == f[i][j - 2], 前提条件为p[j] == '*' && p[j - 1] != s[i]
f[i][j] == f[i - 1][j], 前提条件为p[j] == '*' && ( p[j - 1] == s[i] || p[j - 1] == '.')
c++代码
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<vector<bool>> f(n + 1, vector<bool>(m + 1));
f[0][0] = true;
for(int i = 0; i <= n; i++)
for(int j = 1; j <= m; j++){
if(j + 1 <= m && p[j + 1] == '*') continue;
if(i &&p[j] != '*'){
f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
}else if(p[j] == '*'){
f[i][j] = f[i][j - 2] || i && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.');
}
}
return f[n][m];
}
};
5. 最长回文子串
思路
(双指针) O(n^2)
- 1、枚举数组中的每个位置
i,从当前位置开始向两边扩散 - 2、当回文子串的长度是奇数时,从
i - 1,i + 1开始往两边扩散 - 3、当回文子串的长度是偶数时,从
i,i + 1开始往两边扩散 - 4、找到以
i为中心的最长回文子串的长度,若存在回文子串比以前的长,则更新答案。
图示:
时间复杂度分析: 枚举数组中的每个位置
i需要O(n)的时间复杂度,求回文子串需要O(n)的时间复杂度,因此总的时间复杂度为O(n^2)。
c++代码
class Solution {
public:
string longestPalindrome(string s) {
string res;
for(int i = 0; i < s.size(); i++){
int l = i, r = i + 1; //回文串长度为偶数
while(l >= 0 && r < s.size() && s[l] == s[r]) l--, r++;
if(res.size() < r - l - 1) res = s.substr(l + 1, r - l - 1);
l = i - 1, r = i + 1; //回文串长度为奇数
while(l >= 0 && r < s.size() && s[l] == s[r]) l--, r++;
if(res.size() < r - l - 1) res = s.substr(l + 1, r - l - 1);
}
return res;
}
};