剑指offer 41 数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。

示例 1

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

示例 2

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制

  • 最多会对 addNum、findMedia进行 50000 次调用。

插入排序解法

对于中位数和顺序统计量的问题,算法导论中有专门的一章来讲。对于一个长度为 n 的未排序数组,要查找它的第几大的数,或者中位数。可以基于快速排序中的分治思想实现一种期望时间复杂度为 O ( n ) O(n) O(n) 的算法,但是,这种方法的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)。不过一种更加复杂的方法可以将最坏时间复杂度降至 O ( n ) O(n) O(n)。但是想了想,对于这个题来说,没有想出怎么用这种方式实现一种优于插入排序方法的解法。

但是基于好奇,还是想看下用插入排序的插入过程维护数组,效果怎么样。用插入排序的插入过程维持数组,就是当需要插入新数值时,每次都将其插入到正确的位置上,这样保证整个数组始终是排好序的。这样插入新数的时间复杂度为 O ( n ) O(n) O(n),而返回中位数时,我们只需要根据数组的下标计算即可,所以时间复杂度为 O ( 1 ) O(1) O(1)。下面是具体的Java程序实现,比较简单。

public class MedianFinder {
    int[] nums;
    int size;
    public MedianFinder(){
        nums = new int[50000];
        size = 0;
    }

    public void addNum(int num){
        int ptr = size;
        while(ptr>0 && nums[ptr-1]>num){
            nums[ptr] = nums[ptr-1];
            ptr--;
        }
        nums[ptr] = num;
        size++;
    }

    public double findMedian(){
        if(size==0){
            return 0;
        }
        if(size%2==1){
            return nums[size/2];
        }else{
            return (0.0+nums[size/2]+nums[size/2-1]) / 2.0;
        }

    }
}


/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

提交结果为

执行用时:215 ms, 在所有 Java 提交中击败了9.34%的用户
内存消耗:49.5 MB, 在所有 Java 提交中击败了99.21%的用户

从直觉上判断,这个题时间复杂的极限可能是把这个 O ( n ) O(n) O(n)改为 O ( l o g n ) O(logn) O(logn)

AVL树解法

打开《剑指offer》,才发现这个题可以用AVL树的解法来实现 O ( l o g n ) O(logn) O(logn)。不过,要是用AVL树解的话,可以放弃了。无论是笔试还是面试,应该都是写不完的。因而,这里也就不放代码了,其实我也没去实现。

最大堆,最小堆解法

由于AVL树解法的不现实性,《剑指offer》书中给出了另一种同时使用最大堆和最小堆的方法。这个方法还是颇为巧妙的。让较大的一般放在最大堆中,较小的一半放在最小堆中,这样插入新数字的时间复杂度为 O ( l o n g n ) O(longn) O(longn),获取中位数的时间复杂度为 O ( 1 ) O(1) O(1)。妙呀。

下面是具体的Java程序实现,建议再写的时候把最大堆和最小堆单独拿出来分别作为一个新的类,我这种写法显得类有点大。

public class MedianFinder {
    int size, leftSize, rightSize;
    int[] leftHeap;  // 最大堆,存放较小的一半数据
    int[] rightHeap;  // 最小堆,存放较大的一半数据
    final int MAXLENGTH = 50000;

    public MedianFinder(){
        leftHeap = new int[MAXLENGTH];
        rightHeap = new int[MAXLENGTH];
        size = 0;
        leftSize = 0;
        rightSize = 0;
    }

    public void addNum(int num){
        if(size==0){
            leftHeap[0] = num;
            size = 1;
            leftSize = 1;
            return;
        }

        if(num>leftHeap[0]){
            // 将其放入最小堆
            rightHeap[rightSize] = num;
            rightSize++;
            keepMinHeap();
        }else{
            // 将其放入最大堆
            leftHeap[leftSize] = num;
            leftSize++;
            keepMaxHeap();
        }
        size = leftSize + rightSize;
        keepMean();
    }

    public double findMedian(){
        if(size==0){
            return -1;
        }
        if(size%2==1){
            return leftHeap[0];
        }else{
            return (0.0+leftHeap[0]+rightHeap[0]) / 2.0;
        }

    }

