小探索--LintCode 滑動視窗的中位數
對於陣列 [1,2,7,8,5]
, 滑動大小 k = 3 的視窗時,返回 [2,7,7]
最初,視窗的陣列是這樣的:
[ | 1,2,7 | ,8,5]
, 返回中位數 2
;
接著,視窗繼續向前滑動一次。
[1, | 2,7,8 | ,5]
, 返回中位數 7
;
接著,視窗繼續向前滑動一次。
[1,2, | 7,8,5 | ]
, 返回中位數 7
;
本次實驗用C++程式碼實現。基本上是一個循序漸進的過程,剛開始用氣泡排序法處理資料,對於大量資料就力不從心,時間複雜度過高。改用堆排序法有改善,但是演算法不夠完善,每次重新建立新的視窗資料,就浪費時間。查閱網上的資料,用multiset(多重集合)來排序,就能夠解決問題。每次建立視窗資料,改用添加當前視窗的尾元素,並刪除上一個視窗的頭元素,節省了一定的時間。
一、氣泡排序法
在取出視窗陣列後,就取(length+1)/2次最小值,得出中位數。但是由於演算法不適用於大量資料,所以實驗資料只通過了%77,沒有取得成功。此時時間複雜度為O(n)=n*n/2
二、堆排序法class Solution { public: /* * @param : A list of integers * @param : An integer * @return: The median of the element inside the window at each moving */ vector<int> medianSlidingWindow(vector<int> nums, int k) { // write your code here vector<int> numWin, resNum; int i, j, x; i = 0; int minIndex, min; while (i+k-1 < nums.size()) { // i次視窗滑動 numWin.clear(); for (j = 0; j < k; j++) { // 取出視窗資料 numWin.push_back(nums[i+j]); } //冒泡取中位數 for (j = 0; j < (k+1)/2; j++) { minIndex = 0; min = numWin[minIndex]; for (x = 0; x < numWin.size(); x++) { if (numWin[minIndex] > numWin[x]) { minIndex = x; } } min = numWin[minIndex]; numWin.erase(numWin.begin() + minIndex); } resNum.push_back(min); i++; } return resNum; } }
之前的氣泡排序法處理大量資料時,在時間上花費很大。在網上查過資料,考慮用堆排序法來取一個數組的中位數。
堆的性質
堆實際上是一棵完全二叉樹,其任何一非葉節點滿足性質:
Key[i]的左孩子是Key[2i+1],右孩子是Key[2i+2]。保證節點比他的兩個子節點都小(或大).。
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非葉節點的關鍵字不大於或者不小於其左右孩子節點的關鍵字。
堆分為大頂堆和小頂堆,滿足Key[i]>=Key[2i+1]&&key>=key[2i+2]稱為大頂堆,滿足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]稱為小頂堆。由上述性質可知大頂堆的堆頂的關鍵字肯定是所有關鍵字中最大的,小頂堆的堆頂的關鍵字是所有關鍵字中最小的。
堆排序演算法大概就是有兩個過程:將一個無序陣列初始化為一個大頂堆,取出堆頂元素,對剩餘繼續排序。依次取出堆頂的元素,就是一個遞減的序列。
堆的排序,拿節點與左右孩子比較大小,若堆頂元素不是最大的,則與最大的子節點對換,同時對子節點下面的堆進行排序。
//對一個堆進行排序,堆頂元素為a[i],堆的大小為length
//此排序使用遞迴呼叫,對一個堆下面的子堆都進行排序,使其滿足堆的性質
void heapAdjust(vector<int> &a, int i, int length) {
int lchild, rchild, max;
lchild = 2 * i + 1;
rchild = 2 * i + 2;
max = i;
if(i <= length/2) {
if ((lchild < length) && (a[max] < a[lchild])) {
max = lchild;
}
if ((rchild < length) && (a[max] < a[rchild])) {
max = rchild;
}
if (max != i) {
swap(a[i], a[max]);
heapAdjust(a, max, length);
}
}
}
堆的初始化,是從堆的最下面一排進行排序,即從a[length/2]處開始排序,依次往前向上排序,最終初始化一個堆。
//將一個無序的陣列,初始化成堆
void heapBuild(vector<int> &a, int length) {
int i;
for (i = length / 2; i >= 0; i--) {
heapAdjust(a, i, length);
}
}
取堆頂元素,繼續排序。
//排序,依次取堆頂元素與最後一個元素互換,對剩餘的堆(不算最後一個元素)繼續排序
void heapSort(vector<int> &a, int length) {
int i;
heapBuild(a, length);
for (i = length; i > 0; i--) {
swap(a[0], a[i-1]);
heapAdjust(a, 0, i-1);
}
}
//只排序出前一半的元素,找出中位數就終止
int midHeapSort(vector<int> &a, int length) {
int i;
heapBuild(a, length);
for(i = length; i > (length+1)/2; i--) {
swap(a[0], a[i-1]);
heapAdjust(a, 0, i-1);
}
return a[0];
}
最終程式碼如下,可是實驗資料僅通過了%91,還是沒有通過。總結以後,應該是每次都重新取新的視窗資料,導致有一個大的時間複雜度。
class Solution {
public:
/*
* @param : A list of integers
* @param : An integer
* @return: The median of the element inside the window at each moving
*/
vector<int> medianSlidingWindow(vector<int> nums, int k) {
// write your code here
vector<int> numWin, resNum;
int i, j;
i = 0;
while (i+k-1 < nums.size()) { // i次視窗滑動
numWin.clear();
for (j = 0; j < k; j++) { // 取出視窗資料
numWin.push_back(nums[i+j]);
}
resNum.push_back(midHeapSort(numWin, numWin.size()));
i++;
}
return resNum;
}
void heapAdjust(vector<int> &a, int i, int length) {
int lchild, rchild, max;
lchild = 2 * i + 1;
rchild = 2 * i + 2;
max = i;
if(i <= length/2) {
if ((lchild < length) && (a[max] < a[lchild])) {
max = lchild;
}
if ((rchild < length) && (a[max] < a[rchild])) {
max = rchild;
}
if (max != i) {
swap(a[i], a[max]);
heapAdjust(a, max, length);
}
}
}
void heapBuild(vector<int> &a, int length) {
int i;
for (i = length / 2; i >= 0; i--) {
heapAdjust(a, i, length);
}
}
int midHeapSort(vector<int> &a, int length) {
int i;
heapBuild(a, length);
for(i = length; i > (length+1)/2; i--) {
swap(a[0], a[i-1]);
heapAdjust(a, 0, i-1);
}
return a[0];
}
};
三、利用multiset模板排序
在網上查閱關於這個問題的資料,看到這篇文章介紹了利用模板multiset來排序。
multiset的性質
資料存放到multiset中就已經自動進行過排序了。所以只需要直接取multiset的最中間的元素就可以了。
關鍵是當視窗滑動時,需要將下一個元素插入,並將上一次的視窗第一個元素清除。
對於固定的視窗multiset,中位數固定在一個位置,只有當中位數的前面插入或刪除元素,才會影響到中位數的位置。
直接將下一個元素(nums[i])插入到multiset中,然後比較新元素與中位數,若新元素比較小,則mid前移一個單元;準備刪除上一個視窗的第一個元素(舊元素),先比較舊元素與中位數比較,若舊元素比較小,則mid後移一個單元。最後刪除舊元素,mid指向視窗的中位數。
程式碼如下,經過測試,順利通過%100的實驗資料。
class Solution {
public:
/*
* @param : A list of integers
* @param : An integer
* @return: The median of the element inside the window at each moving
*/
vector<int> medianSlidingWindow(vector<int> nums, int k) {
// write your code here
vector<int> res;
if (nums.empty()) {
return res;
}
//初始化multiset,將前k個元素放入ms中
multiset<int> ms(nums.begin(), nums.begin() + k);
//定義中位數的迭代器
multiset<int>::iterator mid = next(ms.begin(), k / 2);
for (int i = k; ; ++i) {
if(k%2) {
res.push_back(*mid);
}
else {
res.push_back(*prev(mid, 1));
}
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]));
}
return res;
}
};