介绍
在以往工作或者面试的时候常会碰到一个问题,如何实现海量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.
结束
最后想说的就是 [算法+数据结构] 真的非常重要,一个好的算法可以使我们的效率大大提高。