1. 程式人生 > >[LeetCode] Sliding Window Median 滑動視窗中位數

[LeetCode] Sliding Window Median 滑動視窗中位數

Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.

Examples:

[2,3,4] , the median is 3

[2,3], the median is (2 + 3) / 2 = 2.5

Given an array nums, there is a sliding window of size k

which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Your job is to output the median array for each window in the original array.

For example,
Given nums = [1,3,-1,-3,5,3,6,7]

, and k = 3.

Window position                Median
---------------               -----
[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

Therefore, return the median sliding window as [1,-1,-1,3,5,6].

Note:
You may assume k is always valid, ie: 1 ≤ k ≤ input array's size for non-empty array.

這道題給了我們一個數組,還是滑動視窗的大小,讓我們求滑動視窗的中位數。我想起來之前也有一道滑動視窗的題Sliding Window Maximum,於是想套用那道題的方法,可以用deque怎麼也做不出,因為求中位數並不是像求最大值那樣只操作deque的首尾元素。後來看到了史蒂芬大神的方法,原來是要用一個multiset集合,和一個指向最中間元素的iterator。我們首先將陣列的前k個數組加入集合中,由於multiset自帶排序功能,所以我們通過k/2能快速的找到指向最中間的數字的迭代器mid,如果k為奇數,那麼mid指向的數字就是中位數;如果k為偶數,那麼mid指向的數跟前面那個數求平均值就是中位數。當我們新增新的數字到集合中,multiset會根據新數字的大小加到正確的位置,然後我們看如果這個新加入的數字比之前的mid指向的數小,那麼中位數肯定被拉低了,所以mid往前移動一個,再看如果要刪掉的數小於等於mid指向的數(注意這裡加等號是因為要刪的數可能就是mid指向的數),則mid向後移動一個。然後我們將滑動視窗最左邊的數刪掉,我們不能直接根據值來用erase來刪數字,因為這樣有可能刪掉多個相同的數字,而是應該用lower_bound來找到第一個不小於目標值的數,通過iterator來刪掉確定的一個數字,參見程式碼如下:

解法一:

class Solution {
public:
    vector<double> medianSlidingWindow(vector<int>& nums, int k) {
        vector<double> res;
        multiset<double> ms(nums.begin(), nums.begin() + k);
        auto mid = next(ms.begin(), k /  2);
        for (int i = k; ; ++i) {
            res.push_back((*mid + *prev(mid,  1 - k % 2)) / 2);        
            if (i == nums.size()) return res;
            ms.insert(nums[i]);
            if (nums[i] < *mid) --mid;
            if (nums[i - k] <= *mid) ++mid;
            ms.erase(ms.lower_bound(nums[i - k]));
        }
    }
};

上面的方法用到了很多STL內建的函式,比如next,lower_bound啥的,下面我們來看一種不使用這些函式的解法。這種解法跟Find Median from Data Stream那題的解法很類似,都是維護了small和large兩個堆,分別儲存有序陣列的左半段和右半段的數字,保持small的長度大於等於large的長度。我們開始遍歷陣列nums,如果i>=k,說明此時滑動視窗已經滿k個了,再滑動就要刪掉最左值了,我們分別在small和large中查詢最左值,有的話就刪掉。然後處理增加數字的情況(分兩種情況:1.如果small的長度小於large的長度,再看如果large是空或者新加的數小於等於large的首元素,我們把此數加入small中。否則就把large的首元素移出並加入small中,然後把新數字加入large。2.如果small的長度大於large,再看如果新數字大於small的尾元素,那麼新數字加入large中,否則就把small的尾元素移出並加入large中,把新數字加入small中)。最後我們再計算中位數並加入結果res中,根據k的奇偶性來分別處理,參見程式碼如下:

解法二:

class Solution {
public:
    vector<double> medianSlidingWindow(vector<int>& nums, int k) {
        vector<double> res;
        multiset<int> small, large;
        for (int i = 0; i < nums.size(); ++i) {
            if (i >= k) {
                if (small.count(nums[i - k])) small.erase(small.find(nums[i - k]));
                else if (large.count(nums[i - k])) large.erase(large.find(nums[i - k]));
            }
            if (small.size() <= large.size()) {
                if (large.empty() || nums[i] <= *large.begin()) small.insert(nums[i]);
                else {
                    small.insert(*large.begin());
                    large.erase(large.begin());
                    large.insert(nums[i]);
                }
            } else {
                if (nums[i] >= *small.rbegin()) large.insert(nums[i]);
                else {
                    large.insert(*small.rbegin());
                    small.erase(--small.end());
                    small.insert(nums[i]);
                }
            }
            if (i >= (k - 1)) {
                if (k % 2) res.push_back(*small.rbegin());
                else res.push_back(((double)*small.rbegin() + *large.begin()) / 2);
            }
        }
        return res;
    }
};

類似題目:

參考資料: