案例五:有一个整形数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次想右边滑一个位置。返回一个长度为n-w+1的数组res,res[i[表示每一种窗口状态下的最大值。以数组为[4,3,5,4,3,3,6,7],w=3为例。因为第一个窗口为[4.3.5]的最大值为5,第二个窗口[3,5,4]的最大值为5,第三个窗口[5,4,3]的最大值为5,。第四个窗口[4,3,3]的最大值为4,第五个窗口[3,3,6]的最大值为6,第六个窗口[3,6,7]的最大值为7,所以最终返回[5,5,4,6,7]。
对于数组长度为n,窗口大小为w,普通解法的时间复杂度为o(n*w),也就是每次对每一个窗口遍历其中的w个数,选出最大值。但是本题最优解可以做到时间复杂度为o(n)。
主要思想是利用双端队列来实现窗口最大值的更新。
双端队列qmax={},双端队列存放着数组中的下标值,假设当前数为arr[i],放入规则如下:
1. 如果qmax为空,直接把下标i放入qmax中;
2. 如果qmax不为空,取出当前qmax队尾存放的下标j,如果arr[j]>arr[i],直接把下标i放进qmax的队尾;
3.如果arr[j]<=arr[i],则一直从qmax的队尾弹出下标,直到某个下标在qmax中对应的值大于arr[i],把i放入qmax队尾;
弹出规则如下:
如果qmax队头的下标等于i-w,也就是说这个队头已经过期了,不属于当前窗口了,弹出qmax当前队头下标;
根据上面的规则,qmax就成为了维护窗口为w的子数组最大值的更新结构;
同时,还应该有一个数组res长度为n-w+1,每次出现一个新窗口的时候,都要考察队头的元素,如果队头的元素没有过期,那么就将它出队列进入res数组中;如果过期了就出队列之后,再考察下一个队头元素。
上面的过程中每个下标最多进队列一次,最多出队列也是一次,所以数据进出队列的时间复杂度为o(n).所以整个时间复杂度也是o(n).
class SlideWindow {
public:
vector<int> slide(vector<int> arr, int n, int w) {
// write code here
vector<int> res;
deque<int> q;
for(int i=0;i<n;i++){
//利用i来控制窗口
if(q.empty())
q.push_back(i);//只存放下标
else{
if(arr[i]<arr[q.back()])
q.push_back(i);
else{
q.pop_back();
while(!q.empty()&&arr[i]>=arr[q.back()]){//这里一定要判断q是否为空
q.pop_back();
}
q.push_back(i);
}
}
if(q.front()==(i-w))
q.pop_front();
if(i>=w-1)
res.push_back(arr[q.front()]);
}
return res;
}
};
有上面的程序可知程序的时间复杂度确实为o(n).
案例六:
给定一个没有重复元素的数组arr,写出生成这个数组的MaxTree函数。要求如果数组长度为N,则时间复杂度为o(n),额外空间复杂度为o(n).MaxTree的概念如下:
1. MaxTree是一颗二叉树,数组的每一个值对应一个二叉树节点。
2. 包括MaxTree树在内且在其中的每一颗子树上,值最大的节点都是树的头。
这道题的关键在于一定是没有重复元素的数组!!!!!如果有重复的元素的话,下面的过程得到的就可能不是一个二叉树了!!
具体过程为首先记录每个数据的左边第一个比他大的数以及右边第一个比他大的数;
然后每一个节点的父节点是它左边第一个比他大的数和它 右边第一个比他大的数中的比较小的那个。
证明该方法的正确性:
首先,该方法可以生成一棵树,而不是森林。
然后,生成的这一棵树是二叉树,而不是多叉树。
方法证明有效之后,利用栈得到每个数左右两边第一个比它大的数。
class MaxTree {
public:
vector<int> buildMaxTree(vector<int> A, int n) {
// write code here
//最后返回的数组是当前数组中的元素的父节点的下标
//首先生成存放元素i的左边第一个大于它的元素的数组,存放下标
vector<int> lfirstm;
//然后生成存放元素i的右边第一个大于它的元素的数组,存放下标
vector<int> rfirstm;
//用栈来实现这个过程
stack<int> b;
for(int i=0;i<n;i++){//这是计算左边第一个大于i的元素的数组也就是lfirstm
if(b.empty()){
b.push(i);
lfirstm.push_back(-1);//代表i的左边第一个大于它的数是空
}else{
if(A[b.top()]>A[i]){
lfirstm.push_back(b.top());
b.push(i);//这里不能忘记进栈
}
else{
while(!b.empty()&&A[b.top()]<A[i])
b.pop();
if(b.empty()){
lfirstm.push_back(-1);
b.push(i);//这里不能忘记进栈
}
else{
lfirstm.push_back(b.top());
b.push(i);//这里不能忘记进栈
}
}
}
}
//b.clear();stack没有这个函数,必须自己清空
while(!b.empty()){
b.pop();
}
for(int i=n-1;i>=0;i--){//这是计算右边第一个大于i的元素的数组也就是lfirstm
if(b.empty()){
b.push(i);
rfirstm.push_back(-1);//代表i的左边第一个大于它的数是空
}else{
if(A[b.top()]>A[i]){
rfirstm.push_back(b.top());
b.push(i);//这里不能忘记进栈
}
else{
while(!b.empty()&&A[b.top()]<A[i])
b.pop();
if(b.empty()){
rfirstm.push_back(-1);
b.push(i);//这里不能忘记进栈
}
else{
rfirstm.push_back(b.top());
b.push(i);//这里不能忘记进栈
}
}
}
}
vector<int> result;
int j=0;
int i=n-1;
while(j<n&&i>=0){//因为lfirstm第一个元素是A数组中的第一个元素对应值,但是rfirstm第一个元素是A数组中第n-1个元素对应值
if(lfirstm[j]!=-1&&rfirstm[i]!=-1){
if(A[lfirstm[j]]<A[rfirstm[i]])//注意这里面进行比较的不是下标,也就是说并不是lfirstm和rfirstm中的数,而是真正的数组中的数
result.push_back(lfirstm[j]);
else
result.push_back(rfirstm[i]);
j++;
i--;
}else if(lfirstm[j]!=-1&&rfirstm[i]==-1){
result.push_back(lfirstm[j]);
j++;
i--;
}
else if(lfirstm[j]==-1&&rfirstm[i]!=-1){
result.push_back(rfirstm[i]);
j++;
i--;
}
else{
result.push_back(-1);
j++;
i--;
}
}
return result;
}
};
对于上面的实现来说,一定更要注意在进行比较的时候,是比较的数组A中的数,并不是rfirstm以及lfirstm中的下标!!!