《算法进阶50讲》中位数

Posted 英雄哪里出来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《算法进阶50讲》中位数相关的知识,希望对你有一定的参考价值。

文章目录

前言

一、概念

    什么是中位数?对于一个序列,中位数就是所有数值排序之后位于中间的数值。如果总共有偶数个数,那么中位数就是所有数排序之后中间两个数的平均值。

1、奇数

    如下图所示,这五个数排序以后,位于中的数是 4,所以中位数就是 4。

2、偶数

    对于偶数的情况,排序以后,中间两个数是 4 和 7,所以中位数就是 4 + 7 2 = 5.500000 \\frac 4+7 2 = 5.500000 24+7=5.500000

二、数据流中位数

    前置算法:《画解数据结构》画解二叉堆

1、问题描述

    设计一种容器,支持两种操作:
    void addNum(int num)- 从当前容器中添加一个整数。
    double findMedian()- 返回目前容器的中位数。
最多会对 addNumfindMedian进行 1 0 5 10^5 105 次调用。

2、算法思路

     ( 1 ) (1) (1) 准备两个堆,一个大顶堆,一个小顶堆。小顶堆中所有元素都是大于等于大顶堆的元素的。
     ( 2 ) (2) (2) 如果能够保证两个堆元素的个数始终相差不超过 1,则可以在 O ( 1 ) O(1) O(1) 的时间内快速得到整个序列的中位数;
     ( 3 ) (3) (3) 如果总的元素个数是偶数,则为两个堆顶元素相加之和的平均数;如果总的元素个数是奇数,则为元素多的那个堆的堆顶,举个例子:

小顶堆元素大顶堆元素
9 8 75 4 3 2
9 8 75 4 3
9 8 7 54 3 2

     ( 4 ) (4) (4) 那么,进行元素插入呢?我们发现,小顶堆的元素是数组大的那一半,大顶堆的元素是数组小的那一半,所以我们可以根据它和小顶堆堆顶的元素大小,来选择插入哪个堆。假设插入的数为 x x x,小顶堆堆顶的元素为 t t t,则:
         ( 4.1 ) (4.1) (4.1) x ≥ t x \\ge t xt, 直接插入小顶堆;
         ( 4.2 ) (4.2) (4.2) x < t x < t x<t,直接插入大顶堆;
    插入完毕,有可能导致两个堆的数量差值超过 1,于是需要进行调整操作;
     ( 5 ) (5) (5) 如果 小顶堆数量 < 大顶堆数量 - 1,则弹出大顶堆元素,放入小顶堆;反之,则将小顶堆元素弹出,放入大顶堆;

3、时间复杂度

     ( 1 ) (1) (1) 插入:由于任何时候,在插入之前,两个堆的数据量差值不超过 1,所以插入以后必然不会超过 2,最多进行一次调整,一次调整的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)
     ( 2 ) (2) (2) 查找:由于每次查找就是找两个堆的堆顶,所以时间复杂度为 O ( 1 ) O(1) O(1)

4、源码分析

class MedianFinder 
    priority_queue<int, vector<int>, greater<> > minHeap;
    priority_queue<int, vector<int>, less<> > maxHeap;
public:
    MedianFinder()                              // (1)
        while(!minHeap.empty()) minHeap.pop();
        while(!maxHeap.empty()) maxHeap.pop();
    

    void balanceHeap()                          // (2)
        int minSize = minHeap.size();
        int maxSize = maxHeap.size();
        if(minSize - maxSize < -1) 
            minHeap.push( maxHeap.top() );
            maxHeap.pop();
        else if(minSize - maxSize > 1) 
            maxHeap.push( minHeap.top() );
            minHeap.pop();
        
    

    void addHeap(int num)                       // (3)
        if(minHeap.empty()) 
            minHeap.push(num);
            return ;
        
        if(num >= minHeap.top()) 
            minHeap.push(num);
        else 
            maxHeap.push(num);
        
    
    
    void addNum(int num)                         // (4)
        addHeap(num);
        balanceHeap();
    
    
    double findMedian()                          // (5)
        int minSize = minHeap.size();
        int maxSize = maxHeap.size();
        if(minSize + maxSize == 0) 
            return 0;
        
        if(minSize > maxSize) 
            return minHeap.top();    
        else if(minSize < maxSize) 
            return maxHeap.top();
        
        return ( minHeap.top() + maxHeap.top() ) / 2.0;
    
