窗口
对于一个数组,我们有一个L来表示窗口的最左边,还有一个R来表示最右边。L和R只能向右移动,不能回退。L往右走是减数,R往右走就是加数,并且L也不能超过R。
窗口内最大值的更新结构
如果你想要得到一个窗口的最大值,那么你当然可以通过遍历的方法来实现,但这样每次获得窗口中最大值的代价就是遍历的代价。如果想要O(1)的时间复杂度那么可以继续往下看。
窗口内最大值的更新结构使用的是双向链表(因为不需要遍历),当我们需要加数的时候,如果先看链表尾小于等于当前值的话就直接弹出链表尾元素,直到当前链表中末尾元素比当前值大或者当前链表为空。如果链表尾元素大于当前值的话那就直接加入到链表尾。减数的更新逻辑如果移动了L,那么就需要检查头元素的有效性,这也是我们为什么要再链表中同时存储下标和对应的值的原因(其实只存储下标是可以的,不管你用不用,数组就在那儿)。窗口内存储的所有值在窗口缩小的时候都有可能成为最大值的。而从尾部弹出的元素表示它没有可能成为最大值了。
为什么相等不重复压入,而是弹出?
为了节省空间。
时间复杂度
使用这个结构的时间复杂度是O(N),但是我们找最大值并不是O(N)的,而是O(1)的。
面试题
面试题1
有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右滑一个位置。请记录下每个窗口的最大值到一个数组中。例如对于数组[4,3,5,4,3,3,6,7]这样的数组,并指定窗口大小为3,那么我们应该返回一个内容为[5,5,5,4,6,7]这样的数组。要求时间复杂度O(N)
分析
我们搞一个最大值的更新结构,每当我们遍历到数组中的某一个位置的时候,只有当单向链表的为空或者最后一个值大于数组当前元素的时候,那么我们才可以直接加入到单向链表中,否则就不断的从单向链表中弹出元素,直到满足条件位置,每次循环都要判断单向链表头是否过期了,如果过去了那么我们就直接pop掉,最后我们要判断是否形成宽为w的窗口了,如果形成了我们需要将单向链表的头元素加入进去。因为每个数都会进队列一次和出队列一次,所以复杂度O(N)。
示例代码
#include <iostream>
#include <vector>
#include <list>
using namespace std;
vector<int> getMaxWindow(const vector<int> &arr, int w)
{
if (w < 1 || arr.size() < w)
{
return vector<int>{};
}
list<int> qmax;
vector<int> res;
for (int i = 0; i < arr.size(); i++)
{
// qmax后面加入一个数字
while (!qmax.empty() && arr[qmax.back()] < arr[i])
{
qmax.pop_back();
}
qmax.push_back(i);
// 如果过期,则减数
if (qmax.front() == i - w)
{
qmax.pop_front();
}
// 窗口形成则加入到res中
if (i >= w - 1)
{
res.push_back(arr[qmax.front()]);
}
}
return res;
}
int main(int argc, char ** argv)
{
vector<int> arr = { 4, 3, 5, 4, 3, 3, 6, 7 };
int w = 3;
vector<int> res = getMaxWindow(arr, w);
for (auto e : res)
{
cout << e << ",";
}
cout << endl;
system("pause");
return EXIT_SUCCESS;
}
面试题2
给定数组arr和整数,共返回有多少个子数组满足如下条件:max(arr[i…j]) - min(arr[i…j]) <=num,max(arr[i…j]) 表示子数组arr[i…j]中最大值,min(arr[i…j]) 表示子数组arr[i…j]中最小值。要求数组长度为N,请实现时间复杂度为O(N)的解法。
暴力解法
我们可以在双重for循环中找到所有的子数组,然后判断该数组是否满足条件。
以上方法是O(N^3)的,两个for,一个遍历。
示例代码
bool isValid(const vector<int> &arr, int start, int end, int num)
{
int maxVal = INT32_MIN;
int minVal = INT32_MAX;
for (int i = start; i <= end; ++i)
{
maxVal = max(maxVal, arr[i]);
minVal = min(minVal, arr[i]);
}
return maxVal - minVal <= num;
}
int getNum1(const vector<int> &arr, int num)
{
int res = 0;
for (int start = 0; start < arr.size(); ++start)
{
for (int end = start; end < arr.size(); ++end)
{
if (isValid(arr, start, end, num))
{
res++;
}
}
}
return res;
}
窗口更新结构解法(时间复杂度O(N))
结论:
- 如果一个子数组达标,那么内部任何一个子数组都满足条件。
- 如果一个子数组不达标,那么包含该数组的任何一个子数组都不满足条件。
- 如果数组长为N,那么子数组的个数为N+N-1+N-2+…1
思路:
我们首先弄一个最大值更新结构和一个最小值更新结构,首先让R移动到不能满足条件之前的前一个元素,所有的以L位置开始的所有子数组都可以得到了,下一步我们将L向后移动,此时验证下R是否可以向右移动,如果R可以向后移动了,那么就一直移动到不满足条件的节点的前一个节点位置,如果不能R不能移动,那么就继续移动L。以此类推。时间复杂度O(N)。
示例代码:
int getNum2(const vector<int> &arr, int num)
{
if (arr.size() < 1)
{
return 0;
}
list<int> qmax;
list<int> qmin;
int start = 0;
int end = 0;
int res = 0;
while(start < arr.size())
{
while (end < arr.size())
{
while (!qmax.empty() && arr[qmax.back()] <= arr[end])
{
qmax.pop_back();
}
qmax.push_back(end);
while (!qmin.empty() && arr[qmin.back()] >= arr[end])
{
qmin.pop_back();
}
qmin.push_back(end);
if (arr[qmax.front()] - arr[qmin.front()] > num)
{
break;
}
end++;
}
if (qmin.front() == start)
{
qmin.pop_front();
}
if (qmax.front() == start)
{
qmax.pop_front();
}
res += end - start;
start++;
}
return res;
}