題外話:
前段時間參加校園招聘,經常在一些公司的筆試或者面試中遇到一些不錯的算法題,回到宿舍和同學進行交流,收獲許多。這段時間,工作定下來后,整天閑着沒事,就整理之前一些不錯的算法題及其想法。下面這個算法題是一個同學去參加百度校園招聘面試時遇到的題目,當時他寫了一篇日志。看到他那篇日志,我和舍友小平同學討論了兩三個小時。下面對當時的想法進行一些整理。
問題:
給定n個int型的數和一個空的集合,每次往集合中插入一個數,每次插入之后給出這個集合的中位數。(中位數的概念是:如果集合有奇數個數,給出排序后處在最中間的那個數;如果是偶數個數,給出排序后最中間兩個數的均值。)
分析:
該同學在日記里寫到,他看到題目時想到的是O(N*N)的算法,但是沒有說具體的算法步驟和思想。我猜想,他可能是想在插入時是采用插入排序的思想,則在集合中插入一個數的復雜度為O(N),從排好序的集合中選出中位數則是一個復雜度為O(1)的過程,總共插入N次,所以總復雜度為O(N*N)。
小平和我在看到題目時,第一時間想到的都是“二分”。在每次將數插入集合時,采用“二分查找”尋找該數在集合中的位置,這個過程的復雜度是O(log(N)),得出中位數的過程則是O(1),插入了N次,所以復雜度為O(N*log(N))。
關於二分插入的思想,上面的復雜度分析好像毋庸置疑。但是,上面的分析過程是不對的。采用二分查找,則集合必須能隨機訪問,而這只能使用數組來實現。然而,在數組插入一個數,則需要將該位置后面的所有元素向后移動。上面的思想中,在二分查找到一個數的位置,並將該數插入集合中,則需要后移該位置后的所有元素。在最壞情況下,每次插入的位置都是第一個,則“移動元素”操作的復雜度為O(N),那么該思想的復雜度仍然是O(N*N)。
有個同學在該日記的回復給了我思路,該同學的回復是:
“感覺用堆也是可以的,所有比中位數大的組成一個最小堆,比中位數小的組成一個最大堆,每次插入只進行一個堆的插入,然后中位數一定是原中位數、最大堆的最大值和最小堆最小值中產生,再進行堆的調整就可以了,不過復雜度也是NlogN”
剛開始,小平和我對這個回復進行了一下討論,但是沒討論出一個結果。但是,這個“最大堆和最小堆”卻一直縈繞在我的潛意識中。躺在床上,准備睡覺的時候,恍然大悟:可以使用兩個變量來控制最大堆和最小堆中元素的個數差。
具體的思路如下:
集合中元素,前一半存儲在一個最大堆中,后一半存儲在一個最小堆中。使用變量MaxHeapNum記錄最大堆元素的個數,使用變量MinHeapNum記錄最小堆元素的個數。控制MaxHeapNum與MinHeapNum的差不能超過1。每次將要插入的元素Num與最大堆頂部元素MaxHeapTop和最小堆的頂部元素MinHeapTop將進行比較,根據具體情況進行插入:
1.如果Num < MaxHeapTop,則
1.1 如果MaxHeapNum <= MinHeapNum,將Num插入最大堆;
1.2 如果MaxHeapNum == MinHeapNum + 1,將MaxHeapTop從最大堆中移到最小堆,並將Num插入最大堆。
2.如果MaxHeapTop <= Num <= MinHeapTop,則
2.1 如果MaxHeapNum <= MinHeapNum,將Num插入最大堆;
2.2 如果MaxHeapNum == MinHeapNum + 1,將Num插入最小堆;
3.如果Num > MinHeapTop,則
3.1 如果MinHeapNum <= MaxHeapNum,將Num插入最小堆;
3.2 如果MinHeapNum == MaxHeapNum + 1,將MinHeapTop移到最大堆中,將Num插入最小堆。
在每次插入后,都要根據情況對MaxHeapNum和MinHeapNum進行變更,並將有改動的堆進行堆調整。
上面的插入情況會保證最大堆和最小堆的元素個數差小於1,中位數就只在最大堆和最小堆的頂部元素中產生:如果最大堆和最小堆的元素個數相等,則中位數為最大堆和最小堆的頂部元素的平均值;否則,中位數為元素個數多的那個堆的堆頂元素。
復雜度分析:每次插入元素時的堆調整平均復雜度為O(log(N/2)),插入N次,所以總的復雜度為O(N*log(N/2))。
總結:
前面插入排序的思想和二分查找的思想之所以復雜度高,是因為其做了許多尋找中位數之外的操作,即排序。題目只是要求每次插入集合時,求出集合的中位數,而對集合中的元素是否排序沒有要求。尋找中位數,我只需要知道中間的數就OK了,沒有必要對所有元素排好序。這正是最后一種思想的精髓:盡量減少額外操作的消耗。
