【刷穿 LeetCode】480. 滑動視窗中位數(困難)
技術標籤:LeetCode 題解演算法與資料結構刷穿 LeetCode演算法java資料結構面試
點選 這裡 可以檢視更多演算法面試相關內容~
題目描述
中位數是有序序列最中間的那個數。
如果序列的長度是偶數,則沒有最中間的數;此時中位數是最中間的兩個數的平均數。
例如:
[2,3,4]
,中位數是3
[2,3]
,中位數是(2 + 3) / 2 = 2.5
給你一個數組 nums,有一個長度為 k 的視窗從最左端滑動到最右端。
視窗中有 k 個數,每次視窗向右移動 1 位。
你的任務是找出每次視窗移動後得到的新視窗中元素的中位數,並輸出由它們組成的陣列。
示例:
給出 nums = [1,3,-1,-3,5,3,6,7]
視窗位置 中位數
--------------- -----
[1 3 -1] -3 5 3 6 7 1
1 [3 -1 -3] 5 3 6 7 -1
1 3 [-1 -3 5] 3 6 7 -1
1 3 -1 [-3 5 3] 6 7 3
1 3 -1 -3 [5 3 6] 7 5
1 3 -1 -3 5 [3 6 7] 6
提示:
- 你可以假設 k 始終有效,即:k 始終小於輸入的非空陣列的元素個數。
- 與真實值誤差在 1 0 − 5 10 ^ {-5} 10−5 以內的答案將被視作正確答案。
樸素解法
一個直觀的做法是:對每個滑動視窗的數進行排序,獲取排序好的陣列中的第 k / 2
和 (k - 1) / 2
個數(避免奇偶數討論),計算中位數。
我們大概分析就知道這個做法至少 O ( n ∗ k ) O(n * k) O(n∗k) 的,算上排序的話應該是 O ( n ∗ ( k + k log k ) ) O(n * (k + k\log{k})) O(n∗(k+klogk))。
比較無奈的是,這道題不太正規,沒有給出資料範圍。我們無法根據判斷這樣的做法會不會超時。
PS. 實際上這道題樸素解法是可以過的,有藍橋杯內味了 ~
樸素做法通常是優化的開始,所以我還是提供一下樸素做法的程式碼:
class Solution {
public double[] medianSlidingWindow(int[] nums, int k) {
int n = nums.length;
int cnt = n - k + 1;
double[] ans = new double[cnt];
int[] tmp = new int[k];
for (int l = 0, r = l + k - 1; r < n; l++, r++) {
for (int i = l; i <= r; i++) tmp[i - l] = nums[i];
Arrays.sort(tmp);
ans[l] = (tmp[k / 2] / 2.0) + (tmp[(k - 1) / 2] / 2.0);
}
return ans;
}
}
- 時間複雜度:最多有
n
個視窗需要滑動計算。每個視窗,需要先插入資料,複雜度為 O ( k ) O(k) O(k),插入後需要排序,複雜度為 O ( k log k ) O(k\log{k}) O(klogk)。整體複雜度為 O ( n ∗ ( k + k log k ) ) O(n * (k + k\log{k})) O(n∗(k+klogk))。 - 空間複雜度:使用了長度為
k
的臨時陣列。複雜度為 O ( k ) O(k) O(k)。
優先佇列(堆)解法
從樸素解法中我們可以發現,其實我們需要的就是滑動視窗中的第 k / 2
小的值和第 (k - 1) / 2
小的值。
我們知道滑動視窗求最值的問題,可以使用優先佇列來做。
但這裡我們求的是第 k
小的數,而且是需要兩個值。還能不能使用優先佇列來做呢?
我們可以維護兩個堆:
- 一個大根堆維護著滑動視窗中一半較小的值(此時堆頂元素為滑動視窗中的第
(k - 1) / 2
小的值) - 一個小根堆維護著滑動視窗中一半較大的值(此時堆頂元素為滑動視窗中的第
k / 2
小的值)
滑動視窗的中位數就是兩個堆的堆頂元素的平均值。
實現細節:
-
初始化時,先讓
k
個元素直接入right
,再從right
中倒出k / 2
個到left
中。這時候可以根據left
和right
得到第一個滑動視窗的中位值。 -
開始滑動視窗,每次滑動都有一個待新增和待移除的數:
2.1 根據與右堆的堆頂元素比較,決定是插入哪個堆和從哪個堆移除
2.2 之後調整兩堆的大小(確保只會出現
left.size() == right.size()
或right.size() - left.size() == 1
,對應了視窗長度為偶數或者奇數的情況)2.3 根據
left
堆 和right
堆得到當前滑動視窗的中位值
程式碼:
class Solution {
public double[] medianSlidingWindow(int[] nums, int k) {
int n = nums.length;
int cnt = n - k + 1;
double[] ans = new double[cnt];
// 如果是奇數滑動視窗,讓 right 的數量比 left 多一個
// 1.滑動視窗的左半部分
PriorityQueue<Integer> left = new PriorityQueue<>((a,b)->Integer.compare(b,a));
// 2.滑動視窗的右半部分
PriorityQueue<Integer> right = new PriorityQueue<>((a,b)->Integer.compare(a,b));
for (int i = 0; i < k; i++) right.add(nums[i]);
for (int i = 0; i < k / 2; i++) left.add(right.poll());
ans[0] = getMid(left, right);
for (int i = k; i < n; i++) {
// 人為確保了 right 會比 left 多
// 因此,刪除和新增都與 right 比較(left 可能為空)
int add = nums[i], del = nums[i - k];
if (add >= right.peek()) {
right.add(add);
} else {
left.add(add);
}
if (del >= right.peek()) {
right.remove(del);
} else {
left.remove(del);
}
adjust(left, right);
ans[i - k + 1] = getMid(left, right);
}
return ans;
}
void adjust(PriorityQueue<Integer> left,
PriorityQueue<Integer> right) {
while (left.size() > right.size())
right.add(left.poll());
while (right.size() - left.size() > 1)
left.add(right.poll());
}
double getMid(PriorityQueue<Integer> left,
PriorityQueue<Integer> right) {
if (left.size() == right.size()) {
return (left.peek() / 2.0) + (right.peek() / 2.0);
} else {
return right.peek() * 1.0;
}
}
}
- 時間複雜度:調整過程中堆大小最大為
k
,因此堆操作複雜度為 O ( log k ) O(\log{k}) O(logk);視窗數量最多為n
。整體複雜度為 O ( n ∗ log k ) O(n * \log{k}) O(n∗logk)。 - 空間複雜度:最多有
n
個元素在堆內。複雜度為 O ( n ) O(n) O(n)。
最後
這是我們「刷穿 LeetCode」系列文章的第 No.*
篇,系列開始於 2021/01/01,截止於起始日 LeetCode 上共有 1916 道題目,部分是有鎖題,我們將先將所有不帶鎖的題目刷完。
在這個系列文章裡面,除了講解解題思路以外,還會盡可能給出最為簡潔的程式碼。如果涉及通解還會相應的程式碼模板。
由於 LeetCode 的題目隨著周賽 & 雙週賽不斷增加,為了方便我們統計進度,我們將按照系列起始時的總題數作為分母,完成的題目作為分子,進行進度計算。當前進度為 */1916
。
為了方便各位同學能夠電腦上進行除錯和提交程式碼,我建立了相關的倉庫:Github 地址 & Gitee 地址。
在倉庫地址裡,你可以看到系列文章的題解連結、系列文章的相應程式碼、LeetCode 原題連結和一些其他的優選題解。
#演算法與資料結構
#LeetCode題解
#演算法面試