持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情
32. 最长有效括号
思路
(栈) O(n)
我们可以发现一个规律,每一段合法括号序列它在字符串s中出现的位置一定是连续且不相交的,如下图所示:
因此我们能想到的最直接的做法是找到每个可能的子串后判断它是否为合法括号序列,但这样的时间复杂度会达到 O(n^3) 。
有没有一种更高效的做法?
我们知道栈在处理括号匹配有着天然的优势,于是考虑用栈去判断序列的合法性。遍历整个字符串s,把所有的合法括号序列按照右括号来分类,对于每一个右括号,都去求一下以这个右括号为右端点的最长的合法括号序列的左端点在什么位置。我们把每个右括号都枚举一遍之后,再取一个max,就是整个的最大长度。
具体过程如下:
-
1、用栈维护当前待匹配的左括号的位置,同时用
start记录一个新的可能合法的子串的起始位置,初始设为0。 -
2、如果
s[i] == '(',那么把i进栈。 -
3、如果
s[i] == ')',那么弹出栈顶元素 (代表栈顶的左括号匹配到了右括号),出栈后:-
如果栈为空,说明以当前右括号为右端点的合法括号序列的左端点为
start,则更新答案i - start + 1。 -
如果栈不为空,说明以当前右括号为右端点的合法括号序列的左端点为栈顶元素的下一个元素,则更新答案
i - (st.top() + 1) + 1。
-
-
4、遇到右括号
)且当前栈为空,则当前的start开始的子串不再可能为合法子串了,下一个合法子串的起始位置可能是i + 1,更新start = i + 1。 -
5、最后返回答案即可。
实现细节: 栈保存的是下标。
时间复杂度分析: 每个位置遍历一次,最多进栈一次,故时间复杂度为 O(n)。
c++代码
class Solution {
public:
int longestValidParentheses(string s) {
stack<int> stk;
int res = 0, start = 0;
for(int i = 0; i < s.size(); i++){
if(s[i] == '(') stk.push(i);
else{
if(!stk.empty()){
stk.pop();
if(stk.empty()) res = max(res, i - start + 1);
else res = max(res, i - stk.top());
}else{
start = i + 1;
}
}
}
return res;
}
};
33. 搜索旋转排序数组
思路
(二分) O(logn)
1、先找到旋转点,在旋转点左边的点都大于等于nums[0],右边的点都小于nums[0],因此可以用二分找到该旋转点,即>= nums[0]的最右边界。
-
当
nums[mid] >= nums[0]时,往右边区域找,l = mid。 -
当
nums[mid] < nums[0]时,往左边区域找,r = mid - 1。
2、找到旋转点l后,可以知道[0,l],[l + 1, n - 1]是两个有序数组,判断出target的值在哪个有序数组中,确定好二分的区间[l,r] 。
- 当
target >= nums[0],说明target在[0, l]区间内,我们令l = 0,r保持不变。 - 否则,说明
target在[l + 1, n - 1]区间内,我们令l = r + 1,r = n - 1。
3、在[l,r]区间中,由于该区域也具有单调性,通过二分找到该值的位置,即二分>= target的最左边界
-
当
nums[mid] >= target时,往左边区域找,r = mid。 -
当
nums[mid] < target时, 往右边区域找,l = mid + 1。
4、若最后找到的元素
nums[r] != target,则表示不存在该数,返回-1,否则返回该数值。
时间复杂度分析: 二分的时间复杂度为 O(logn)
c++代码
class Solution {
public:
int search(vector<int>& nums, int target) {
if(nums.empty()) return -1;
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = (l + r + 1) / 2;
if(nums[mid] >= nums[0]) l = mid;
else r = mid - 1;
} // l == r
if(target >= nums[0]) l = 0;
else l = r + 1, r = nums.size() - 1;
while(l < r){
int mid = (l + r) / 2;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
if(target == nums[r]) return r;
return -1;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
思路
(二分) O(logn)
两次二分,第一次二分查找第一个>=target的位置,第二次二分查找最后一个<=target的位置。查找成功则返回两个位置下标,否则返回[-1,-1]。
第一次
-
1、二分的范围,
l = 0,r = nums.size() - 1,我们去二分查找>=target的最左边界。 -
2、当
nums[mid] >= target时,往左半区域找,r = mid。
-
3、当
nums[mid] < target时, 往右半区域找,l = mid + 1。
- 4、如果
nums[r] != target,说明数组中不存在目标值target,返回[-1, -1]。否则我们就找到了第一个>=target的位置L。
第二次
-
1、二分的范围,
l = 0,r = nums.size() - 1,我们去二分查找<=target的最右边界。 -
2、当
nums[mid] <= target时,往右半区域找,l = mid。
-
3、当
nums[mid] > target时, 往左半区域找,r = mid - 1。
- 4、找到了最后一个
<=target的位置R,返回区间[L,R]即可。
时间复杂度分析: 两次二分查找的时间复杂度为 O(logn)。
c++代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(!nums.size()) return {-1, -1};
int l = 0, r = nums.size() - 1;
while(l < r){ //查找 >= target的最左边界
int mid = (l + r) / 2;
if(nums[mid] >= target) r = mid;
else l = mid + 1;
}
if(nums[r] != target) return {-1, -1};
int L = r;
l = 0, r = nums.size() - 1;
while(l < r){
int mid = (l + r + 1) / 2;
if(nums[mid] <= target) l = mid;
else r = mid - 1;
}
return {L, r};
}
};