BFPRT
题目:在一个无序数组中找到一个第K小的数
一:背景介绍
原文链接: https://segmentfault.com/a/1190000008322873
在一堆数中求其前 k 大或前 k 小的问题,简称 TOP-K 问题。而目前解决 TOP-K 问题最有效的算法即是 BFPRT 算法,又称为中位数的中位数算法,该算法由 Blum、Floyd、Pratt、Rivest、Tarjan 提出,最坏时间复杂度为 O(N*logk)。
在首次接触 TOP-K 问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前 k 即可,但是这么做有两个问题:
- 快速排序的平均复杂度为 O(N*logN),但最坏时间复杂度为O(N^2),不能始终保证较好的复杂度;
- 我们只需要前 k 大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。
除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为 k 的堆。
那是否还存在更有效的方法呢?我们来看下 BFPRT 算法的做法。
在快速排序的基础上,首先通过判断主元位置与k的大小使递归的规模变小,其次通过修改快速排序中主元的选取方法来降低快速排序在最坏情况下的时间复杂度。
下面先来简单回顾下快速排序的过程,以升序为例:
- 选取主元;
- 以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
- 分别对左边和右边进行递归,重复上述过程。
二:算法过程及代码
-
选取主元
- 将数组中的 n 个元素按顺序分为多个组,每组5个元素, 不足5个,自成一组
- 使用插入排序,分别求这几个组的中位数,求的过程中把每一组的中位数依次放在数组的前面。
比如数组总共有25个元素,这一步结束,数组前五个元素分别为5个组的中位数。 - 然后使用BFPRT算法从放于数组前端的中位数中选取中位数,此数值即为主元。返回此数值的下标,而不是值。
-
以 1.3 选取的主元为分界点进行划分,把小于主元的放在左边,大于主元的放在右边 。划分完成后,返回主元的下标,即等于区域的右边界。
-
判断主元的位置与 k 的大小,如果主元的下标恰好等于K,则该主元就是第k小的元素,否则,有选择的对左边或右边递归。
注意:
- BFPRT算法和代码中的插入排序算法,返回的都是下标。
- 第K小的元素,K值不能为0,所以在写代码的过程中要保证
K>0
三:代码(JAVA)
class Solution_BFPRT{
/**
* 返回数组 arr[left, right] 的第 k 小数的下标
*/
public static int bfprt(int[] arr, int left, int right, int k){
//将数组划分成N/5份,然后找这N/5个中位数中的中位数,把它当做划分点
int pivot = getPivotIndex(arr, left, right);
//根据pivot,将数组划分成三个区域,返回等于区域的右边界
int index = partition(arr, left, right, pivot);
//下标从left到index之间,总共有left_num个数
int left_num = index - left + 1;
if(left_num == k){//如果下标从left到index之间刚好有k个数,则index为第k小的数的下标
return index;
}else if(left_num > k){//否则,接着划分
return bfprt(arr, left, index - 1, k);
}else{
return bfprt(arr, index + 1, right, k - left_num);
}
}
//将数组划分成N/5份,然后找这N/5个中位数中的中位数
public static int getPivotIndex(int[] arr, int left, int right){
int len = right - left + 1;
if(len <= 5){
return insertSort(arr, left, right);
}
int i = left, j = left - 1;
while(i + 4 <= right){
int index = insertSort(arr, i, i + 4);
//将该5个数中的中位数,放到arr数组的前面。
swap(arr, index, ++j);
i += 5;
}
//以上操作完成后,数组[left,j]区间上的数为每一个小数组的中位数。使用BFPRT算法再求中位数的中位数,是为了降低时间复杂度。
//最后一个参数多加一个 1 的作用其实就是向上取整的意思,这样可以始终保持 k 大于 0。
//因为数组中第k小的元素,k的值是从1开始的
return bfprt(arr, left, j, (j - left + 1) / 2 + 1);
}
/*
* k为主元的下标,以下标k的元素值,进行一次划分,将数组划分成三个区域
* 返回等于区域的右边界
* */
public static int partition(int[] arr, int left, int right, int k){
swap(arr, right, k);
int less = left - 1;
int more = right;
int pivot = arr[right];
int cur = left;
while(cur < more){
if(arr[cur] < pivot){
swap(arr, ++less, cur++);
}else if(arr[cur] > pivot){
swap(arr, --more, cur);
}else{
cur++;
}
}
swap(arr, cur, right);
return cur;
}
//使用插入排序找数组的中位数的下标
public static int insertSort(int[] arr, int left, int right){
int mid = (left + right) / 2;
for(int i = left + 1; i <= right; i++){
for(int j = i; j > left && arr[j] < arr[j - 1]; j--){
swap(arr, j, j - 1);
}
}
return mid;//返回下标。
}
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5, 6,8,7,9,10, 1,6,8,4,9, 2,5,4,9,6, 3,4,7,8,1};
//使用bfprt算法获取第k小的元素的下标
int index = bfprt(arr, 0, arr.length - 1, 10);
//则第k小的元素的值为arr[index],此时arr数组中的顺序已经被更改。
System.out.println(arr[index]);
}
}
时间复杂度O(N)
请看:https://segmentfault.com/a/1190000008322873