昨天和好友 @覃会程 仔细探讨了关于在无序数组中 求第K小或者第K大的数这道题,记下来->直呼妙啊!
- 需要奠定的知识点
- 对于中间的数provit的选取,为将最坏的情况概率降到最低,这块将使用随机的方法。
- 在快排的基础上进行,又不与快排相似,正是这个不相似的点,让时间复杂度由O(NlogN)收敛到 O(N)。
- 区别:
- 快排需要即向左,又向右
- 而此方法只需往一边走,另一边就被排除掉了,由此缩小范围
- 区别:
- 以及对partition函数的详述:将数组分为:等于,大于,小于三段。
- 对于partition方法的解释:
-
话不多说,上代码--->看看较为简单的路数。BFPRT和简单路数的区别在于取provit这个数:可谓是精雕细琢,十分严谨!整体来说:大差不差。
package LiKou.BFPRT;
public class SimpleGetMinKTH {
public static void main(String[] args) {
int[] arr = {4, 2, 8, 1, 5};
int i = getKTH(arr, 0, arr.length - 1, 3); // 要进行查找的数组 ,起点 终点 求第几小的数
System.out.println(i);
}
/**
* @param arr 查找的数组
* @param left 起点
* @param right 终点
* @param th 第几小
* @return
*/
private static int getKTH(int[] arr, int left, int right, int th) {
//base case
if (left == right) {
return arr[left];
}
//寻找一个随机的坐标作为其中间值
int provit = arr[left + (int) (Math.random() * (right - left + 1))];
//将数组分为小于 等于 大于三段 最终只返回一个范围
int[] range = partition(arr, left, right, provit);
//如果在范围内直接返回,说明命中
if (th - 1 >= range[0] && th - 1 <= range[1]) {
return arr[range[0]];
} else if (th - 1 < range[0]) { //说明没有命中,在小于的区间里,因此重新进行划分的数组坐标范围为 起点:不变,终点:范围内最小的索引-1
return getKTH(arr, left, range[0] - 1, th);
} else { //反之
return getKTH(arr, range[1] + 1, right, th);
}
}
//分割函数
private static int[] partition(int[] arr, int left, int right, int provit) {
int l = left - 1; //记录小于的已经处理的坐标
int r = right + 1;//记录大于的已经处理的坐标
int cur = left; //轮询
while (cur < r) {//不要到已经确定的大于的索引了
if (arr[cur] < provit) {
swap(arr, cur++, ++l);//小于provit的话交换,放未处理的到左边使之变为已经处理的,因为最左边已经处理过了,cur然后进行下一个
} else if (arr[cur] > provit) {
swap(arr, cur, --r); //大于小于provit的话交换,放到右边,因为最右边已经处理过了,cur交换后的值和provit的大小关系不确定,因此需要接着比较
} else {
cur++;//等于的话直接完后推,没必要交换
}
}
return new int[]{l + 1, r - 1}; //考虑到可能由多个和provit值一样的,因此两个值可能一样,可能不一样
}
//交换
private static void swap(int[] arr, int l, int r) {
int temp = arr[r];
arr[r] = arr[l];
arr[l] = temp;
}
}
- 再来看看真正的BFPRT算法
- 核心思路:对分割值provit严谨的选用
- 选用步骤:
- 以5个值为1组对数组进行分组,剩余不足5个或者刚好5个为1组
- 将所有组进行排序:即组内有序,各组之间无序
- 取所有组对中位数,到最后一个组的时候(如果不为5个,奇数个取中间的,偶数个取上中位数或者下中位数),总之只能取一个。
- 对每个组的中位数形成一个新的中位数数组,然后用这个新的中位数数组重新进行选举,最终返回中位数数组中的中位数。此处数据规模减小到n/5
- 最后返回一个最终的provit
- 下面的分割步骤就和上面的一样了。
- 看代码
-
package LiKou.BFPRT; public class MyBFPRT { public static void main(String[] args) { int[] arr = {4, 2, 8, 1, 5}; int i = bfprt(arr, 0, arr.length - 1, 3); System.out.println(i); } // arr[L..R] public static int bfprt(int[] arr, int L, int R, int index) { if (L == R) { //剩一个数的时候,直接返回 return arr[L]; } int pivot = medianOfMedians(arr, L, R); //***关键点*** int[] range = partition(arr, L, R, pivot); if (index >= range[0] && index <= range[1]) { return arr[index]; } else if (index < range[0]) { return bfprt(arr, L, range[0] - 1, index); } else { return bfprt(arr, range[1] + 1, R, index); } } // arr[L...R] 五个数一组 // 每个小组内部排序 // 每个小组中位数领出来,组成marr // marr中的中位数,返回 public static int medianOfMedians(int[] arr, int L, int R) { int size = R - L + 1;//总大小 int offset = size % 5 == 0 ? 0 : 1; // 看是否会多1个 int[] mArr = new int[size / 5 + offset]; //创建一个新的数组 for (int team = 0; team < mArr.length; team++) { //遍历新的数组 int teamFirst = L + team * 5;//用来控制范围 // L ... L + 4 // L +5 ... L +9 // L +10....L+14 mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));//排序,返回范围内的中位数 } // marr中,找到中位数 // marr(0, marr.len - 1, mArr.length / 2 ) return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);//用bfprt算法取得坐标为(长度-1)/2位置的数 } //排序并返回范围内的中位数 public static int getMedian(int[] arr, int L, int R) { insertionSort(arr, L, R); return arr[(L + R) / 2]; } //排序 public static void insertionSort(int[] arr, int L, int R) { for (int i = L + 1; i <= R; i++) { for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } } public static void swap(int[] arr, int i1, int i2) { int tmp = arr[i1]; arr[i1] = arr[i2]; arr[i2] = tmp; } public static int[] partition(int[] arr, int L, int R, int pivot) { int less = L - 1; //-1 int more = R + 1; //数组的长度 int cur = L; // 0 while (cur < more) { //如果没越界 if (arr[cur] < pivot) { //左边的小于中间的 swap(arr, ++less, cur++); } else if (arr[cur] > pivot) {//左边的大于中间的,当前坐标和最右边进行交换 swap(arr, cur, --more); } else { //等于的话直接跳过 cur++; } } return new int[]{less + 1, more - 1}; } }
大致思路就是这样,看不懂的童鞋 debug 一下就会拨开云雾,加油!