    // 均衡最小堆与最大堆之间的元素个数,使最大堆中元素个数比最小堆多1,或相等
    private void keepMean(){
        if(leftSize==rightSize || leftSize-rightSize==1){
            return;
        }

        if(leftSize>rightSize){
            // 需要从最大堆中取出最大值,并放到最小堆中
            int temp = peekMaxHeap();
            rightHeap[rightSize] = temp;
            rightSize++;
            keepMinHeap();
        }else{
            // 需要从最小堆中取出最小值,并放到最大堆中
            int temp = peekMinHeap();
            leftHeap[leftSize] = temp;
            leftSize++;
            keepMaxHeap();
        }
    }

    // 从最小堆中取出堆顶元素
    private int peekMinHeap(){
        if(rightSize==0){
            return -1;
        }
        if(rightSize==1){
            rightSize = 0;
            return rightHeap[0];
        }
        int res = rightHeap[0];

        rightHeap[0] = rightHeap[rightSize-1];
        rightSize--;
        int current = 0;
        int leftChild = leftChildIndex(current);
        int rightChild = rightChildIndex(current);
        while(leftChild<rightSize){
            int min = current;
            if(rightHeap[leftChild]<rightHeap[min]){
                min = leftChild;
            }
            if(rightChild<rightSize && rightHeap[rightChild]<rightHeap[min]){
                min = rightChild;
            }
            if(min==leftChild){
                int temp = rightHeap[current];
                rightHeap[current] = rightHeap[leftChild];
                rightHeap[leftChild] = temp;
                current = leftChild;
            }else if(min==rightChild){
                int temp = rightHeap[current];
                rightHeap[current] = rightHeap[rightChild];
                rightHeap[rightChild] = temp;
                current = rightChild;
            }else{
                break;
            }

            leftChild = leftChildIndex(current);
            rightChild = rightChildIndex(current);
        }

        return res;
    }

    // 从最大堆中取出顶元素
    private int peekMaxHeap(){
        if(leftSize==0){
            return -1;
        }
        if(leftSize==1){
            leftSize = 0;
            return leftHeap[0];
        }

        int res = leftHeap[0];
        leftHeap[0] = leftHeap[leftSize-1];
        leftSize--;

        int current = 0;
        int leftChild = leftChildIndex(current);
        int rightChild = rightChildIndex(current);
        while(leftChild<leftSize){
            int max = current;
            if(leftHeap[leftChild]>leftHeap[max]){
                max = leftChild;
            }
            if(rightChild<leftSize && leftHeap[rightChild]>leftHeap[max]){
                max = rightChild;
            }

            if(max==leftChild){
                int temp = leftHeap[current];
                leftHeap[current] = leftHeap[leftChild];
                leftHeap[leftChild] = temp;
                current = leftChild;
            }else if(max==rightChild){
                int temp = leftHeap[current];
                leftHeap[current] = leftHeap[rightChild];
                leftHeap[rightChild] = temp;
                current = rightChild;
            }else{
                break;
            }

            leftChild = leftChildIndex(current);
            rightChild = rightChildIndex(current);
        }

        return res;
    }

    // 往最小堆中插入元素之后,调整最小堆
    private void keepMinHeap(){
        if(rightSize<=1){
            return;
        }

        int current = rightSize-1;
        while(current>0){
            int parent = parentIndex(current);
            if(rightHeap[current]<rightHeap[parent]){
                int temp = rightHeap[current];
                rightHeap[current] = rightHeap[parent];
                rightHeap[parent] = temp;
                current = parent;
            }else{
                break;
            }
        }
    }

    // 往最大堆中插入元素之后,调整最大堆
    private void keepMaxHeap(){
        if(leftSize<2){
            return;
        }

        int current = leftSize - 1;
        while(current>0){
            int parent = parentIndex(current);
            if(leftHeap[current]>leftHeap[parent]){
                int temp = leftHeap[current];
                leftHeap[current] = leftHeap[parent];
                leftHeap[parent] = temp;
                current = parent;
            }else{
                break;
            }
        }
    }

    // 计算堆中某个位置的父节点索引
    private int parentIndex(int current){
        return (current-1)/2;
    }

    private int leftChildIndex(int current){
        return current*2+1;
    }

    private int rightChildIndex(int current){
        return current*2+2;
    }
}

在 LeetCode 系统中提交的结果为:

执行用时:67 ms, 在所有 Java 提交中击败了99.41%的用户
内存消耗:50.9 MB, 在所有 Java 提交中击败了81.18%的用户

当数据具有一定规模时,堆的方法明显要比插入排序的方法快很多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值