算法簡介:
滑動窗口
,顧名思義,就是有一個大小可變的窗口,左右兩端方向一致的向前滑動(右端固定,左端滑動;左端固定,右端滑動)。
可以想象成隊列,一端在push元素,另一端在pop元素,如下所示:
假設有數組[a b c d e f g h]
一個大小爲3的滑動窗口在其上滑動,則有:
[a b c]
[b c d]
[c d e]
[d e f]
[e f g]
[f g h]
使用範圍:
- 字符串或者子數組的問題。
- 求滿足某一條件的子串或者子數組的最大或者最小的長度。
- 使用滑動窗口可以降低算法的時間複雜度。
算法思想:
- 1、在序列中使用雙指針中的左右指針技巧,初始化 left = right = 0,把索引閉區間 [left, right] 稱爲一個窗口。
- 2、先不斷地增加 right 指針擴大窗口 [left, right],直到窗口中的序列符合要求。
- 3、此時,停止增加 right,轉而不斷增加 left 指針縮小窗口 [left, right],直到窗口中的序列不再符合要求。同時,每次增加 left前,都要更新一輪結果。
- 4、重複第 2 和第 3 步,直到 right 到達序列的盡頭。
思路其實很簡單:第 2 步相當於在尋找一個可行解,然後第 3 步在優化這個可行解,最終找到最優解。左右指針輪流前進,窗口大小增增減減,窗口不斷向右滑動。
算法模板:
1、單層循環:
def template():
# 初始化滑動窗口兩端
left = right = 0
# 序列及序列長度
seq, seq_len = xx, xx
# 滑動窗口序列
slide_win = []
# 結果值
rst = xx
while right < seq_len:
slide_win.append(seq[right])
# 還沒找到一個可行解
if not avaliable(slide_win):
# 擴大窗口
right += 1
else:
# 找到一個可行解,更新結果值
rst = update()
# 縮小窗口
left += 1
2、雙層循環:
def template():
# 初始化滑動窗口兩端
left = right = 0
# 序列及序列長度
seq, seq_len = xx, xx
# 滑動窗口序列
slide_win = []
# 結果值
rst = xx
while right < seq_len:
slide_win.append(seq[right])
# 還沒找到一個可行解
if not avaliable(slide_win):
# 擴大窗口
right += 1
continue
# 循環更新可行解
while avaliable(slide_win):
# 找到一個可行解,更新結果值
rst = update()
# 縮小窗口
left += 1
算法實例:
https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/
1、請從字符串中找出一個最長的不包含重複字符的子字符串,計算該最長子字符串的長度。
相信看懂了上面的算法介紹,下面的代碼很容易理解
#include<algorithm>
#include <deque>
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//使用雙端隊列,不能使用hash,因爲這是一個最長子串,保證窗口裏面的元素保持原來的順序
deque<char> need;
size_t max_num = 0;
int index = 0;
while (index < s.size())
{
if (!judge(need, s[index]))
{
need.push_back(s[index]);
index++;
continue;
}
else
{
//更新結果
max_num = max(max_num, need.size());
need.pop_front();
}
}
return max_num = max(max_num, need.size());
}
bool judge(deque<char>& tmp, char str)
{
for (auto &e : tmp)
{
if (e == str)
{
return true;
}
}
return false;
}
};
python版本
from collections import deque
class Solution(object):
def lengthOfLongestSubstring(self, s):
"""
:type s: str
:rtype: int
"""
if not s:
return 0
index = 0
# 因爲這個滑動窗口需要從一端進,一端出,因此考慮採用隊列
slide_win = deque()
# 因爲在滑動過程中需要不斷的從窗口中增減元素,因此需要一個變量來保持最大窗口長度
max_len = 1
while index < len(s):
# 一個小的優化點:還沒有遍歷的元素長度加上當前的窗口長度已經小於最大窗口長度,則直接返回結果
if len(slide_win) + (len(s) - index) <= max_len:
return max_len
# 如果當前元素沒有在滑動窗口中,則加入,並且窗口擴大
if s[index] not in slide_win:
slide_win.append(s[index])
index += 1
else:
# 如果當前元素已經在窗口中有值,則更新最大窗口長度
max_len = max(max_len, len(slide_win))
# 窗口縮小,對端不變
slide_win.popleft()
return max(max_len, len(slide_win))
class Solution {
public:
int longestSubarray(vector<int>& nums, int limit) {
if (nums.size() == 0)
return 0;
int len = nums.size();
int left = 0;
int right = 0;
int max_size = 0;
vector<pair<int, int>> max_heap;
make_heap(max_heap.begin(), max_heap.end(), less<pair<int, int>>());
vector<pair<int, int>> min_heap;
make_heap(min_heap.begin(), min_heap.end(), greater<pair<int, int>>());
while (right < len)
{
max_heap.push_back(pair<int,int> (nums[right], right));
push_heap(max_heap.begin(), max_heap.end(), less<pair<int, int>>());
min_heap.push_back(pair<int, int>(nums[right], right));
push_heap(min_heap.begin(), min_heap.end(), greater<pair<int, int>>());
int diff = max_heap[0].first - min_heap[0].first;
if (diff <= limit)
{
right++;
continue;
}
//最大差值超過設定,更新最大窗口的值
max_size = max(max_size, right-left);//不能使用堆的size,因爲窗口中的不是最優解
//保證滑動窗口中的最大差值在要求的範圍內
while (max_heap[0].first-min_heap[0].first>limit)
{
//去掉大頂堆中所有的在窗口之外的最大元素
while (max_heap[0].second<=left)
{
pop_heap(max_heap.begin(), max_heap.end(), less<pair<int, int>>());
max_heap.pop_back();
}
while (min_heap[0].second<=left)
{
pop_heap(min_heap.begin(), min_heap.end(), greater<pair<int, int>>());
min_heap.pop_back();
}
left++;
}
}
return max(max_size, right - left);
}
};
從上面兩個例子來看,所體現的算法思想不變,都是找到符合條件的窗口,然後去求當前的最優解,實現手段不同,第一個比較簡單,直接利用deque的特性很容易完成。第二個要求複雜了一點, 求相互的差值不超過設定,其實說到相互的差值不超過設定,不就是最大的差值不超過設定嘛,(有時遇到棘手的問題,看看換一種思考能不能更簡單的實現),想到了大小堆,問題就迎刃而解了。
在優化窗口的時候,一般就是左指針的天下了,通過左指針作爲參照或者直接將不滿足左指針條件的窗口中的值刪除。知名的算法思想相同,致命的是細節的把控。
3、子數組查找
鏈接:https://www.nowcoder.com/questionTerminal/4406e29ed847446e9745d75f43fad3fe
來源:牛客網
給定長度爲N的整數數組,以及M個互不相同的整數,請找出包含這M個整數的最短子數組。
例如:給定數組[4 2 1 3],包含數字集{2, 3}的最短子數組是[2 1 3],包含數字集{1, 3}的最短子數組是[1 3]。
下面的代碼直接將代碼複雜度降到O(1)級別,利用hash表優化子窗口求解過程,不過思想理解起來稍顯複雜,不理解的還是乖乖使用deque吧,在時間複雜度要求不是很嚴的情況下。
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
int func(vector<int>& v, vector<int>& u)
{
unordered_map<int, int> need, window;
for (int i : u)
need[i]++;
int l = 0, r = 0;
int valid = 0;
int ans = v.size() + 1;
while (r < v.size())
{
int t = v[r];
r++;
if (need.count(t))
{
window[t]++;
if (need[t] == window[t])
valid++;
}
//找到符合條件的區間,接下來要尋求最優解
while (valid == need.size())
{
ans = min(ans, r - l);
int t = v[l];
l++;
if (need.count(t))//是因爲並不確定他是不是need的最左邊的,用deque就很簡單了
{
if (need[t] == window[t])
valid--;
window[t]--;
}
}
}
return ans == v.size() + 1 ? 0 : ans;
}
int main()
{
int T;
cin >> T;
while (T--)
{
int M, N, t;
cin >> N;
vector<int> v;
while (N--)
{
cin >> t;
v.push_back(t);
}
cin >> M;
vector<int> u;
while (M--)
{
cin >> t;
u.push_back(t);
}
cout << func(v, u) << endl;
}
}