BFPRT算法(中位数数组的中位数算法),是由五位发明者共同创建,主要解决TopK的问题在使用快速选择方法的最坏情况下的时间复杂度变为O(n^2)的问题,使得在最坏情况下仍为O(n).
中心思想:将数组拆分成每个包含五个元素的数组,将每个数组进行排序选出中位数,这些选出中位数的数组调用BFPRT取得中位数,得出一个用于分区操作的划分值.得出的这个划分值就可以完美的避免在最坏的情况下时间为O(n^2)的情况.
以下编码为了更好的理解.Top 0是指 第一小的数,以此类推.
快速选择算法本质上和快排算法是一致的.是针对于TopK的问题的一种更好的解决方案.
快排有一种改进方案是:小于放左边,等于放中间,大于放右边.这种改进方案可以在数组当中含有重复值找出正确的TopK.
function quickSelect(list, left, right, k)
if left = right
return list[left]
Select a pivotValue between left and right
pivotRange := partition(list, left, right,
pivotValue) //pivotRange[0] presents left position,pivotRange[1] presents right position
if k <= pivotRange[1] && k >= pivotRange[0]
return list[k]
else if k < pivotRange[0]
right := pivotRange[0] - 1
quickSelect(list, left, right, k)
else
left := pivotRange[1] + 1
quickSelect(list, left, right, k)
以上方法快速排序在最坏的情况下退化成了O(n^2)为了更好的解决这种情况,BFPRT算法提出了一个更优的方案.
1.将数组拆分成每个包含五个元素的拆分数组,不够五个元素的单独成为一组
2.将拆分数组进行排序,每个拆分数组选出中位数构成中位数数组,如果拆分数组不够五个且为偶数时选择下中位数.
3.递归调用BFPRT算法求出中位数数组的中位数.(调用BFPRT求出TopK为count(arr)/2的数)
4.得出中位数数组的中位数以该值作为分区的值,进行分区.分区时大于放右边,小于放左边,等于放中间.
5.如果K在返回分区的左右边界范围内,返回数组中K位置的值.如果K小于分区的左边界,那么在分区左边界与当前的左边界继续递归调用BFPRT算法.否则,那么在分区右边界与当前的右边界继续递归调用BFPRT算法
实现源码:
class BFPRT
{
/**
* @param $arr array
* @param $l int Left position
* @param $r int Right position
* @param $pv int Pivot value of partition
* @return array
*/
public function partition(&$arr, $l, $r, $pv)
{
$left = $l - 1;
$right = $r + 1;
$cur = $l;
while ($cur < $right) {
if ($arr[$cur] < $pv) {
$this->swap($arr[++$left], $arr[$cur++]);
} else if ($arr[$cur] > $pv) {
$this->swap($arr[--$right], $arr[$cur]);
} else {
$cur++;
}
}
return [$left + 1, $right - 1];
}
/**
* 获取中位数数组的中位数
* @param $arr
* @return mixed
*/
public function medianOfMedians($arr)
{
$length = count($arr);
$i = 0;
$mod = 5;
$newMidArr = [];
do {
$newArr = array_slice($arr, $i, $mod); //这段代码可以优化下
if ($i + $mod > $length) { //最后一组不足五个的时候slice会产生空元素,要把空元素去掉
$newArr = array_filter($newArr, function ($val) {
return $val !== null;
});
}
sort($newArr); //排序
$newMidArr[] = $newArr[intdiv(count($newArr), 2)];//取中位数
} while (($i+=$mod) && $i < $length);
return $this->find($newMidArr, 0, count($newMidArr) - 1, intdiv(count($newMidArr), 2));//去中位数数组中第count($newMidArr)/2小的数,也就是中位数数组中的中位数
}
/**
* @param $arr array
* @param $l int Left position
* @param $r int Right position
* @param $k int top k num -> [0, count(arr))
* @return mixed
*/
public function find(&$arr, $l, $r, $k)
{
if ($l == $r) {
return $arr[$l];
}
$median = $this->medianOfMedians(array_slice($arr, $l, $r - $l + 1));//获取数组的中位数数组的中位数
$pivotRange = $this->partition($arr, $l, $r, $median);//以median作为划分值
if ($pivotRange[0] <= $k && $pivotRange[1] >= $k) {//第k小在左右边界内
return $arr[$k];
} else if ($k < $pivotRange[0]) {//k小于左边界
return $this->find($arr, $l, $pivotRange[0] - 1, $k);
} else {//k大于右边界
return $this->find($arr, $pivotRange[1] + 1, $r, $k);
}
}
private function swap(&$left, &$right)
{
$tmp = $left;
$left = $right;
$right = $tmp;
}
}