增量查找中位数

数字是不断进入数组的,在每次添加一个新的数进入数组的同时返回当前新数组的中位数。

样例
持续进入数组的数的列表为:[1, 2, 3, 4, 5],则返回[1, 1, 2, 2, 3]
持续进入数组的数的列表为:[4, 5, 1, 3, 2, 6, 0],则返回 [4, 4, 4, 3, 3, 3, 3]
持续进入数组的数的列表为:[2, 20, 100],则返回[2, 2, 20]

说明
中位数的定义:
中位数是排序后数组的中间值,如果有数组中有n个数,则中位数为A[(n-1)/2]。
比如:数组A=[1,2,3]的中位数是2,数组A=[1,19]的中位数是1。


很好的一道题目。最简单的办法当然是采用插入排序的思想,从下标0开始每插入排序一个数后就能直接找到相应的中位数,时间开销主要在于排序。时间复杂度O(n^2)。

事实上我们只是求中位数,没必要这么奢侈到把整个数组全部排序一遍。

在下面的算法中,我们采用两个优先队列(其实就是STL封装好了的堆),大根堆minQ保存前k个最小的数,小根堆maxQ用于保存前n-k个最大的数。每次迭代后minQ的size为

中位数之前(包括中位数)的个数。当遍历到新的数时,检查之前的中位数是否还有效。有两种情况,第一种情况是中位数之前的个数增加了,这种情况又分(1)若新的数比之前的中位数(即minQ.top())还小, 则新的中位数不变;(2)否则从比之前的中位数要大的数中(现在这些数也包括新的数)取出最小的一个数,这个最小的数为之前的中位数的后继,那么新的中位数就是这个后继。  另一种情况是中位数之前的个数不变,这就要检查当前中位数是否需要更换,因为新的数可能比之前的中位数小,这样新的中位数就是新的数。


堆的每次push和pop都是O(log n)时间复杂度, 所以算法时间复杂度为O(n log n);

class Solution {
public:
    /**
     * @param nums: A list of integers.
     * @return: The median of numbers
     */
    class More{
    public:
        bool operator()(int x, int y)
        {
            return x > y ? true : false;
        }
    };
    
    vector<int> medianII(vector<int> &nums) {
        // write your code here
        vector<int> res;
        if ( nums.empty() ) return res;
        priority_queue<int> minQ;  //max-heap, save top k min, root is the kth min num
        priority_queue<int, vector<int>, More> maxQ;   //min-heap, save top k max, root is the kth max num
        minQ.push(nums[0]);
        res.push_back(nums[0]);
        
        for ( int i = 1; i < nums.size(); ++i )
        {
            int k = i/2 + 1;
            int median = minQ.top();
            //need insert a new element into minQ
            if ( minQ.size() < k )
            {
                if ( nums[i] <= median ) minQ.push(nums[i]);
                else
                {
                    maxQ.push(nums[i]);
                    median = maxQ.top(); //the min of maxQ, which is no-less than minQ.top()
                    maxQ.pop();
                    minQ.push(median); // now root of minQ is median
                }
            }
            else
            {
                //check if need replace the root of minQ with nums[i]
                if ( nums[i] >= minQ.top() ) maxQ.push(nums[i]);
                else
                {   //need replace
                    maxQ.push(minQ.top());
                    minQ.pop();
                    minQ.push(nums[i]);
                    median = minQ.top(); //attention: nums[i] may NOT be the max in minQ!
                }
            }
            
            res.push_back(median);
        }
        return res;
    }
};


再看一个变式


给定一个包含 n 个整数的数组,和一个大小为 k 的滑动窗口,从左到右在数组中滑动这个窗口,找到数组中每个窗口内的最大值。(如果数组中有偶数,则在该窗口存储该数字后,返回第 N/2-th 个数字。)

样例
对于数组 [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;


这个与上面有点不一样,上面的可以看成是窗口不断加1,而这个滑动窗口每次同时减少一个数和增加一个数,如果还用两个堆来做,则每次迭代需要查找滑过的元素在堆中的位置并删除,这个操作是O(n)复杂度的,显然不可行。用堆可以做的,同样用搜索树也可以,只不过搜索树开销略高一点,但时间复杂度都是同阶,开销差别不是很大。而堆做不了的事情则可以考虑用搜索树能否做。 在这里用搜索树是可行的,因为搜索树(平衡搜索树)查找并删除一个元素是O(log n)复杂度。STL采用平衡搜索树的容器有map( multimap),  set (multiset)。 这里因为元素有重复,可以考虑用两个multiset,但效率会差点。更好的做法是用两个map,key为元素值,value为该元素出现的次数,当次数减为0时就删除key。对于存在大量重复元素的情况,用两个map的效率要高的多。


class Solution {
public:
    /**
     * @param nums: A list of integers.
     * @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 ( k == 0 ) return res;
        map<int, int> leftSet, rightSet;
        map<int, int>::iterator it;
        int m = (k-1)/2 + 1;
        for ( int i = 0; i < k; ++i )
        {
            ++leftSet[nums[i]];
            if ( i >= m )
            {
                it = leftSet.end();
                --it;
                ++rightSet[it->first];
                --(it->second);
                if ( it->second == 0 ) leftSet.erase(it);
            }
        }
        
        it = leftSet.end();
        --it;
        res.push_back(it->first);
        for ( int i = k; i < nums.size(); ++i )
        {
            it = leftSet.find(nums[i-k]);
            if ( it != leftSet.end() )
            {
                --(it->second);
                if ( it->second == 0 ) leftSet.erase(it);
                ++rightSet[nums[i]];
                it = rightSet.begin();
                ++leftSet[it->first];
                --(it->second);
                if ( it->second == 0 ) rightSet.erase(it);
            }
            else
            {
                it = rightSet.find(nums[i-k]);
                --(it->second);
                if ( it->second == 0 ) rightSet.erase(it);
                it = leftSet.end();  --it;
                if ( nums[i] < it->first )
                {
                    ++rightSet[it->first];
                    --(it->second);
                    if ( it->second == 0 ) leftSet.erase(it);
                    ++leftSet[nums[i]];
                }
                else ++rightSet[nums[i]];
            }
            it = leftSet.end();
            --it;
            res.push_back(it->first);
        }
        
        return res;
    }
};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值