问题:
给定一个包含 n 个整数的数组,和一个大小为 k 的滑动窗口,从左到右在数组中滑动这个窗口,找到数组中每个窗口内的中位数。(如果数组个数是偶数,则在该窗口排序数字后,返回第 N/2 个数字。)
对于数组 [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;
}
};