;

   ( 1 ) (1) (1) 初始化 大顶堆小顶堆
   ( 2 ) (2) (2) void balanceHeap()用于对两个堆进行平衡操作;
   ( 3 ) (3) (3) addHeap(int num)用于根据数据的大小关系,决定插入大顶堆还是小顶堆;
   ( 4 ) (4) (4) 实现插入API(插入 + 平衡);
   ( 5 ) (5) (5) O ( 1 ) O(1) O(1) 的时间复杂度计算中位数;

三、滑动窗口中位数

    前置算法:
         ( 1 ) (1) (1) 哈希表
         ( 2 ) (2) (2) 离散化
         ( 3 ) (3) (3) 二分枚举
         ( 4 ) (4) (4) 前缀和
         ( 5 ) (5) (5) 树状数组

1、问题描述

    给你一个数组 nums,有一个长度为 k k k 的窗口从最左端滑动到最右端。窗口中有 k k k 个数,每次窗口向右移动 1 位。找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。

2、算法思路

     ( 1 ) (1) (1) 我们先把前 k k k 个数丢到一个容器中;
     ( 2 ) (2) (2) 如果 k k k 为奇数,则就是求这个容器的第 k + 1 2 \\frac k+12 2k+1 大的数;如果 k k k 为偶数,则就是求这个容器的第 k 2 \\frac k2 2k 大的数 和 k 2 + 1 \\frac k2+1 2k+1 大的数的平均值;
     ( 3 ) (3) (3) 然后,窗口滑动一格,其实就是从容器中删除一个元素、加入另一个元素。
     ( 4 ) (4) (4) 于是,我们需要提供一种容器,能够支持以下三种操作:
         ( 4.1 ) (4.1) (4.1) 插入;
         ( 4.2 ) (4.2) (4.2) 删除;
         ( 4.3 ) (4.3) (4.3) 查询 k k k 大数;
     ( 5 ) (5) (5) 这个 k k k 大数的容器,我们可以采用树状数组来实现,利用 二分枚举答案 + 树状数组的求和 通过 O ( l o g 2 2 n ) O(log_2^2n) O(log22n) 的时间复杂度快速找到第 k k k 大的数。

3、时间复杂度

     ( 1 ) (1) (1) 插入: O ( l o g 2 n ) O(log_2n) O(log2n)
     ( 2 ) (2) (2) 删除: O ( l o g 2 n ) O(log_2n) O(log2n)
     ( 3 ) (3) (3) k k k 大数: O ( l o g 2 2 n ) O(log_2^2n) O(log22n)

4、源码分析

class Solution 
    vector<int> bin;
    unordered_map<int, int> value2Index;
    int c[300000];

    void discritizen(vector<int>& nums)            // (1)
        int i;
        bin.clear();
        for(i = 0; i < nums.size(); ++i)         
            bin.push_back(nums[i]);
        
        sort(bin.begin(), bin.end());              // (1.1)
        bin.erase( unique(bin.begin(), bin.end()), bin.end() ); // (1.2)

        value2Index.clear();
        for(i = 0; i < bin.size(); ++i)           // (1.3)
            value2Index[ bin[i] ] = i + 1;
        
    

    // 根据 x 的值,返回需要操作数组对应下标
    int getIndex(int x)                           // (2)
        return value2Index[x];
    

    int getValue(int index)                       // (3)
        return bin[ index-1 ];
    

    int getMaxIndex() 
        return bin.size();
    

    int lowbit(int x) 
        return x & -x;
    
    
    void add(int x, int n, int v)                  // (4)
        while(x <= n) 
            c[x] += v;
            x += lowbit(x);
        
    

    int sum(int x)                                 // (5)
        int s = 0;
        while(x) 
            s += c[x];
            x -= lowbit(x);
        
        return s;
    

    int getkth(int K)                              // (6)
        int l = 1, r 以上是关于《算法进阶50讲》中位数的主要内容,如果未能解决你的问题,请参考以下文章

左神算法进阶班1_5BFPRT算法

英雄哪里出来一文带你吃透算法

英雄哪里出来一文带你吃透算法

《算法零基础100讲》(第32讲) 多维枚举 - 进阶

《算法零基础100讲》(第56讲) 哈希表进阶

《算法零基础100讲》(第36讲) 排序进阶 - 归并排序