Sliding Window
Sliding Window 跟上篇 Multiple Pointers 類似,定義兩個指標,一個是 start,一個是 end。
像是:
| start | end | |||||
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
start 與 end 根據題目所需來移動,這兩個指標所圍出的範圍可大可小,類似像可滑動的兩片窗戶一樣。
在追蹤陣列或字串的子集 (subset) 或子序列 (subsequence) 上非常實用。
Subarrays & Subsequence & Subsets
舉一個最簡單的例子:
const array = [1, 2, 3, 4, 5]
let subarray = [1, 2, 3]
subarray = [2, 3]
subarray = [3, 4, 5]
subarray = [1]
Subarrays 必須要照原本陣列的元素排列順序與連續
const array = [1, 2, 3, 4, 5]
let subsequence = [1, 3, 5]
subsequence = [2, 4]
subsequence = [1, 2, 4, 5]
subsequence = [1]
Subsequence 只要照原本陣列的元素排列順序
const array = [1, 2, 3, 4, 5]
let subset = [5, 4, 1]
subset = [2, 3, 1]
subset = [5, 2, 3]
subset = [1]
Subsets 只要元素有包含在原本陣列即可,不用照順序與連續
Practice 1 - maxSubArraySum
給定一個數字陣列與正整數 n,請找出陣列中的最大子序列和,該子序列的長度必須等於 n。
這邊說要找子序列,所以是要相鄰的元素。
例如:[1, 3, 5, 7, 9, 2, 4, 6, 8, 10],n = 3,最大子序列 ([6, 8, 10]) 和為 24。
例如:[1],n = 2,回傳 null。
較差的解法:
function maxSubArraySum(arr, n){
if (arr.length < n) return null;
let maxSum = -Infinity;
for(let i = 0; i < arr.length - n + 1; i++){
let tempSum = 0;
for(let j = 0; j < n; j++){
tempSum += arr[i + j];
}
if(tempSum > maxSum){
maxSum = tempSum;
}
}
return maxSum;
}
以上有巢狀迴圈,所以在 arr.length 與 n 越來越大的情況下會跑 n * arr.length 次,時間複雜度會是 O(n²) 。
使用 Sliding Window 技巧:
function maxSubArraySum(arr, n) {
if (arr.length < n) return null;
let maxSum = 0;
let tempSum = 0;
for (let i = 0; i < n; i++) {
maxSum += arr[i];
}
tempSum = maxSum;
for (let i = n; i < arr.length; i++) {
tempSum = tempSum - arr[i - n] + arr[i];
maxSum = Math.max(maxSum, tempSum);
}
return maxSum;
// 以範例 [1, 3, 5, 7, 9, 2, 4, 6, 8, 10],n = 3 為例:
// 初始: [1, 3, 5]
// 移到: [3, 5, 7]
// 實際上只需減掉 1,與加上 7,不用每次都遞迴相加一次。
// 換句話說就是每次減掉第一個元素,加上下一個元素。
// 移到: [5, 7, 9]
// 實際上只需減掉 3,與加上 9,不用每次都遞迴相加一次。
// 以此類推
}
以上迴圈是平行的,在 arr.length 與 n 越來越大的情況下頂多跑 n + arr.length 次,時間複雜度會是 O(n) 。
Practice 2 - findLongestSubstring
給定一個字串,請找出其中最長且不重複字母的子字串。
- 字串只有空字串或小寫英文字母的字串,無其他標點符號或語言。
- 空字串回傳
0。
例如:"abcabcbb",回傳 3,因為 "abc" 是最長的不重複字母的子字串。
例如:"bbbbb",回傳 1,因為 "b" 是最長的不重複字母的子字串。
Solution
以 'abccba' 為例:
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
檢查目前 end 指到的字元 a 有沒有出現過,
沒有的話將目前 end 指到的字元 a 用 Frequency Counter 技巧記錄到物件內,紀錄 a 的 index ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
b 也沒出現過重複上述動作。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
c 也沒出現過重複上述動作。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
c 有出現過了,上次出現的位置是 2 ,
所以只好移動 start 到上次出現的位置的下一格,以排除之前出現過的字元。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
紀錄 c 的新出現過的位置 index 等於 3 ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a |
b 雖然有出現過,但上次出現的位置是 1 ,目前 start 的位置是 3 ,已經排除該字元了,所以不用變更 start 的位置。
紀錄 b 的新出現過的位置 index 等於 4 ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
以此類推直到 end 移到陣列最後一個位置。
function findLongestSubstring(str) {
// 字串小於等於 1 個字元,直接回傳字串長度;
if (str.length <= 1) return str.length;
// 此解法也有用到 Frequency Counter 技巧來紀錄每個字元最後出現的位置。
let longest = 0;
let seen = {};
let start = 0;
for (let i = 0; i < str.length; i++) {
let char = str[i];
// 檢查有無出現過且出現過的位置比 start 還後面,則移動 start 到上次出現過的位置的後一格
if (Object.prototype.hasOwnProperty.call(seen, char) && seen[char] >= start) {
start = seen[char] + 1;
}
// 將目前的字串長度跟之前的最長字串長度比較,取最長的。
longest = Math.max(longest, i - start + 1);
// 更新目前字元出現的位置。
seen[char] = i;
}
return longest;
}