文章目录
- 注:滑动窗口可看成一个双端队列
面试题48 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
方法一、滑动窗口(以双指针实现,两个指针控制窗口滑动)
方法二 动态规划
面试题57-I 和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
方法:双指针
题解:
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> res;
int left = 0;
int right = nums.size()-1;
while(left<right){
if(nums[left] + nums[right] == target){
res.push_back(nums[left]);
res.push_back(nums[right]);
return res;
}
else if(nums[left] + nums[right] < target) left++;
else right--;
}
return res;
}
};
时间复杂度 O(N): N 为数组 nums的长度;双指针共同线性遍历整个数组。
空间复杂度 O(1): 变量 i, j 使用常数大小的额外空间。
面试题57 - II. 和为s的连续正数序列
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
方法:(滑动窗口)
设连续正整数序列的左边界 left 和右边界 right,则可构建滑动窗口从左向右滑动。循环中,每轮判断滑动窗口内元素和与目标值 target的大小关系,若大于 target 则移动左边界 left (以减小窗口内的元素和);若小于 target 则移动右边界 right (以增大窗口内的元素和);若相等则记录结果,并移动左边界(或右边界)。
代码:
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
int left=1, right=2, sum=3;
while(left<right){
if(sum<target){
right++;
sum += right;
}
else if(sum>target){
sum -= left;
left++;
}
else{
vector<int> tmp;
for(int i=left; i<=right; i++){
tmp.push_back(i);
}
res.push_back(tmp);
sum -= left;
left++;
}
}
return res;
}
};
面试题 59 - I. 滑动窗口的最大值
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
题解:
- 我们可以动态维护一个双端队列,让队列的头部(front())始终是当前滑动窗口的最大值的索引。
- 这样我们每次只需要取该索引对应的值,就是当前滑动窗口的最大值了。
- 注意,我们的双端队列中储存的是元素的索引,而不是元素的值。因为当当前滑动窗口已经离开队列的头部索引所指向的值时,我们需要弹出当前队列的头部索引。而只有让队列储存的是索引,通过索引运算才能办到这一点。
- 之所以用双端队列,是因为容器的头部和尾部都会弹出元素,所以需要一个两端开口的容器。
具体思路参考《剑指offer》289-291解析:
代码:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
deque<int> index;
if(k<=0 || k>nums.size() || nums.size()==0) return res;
//先移动生成第一个完整的滑动窗口,并计算最大值
for(int i=0; i<k; i++){
while(!index.empty() && nums[i]>=nums[index.back()])
index.pop_back();
index.push_back(i);
}
for(int i=k; i<nums.size(); i++){
//这是先返回上一个滑动窗口的最大值,所以最后一个滑动窗口的最大值,并没有在for循环里返回,循环外需加上。
res.push_back(nums[index.front()]);
//nums[i]>=nums[index.back()]则nums[index.back()]肯定不是当前滑动窗口的最大值,弹出;否则nums[i]可能是一个滑动窗口的最大值;
while(!index.empty() && nums[i]>=nums[index.back()])
index.pop_back();
//将不属于当前滑动窗口的弹出,剩下的队列头元素则是当前滑动窗口的最大值;
if(!index.empty() && (i-index.front()) >= k)
index.pop_front();
index.push_back(i);
}
// 循环外加上的最后一个滑动窗口最大值
res.push_back(nums[index.front()]);
return res;
}
};
可以将生成第一个滑动窗口的过程合并到第二个for循环中:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
deque<int> index;
if(k<=0 || k>nums.size() || nums.size()==0) return res;
//此时i从0开始
for(int i=0; i<nums.size(); i++){
//控制先移动生成第一个完整的滑动窗口,并计算最大值
if(i>=k)
//这是先返回上一个滑动窗口的最大值,所以最后一个滑动窗口的最大值,并没有在for循环里返回,循环外需加上。
//之所以用if(i>=k),是因为首先是一个完整的滑动窗口,才能计算最大值。
res.push_back(nums[index.front()]);
//nums[i]>=nums[index.back()]则nums[index.back()]肯定不是当前滑动窗口的最大值,弹出;否则nums[i]可能是一个滑动窗口的最大值;
while(!index.empty() && nums[i]>=nums[index.back()])
index.pop_back();
//将不属于当前滑动窗口的弹出,剩下的队列头元素则是当前滑动窗口的最大值;
if(!index.empty() && (i-index.front()) >= k)
index.pop_front();
index.push_back(i);
}
// 循环外加上的最后一个滑动窗口最大值
res.push_back(nums[index.front()]);
return res;
}
};
时间复杂度:O(N)
空间复杂度:O(N)
面试题 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:
输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
方法:deque + queue(双端队列) 的组合
思路:
考虑构建一个单调不增列表
来保存队列 递减的元素
,单调不增链表随着入队和出队操作实时更新,这样队列最大元素就始终对应单调不增列表的首元素,实现了获取最大值 O(1)时间复杂度。
注:
单调不增列表
用双端队列实现;- 设计双向队列为
单调不增 的原因
:若队列 queue 中存在两个值相同的最大元素
,此时 queue 和 deque 同时弹出一个最大元素,而 queue 中还有一个此最大元素;即采用单调递减将导致两队列中的元素不一致。
具体步骤:
- 我们先建立一个普通队列 allData,用来储存所有的元素。我们再动态维护一个双端队列 maxData,它的头部元素永远是当前 allData 里所有元素的最大值,用来满足 max_value() 的调用。接下来我们来逐个分析题目要我们实现的三个函数:max_value()、push_back() 以及 pop_front() 的思路。
- max_value():很自然,这个函数是要我们返回当前队列元素的最大值,那我们直接返回 maxData.front() 就好了;当 maxData 为空时,返回 -1。
- push_back():首先我们要将新元素插入 allData 中,因为 allData 是储存所有元素的。然后我们应该更新 maxData,因为
假如
新插入的元素比 maxData 中已有的一些元素大,那么
这些比新插入元素小的元素在任何时期都不可能是最大的。因为新插入的元素肯定会比这些元素要晚弹出
allData 队列,而且新元素比这些元素都大。 - pop_front():首先我们需要返回的是 allData 队列中的头部元素。但是同时我们需要对 maxData 的头部元素做一个判断。假如 allData 弹出的元素恰好就是当前队列中最大的元素(也就是 maxData 的头部元素),我们需要将 maxData 的头部元素也同步弹出;但如果 allData 弹出的元素并不是 maxData 的头部元素,则不需要动 maxData。
代码:
class MaxQueue {
public:
queue<int> allDdata;
deque<int> maxData;
MaxQueue() {
}
int max_value() {
if(maxData.empty()) return -1;
int res = maxData.front();
return res;
}
void push_back(int value) {
allDdata.push(value);
while(!maxData.empty() && value>maxData.back()){
// 均摊时间复杂度满足 O(1)。因为最坏情况是某一次插入操作有 n 次出队操作,且每个数字只出队一次,
// 那么把这 n 次出队平摊到前面的 n 次插入操作的话,均摊到每一次的出队操作就是 O(1) 了。
maxData.pop_back();
maxData.pop_back();
}
maxData.push_back(value);
}
int pop_front() {
if(allDdata.empty()) return -1;
int res = allDdata.front();
allDdata.pop();
if(res==maxData.front()) maxData.pop_front();
return res;
}
};
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue* obj = new MaxQueue();
* int param_1 = obj->max_value();
* obj->push_back(value);
* int param_3 = obj->pop_front();
*/
时间复杂度 O(1) : max_value(), push_back(), pop_front() 方法的均摊时间复杂度均为 O(1);
空间复杂度 O(N): 当元素个数为 N 时,最差情况下deque 中保存 N 个元素,使用 O(N) 的额外空间;