題目描述:
如何得到一個數據流中的中位數?如果從數據流中讀出奇數個數值,那么中位數就是所有數值排序之后位於中間的數值。如果從數據流中讀出偶數個數值,那么中位數就是所有數值排序之后中間兩個數的平均值。我們使用Insert()方法讀取數據流,使用GetMedian()方法獲取當前讀取數據的中位數。
解題思路:
首先要正確理解此題的含義,數據是從一個數據流中讀出來的,因此數據的數目隨着時間的變化而增加。對於從數據流中讀出來的數據,當然要用一個數據容器來保存,也就是當有新的數據從流中讀出時,需要插入數據容器中進行保存。那么我們需要考慮的主要問題就是選用什么樣的數據結構來保存。
方法一:用數組保存數據。數組是最簡單的數據容器,如果數組沒有排序,在其中找中位數可以使用類比快速排序的partition函數,則插入數據需要的時間復雜度是O(1),找中位數需要的復雜度是O(n)。除此之外,我們還可以想到用直接插入排序的思想,在每到來一個數據時,將其插入到合適的位置,這樣可以使數組有序,這種方法使得插入數據的時間復雜度變為O(n),因為可能導致n個數移動,而排序的數組找中位數很簡單,只需要O(1)的時間復雜度。
方法二:用鏈表保存數據。用排序的鏈表保存從流中的數據,每讀出一個數據,需要O(n)的時間找到其插入的位置,然后可以定義兩個指針指向中間的結點,可以在O(1)的時間內找到中位數,和排序的數組差不多。
方法三:用二叉搜索樹保存數據。在二叉搜索樹種插入一個數據的時間復雜度是O(logn),為了得到中位數,可以在每個結點增加一個表示子樹結點個數的字段,就可以在O(logn)的時間內找到中位數,但是二叉搜索樹極度不平衡時,會退化為鏈表,最差情況仍需要O(n)的復雜度。
方法四:用AVL樹保存數據。由於二叉搜索樹的退化,我們很自然可以想到用AVL樹來克服這個問題,並做一個修改,使平衡因子為左右子樹的結點數之差,則這樣可以在O(logn)的時間復雜度插入數據,並在O(1)的時間內找到中位數,但是問題在於AVL樹的實現比較復雜。
方法五:最大堆和最小堆。我們注意到當數據保存到容器中時,可以分為兩部分,左邊一部分的數據要比右邊一部分的數據小。如下圖所示,P1是左邊最大的數,P2是右邊最小的數,即使左右兩部分數據不是有序的,我們也有一個結論就是:左邊最大的數小於右邊最小的數。

因此,我們可以有如下的思路: 用一個最大堆實現左邊的數據存儲,用一個最小堆實現右邊的數據存儲,向堆中插入一個數據的時間是O(logn),而中位數就是堆頂的數據,只需要O(1)的時間就可得到。
而在具體實現上,首先要保證數據平均分配到兩個堆中,兩個堆中的數據數目之差不超過1,為了實現平均分配,可以在數據的總數目是偶數時,將數據插入最小堆,否則插入最大堆。
此外,還要保證所有最大堆中的數據要小於最小堆中的數據。所以,新傳入的數據要和最大堆中最大值或者最小堆中的最小值比較。當總數目是偶數時,我們會插入最小堆,但是在這之前,我們需要判斷這個數據和最大堆中的最大值哪個更大,如果最大值中的最大值比較大,那么將這個數據插入最大堆,並把最大堆中的最大值彈出插入最小堆。由於最終插入到最小堆的是原最大堆中最大的,所以保證了最小堆中所有的數據都大於最大堆中的數據。
總結:


編程實現(Java):
import java.util.*;
public class Solution {
/*
思路:最大堆和最小堆
*/
PriorityQueue<Integer> minHeap=new PriorityQueue<>();
PriorityQueue<Integer> maxHeap=new PriorityQueue<>(new Comparator<Integer>(){
public int compare(Integer o1,Integer o2){
return o2-o1;
}
});
int count=0;
public void Insert(Integer num) {
count++;
if(count%2==0){ //偶數,插入最小堆
if(!maxHeap.isEmpty() && num<maxHeap.peek()){ //如果num小於最大堆,那么先插入最大堆
maxHeap.add(num);
num=maxHeap.poll();
}
minHeap.add(num);
}else{ //奇數,插入最大堆
if(!minHeap.isEmpty() && num>minHeap.peek()){
minHeap.add(num);
num=minHeap.poll();
}
maxHeap.add(num);
}
}
public Double GetMedian() {
if(minHeap.size()==maxHeap.size())
return (minHeap.peek()+maxHeap.peek())/2.0;
else if(maxHeap.size()>minHeap.size())
return maxHeap.peek()/1.0;
else
return minHeap.peek()/1.0;
}
}
或者可以使用java8引入的lamda表達式進行實現:
import java.util.*;
public class Solution {
private PriorityQueue<Integer> maxHeap=new PriorityQueue<>((a,b)->b-a); //大頂堆用於存放左邊的數據
private PriorityQueue<Integer> minHeap=new PriorityQueue<>(); //最小堆存右邊的數據
private int count=0; //當前數據流的數據個數
public void Insert(Integer num) { //第奇數個插入最大堆,偶數個插入最小堆
count++;
if(count%2==0){ //偶數個,插入最小堆,但是插入最小堆的必須大於左邊所有的,所以必須大於左邊的最大值,否則換一下
if(!maxHeap.isEmpty() && num<maxHeap.peek()){
maxHeap.add(num);
num=maxHeap.poll(); //和左邊最大值交換
}
minHeap.add(num);
}else{ //奇數個,插入最大堆,同理
if(!minHeap.isEmpty() && num>minHeap.peek()){
minHeap.add(num);
num=minHeap.poll();
}
maxHeap.add(num);
}
}
public Double GetMedian() {
if(maxHeap.size()>minHeap.size())
return maxHeap.peek()/1.0;
else if(maxHeap.size()<minHeap.size())
return minHeap.peek()/1.0;
else
return (maxHeap.peek()+minHeap.peek())/2.0;
}
}