介紹
在以往工作或者面試的時候常會碰到一個問題,如何實現海量TopN,就是在一個非常大的結果集里面快速找到最大的前10或前100個數,同時要保證內存和速度的效率,我們可能第一個想法就是利用排序,然后截取前10或前100,而排序對于量不是特別大的時候沒有任何問題,但只要量特別大是根本不可能完成這個任務的,比如在一個數組或者文本文件里有幾億個數,這樣是根本無法全部讀入內存的,所以利用排序解決這個問題并不是最好的,所以我們這里就用php去實現一個小頂堆來解決這個問題.
二叉堆
二叉堆是一種特殊的堆,二叉堆是完全二叉樹或者是近似完全二叉樹,二叉堆有兩種,最大堆 和 最小堆,最大堆:父結點的鍵值總是大于或等于任何一個子節點的鍵值;最小堆:父結點的鍵值總是小于或等于任何一個子節點的鍵值
二叉堆一般用數組來表示(看上圖),例如,根節點在數組中的位置是0,第n個位置的子節點分別在2n+1和 2n+2,因此,第0個位置的子節點在1和2,1的子節點在3和4,以此類推,這種存儲方式便於尋找父節點和子節點。
具體概念問題這里就不在多說了,如果對二叉堆有疑問的可以在好好了解下這個數據結構,下面我們就針對上述topN問題來用php代碼實現并解決,為了看出區別這里先用排序的方式去實現下看下效果如何。
- 利用快速排序算法來實現 TopN
//為了測試運行內存調大一點
ini_set('memory_limit', '2024M');
//實現一個快速排序函數
function quick_sort(array $array)
{
$length = count($array);
$left_array = array();
$right_array = array();
if ($length <= 1) {
return $array;
}
$key = $array[0];
for ($i = 1;$i < $length;$i++) {
if ($array[$i] > $key) {
$right_array[] = $array[$i];
} else {
$left_array[] = $array[$i];
}
}
$left_array = quick_sort($left_array);
$right_array = quick_sort($right_array);
return array_merge($right_array, array($key), $left_array);
}
//構造500w不重復數
for ($i = 0;$i < 5000000;$i++) {
$numArr[] = $i;
}
//打亂它們
shuffle($numArr);
//現在我們從里面找到top10最大的數
var_dump(time());
print_r(array_slice(quick_sort($all), 0, 10));
var_dump(time());
可以看到上面打印出了top10的結果,并輸出了下運行時間,大概99s左右,但這只是500w個數且全部能裝入內存的情況,如果我們有一個文件里面有5kw或5億個數,肯定就會有些問題了.
- 利用小頂堆算法來實現 TopN
實現流程是:
1、先寫入10個或100個數到堆容器里面,這就是我們的topN數,寫入過程已經維護完成最小堆.
2、從文件或者數組依次遍歷剩余的所有數值.
4、每遍歷出來一個則跟堆頂的元素進行大小比較,如果小于堆頂元素則拋棄,如果大于堆頂元素則出堆.
5、出堆之后,在把當前需要替換的新值寫入堆容器,同時維護完成最小堆.
6、重復以上4~5步驟,這樣當全部遍歷完畢之后,我們這個小頂堆里面的就是最大的topN,因為我們的小頂堆永遠都是排除最小的留下最大的,而且這個調整小頂堆速度也很快,只是相對調整下,只要保證根節點小于左右節點就可以.
//小頂堆類
class MinHeap
{
//堆容器
private $tree = [];
//堆大小
private $len = 0;
/**
* 入堆
* @param $val
*/
public function pushHeap($val)
{
$this->tree[$this->len] = $val;
$size = ++$this->len;
$offset = floor($size / 2) - 1;
$j = $size - 1;
for ($i = $offset; $i >= 0; $i = ceil($i / 2) - 1) {
if ($this->tree[$i] > $this->tree[$j]) {
$this->swap($i, $j);
$j = $i;
}
}
}
/**
* 出堆
* @return mixed
*/
public function topHeap()
{
$this->len = (($this->len - 1) < 0) ? 0 : $this->len - 1;
$size = $this->len;
$offset = floor($size / 2) - 1;
$this->swap(0, $size);
for ($i = 0; $i <= $offset; $i = $j) {
$left_offset = ($i << 1) + 1;
$right_offset = ($i << 1) + 2;
if ($right_offset < $size && $this->tree[$right_offset] < $this->tree[$left_offset]) {
$j = $right_offset;
} else {
$j = $left_offset;
}
if ($this->tree[$i] > $this->tree[$j]) {
$this->swap($i, $j);
} else {
break;
}
}
return $this->tree[$size];
}
/**
* 交換元素
* @param $i
* @param $j
*/
private function swap($i, $j)
{
$tmpVal = $this->tree[$i];
$this->tree[$i] = $this->tree[$j];
$this->tree[$j] = $tmpVal;
}
/**
* 獲取堆數據
* @return array
*/
public function getHeap()
{
return array_slice($this->tree, 0, $this->len);
}
// public function getHeapAll(){
// return $this->tree;
// }
/**
* 獲取堆頂
* @return mixed
*/
public function getTop(){
return $this->tree[0];
}
}
$obj = new MinHeap();
//這里為了保證跟上面一致,也構造500w不重復數
for($i=0;$i<500000;$i++){
$numArr[] = $i;
}
//打亂它們
shuffle($numArr);
//topK數量
$topNum = 10;
//先取出10個寫入堆
foreach (array_slice($numArr,0,$topNum) as $value){
$obj->pushHeap($value);
}
var_dump(time());
//這里可以看到,就是開始遍歷剩下的所有元素
for($i = $topNum; $i < count($numArr); $i++){
//每遍歷一個則跟堆頂元素進行比較大小
if ($numArr[$i] > $obj->getTop()){
//出堆
$obj->topHeap();
//入堆
$obj->pushHeap($numArr[$i]);
}
}
print_r($obj->getHeap());
var_dump(time());
可以看到最終的結果也是top10,只不過時間只用了1s左右,而且無論是內存還是時間效率都滿足我們的要求,而且跟排序比最好的一點就是不用把所有的數據集都讀如到內存里面來,因為我們不需要排序,而上面是為了演示,所以直接在內存構造了500w元素,然而我們可以把這個全部轉移到文件里面去,然后一行一行讀取進行比較,因為我們這個數據結構的核心點就是線性遍歷跟內存里面很小的小頂堆結構進行比較,最終得到TopN.
結束
最后想說的就是 [算法+數據結構] 真的非常重要,一個好的算法可以使我們的效率大大提高。