bfprt算法:用于在未排序的列表中找到第k小(或第k大)的元素,并能保证在最坏情况下仍然具有线性时间复杂度O(n)。
1、算法概述
-
bfprt算法的核心思想
- bfprt算法也常被称为中位数的中位数算法。它的核心目标是选择一个高质量的"主元"(pivot),
- 使得在后续的划分(Partition)过程中,数组能够被相对均匀地分成两部分。
- 这与快速选择算法思想类似,但bfprt通过一种确定性的方法来选择主元,避免了因主元选择不当而导致性能退化为O(n²)的最坏情况。
- 简单来说,bfprt算法通过分组寻找中位数的中位数来确定主元,从而确保每次划分至少能淘汰一定比例的元素,
- 使得问题规模以几何级数减少,最终保证线性时间复杂度。
-
bfprt算法的详细步骤:
- 1)分组
- 将原数组划分为若干小组,每组包含5个元素(最后不足5个的自成一组)。
- 选择5作为组大小是在算法效率上权衡的结果:组太小,中位数的质量可能不够高;组太大,组内排序的代价会增加。
- 2)组内排序并找中位数
- 对每个小组进行插入排序(因为数据量小,插入排序效率很高),然后找出每个小组的中位数。
- 如果小组元素个数为偶数,取上中位数或下中位数均可。
- 3)递归找中位数的中位数
- 将所有小组的中位数取出,组成一个新的"中位数数组"。
- 递归调用bfprt算法本身,找出这个中位数数组的中位数。这个中位数就是我们要找的主元(pivot)。
- 这一步是算法的精髓,它确保了最终选出的主元是整个数组中相对接近中心的值。
- 4)分区
- 使用上一步得到的主元对原始数组进行分区(Partition),操作与快速排序类似:将小于主元的元素放在左边,等于主元的放中间,大于主元的放右边。分区完成后,主元在数组中的最终位置就确定了。
- 5)判断与递归
- 将目标位置k与主元的位置进行比较:
- 如果k正好落在主元所在的"等于区",那么主元本身就是第k小的元素,直接返回。
- 如果k落在"小于区",则只在小于区内递归寻找第k小的元素。
- 如果k落在"大于区",则只在大于区内递归寻找第 (k - 大于区起始索引) 小的元素。
-
bfprt 算法的时间复杂度
- bfprt算法之所以强大,在于其严格证明了最坏情况下的时间复杂度为 O(n)。其时间复杂度递归式通常表示为:T(n) ≤ T(n/5) + T(7n/10) + O(n)。
- T(n/5):来自递归求解中位数数组的中位数(规模约为n/5)。
- O(n):包括分组、组内排序、分区等线性操作。
- T(7n/10):这是关键。由于主元是"中位数的中位数",可以证明它至少比数组中30%的元素大,也至少比30%的元素小。
- 因此,在分区后,每次递归需要处理的子问题规模最大不超过原规模的7/10。
- 通过递归树或主定理可以证明,这个递归式的解是T(n) = O(n)。
2、通过获取第k小问题的方法推出bfprt算法实现
2.1、快速排序的解法
- 快速排序的解法找到数组中第k小的元素
- 思路:
- 如果一个数组是从小到大有序的,要找到一个数组中第k小的元素,就是获取其k-1索引位置的元素。
- 所以要解决这个问题,就是要把数组进行排序。
- 快速排序的思路就是用一个基准值pivot将数组中的元素分为三部分(也可以是两个部分,分三个部分效率更高):
- 小于pivot的元素、等于pivot的元素、大于pivot的元素,然后分分别对大于和小于pivot的元素递归进行排序。
- 结合快速排序的思路,如果要取k-1位置的元素,只需要包含k-1位置的那一部分是有序的,另一部分不需要有序。
- 虽然这种效率已经很高了,但是因为基准值pivot是随机选取的,如果每次都选到的是这段里面的最大值,就会出现最坏的情况,
- 也就是说,利用快速排序的方法找到k的算法复杂度是O(n),但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
- 复杂度的具体值是概率性的,而不是确定性的。
java
/**
* 快速排序的解法找到数组中第k小的元素
* 思路:
* 如果一个数组是从小到大有序的,要找到一个数组中第k小的元素,就是获取其k-1索引位置的元素。
* 所以要解决这个问题,就是要把数组进行排序。
* 快速排序的思路就是用一个基准值pivot将数组中的元素分为三部分(也可以是两个部分,分三个部分效率更高):
* 小于pivot的元素、等于pivot的元素、大于pivot的元素,然后分分别对大于和小于pivot的元素递归进行排序。
* 结合快速排序的思路,如果要取k-1位置的元素,只需要包含k-1位置的那一部分是有序的,另一部分不需要有序。
* 虽然这种效率已经很高了,但是因为基准值pivot是随机选取的,如果每次都选到的是这段里面的最大值,就会出现最坏的情况,
* 也就是说,利用快速排序的方法找到k的算法复杂度是O(n),但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
* 复杂度的具体值是概率性的,而不是确定性的。
*/
public static int minKWithQuickSort(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 用快速排序的方法取k-1位置的值
return minKWithQuickSortProcess(arr, 0, arr.length - 1, k - 1);
}
/**
* 用快速排序的方法取数组arr[L...R]中第index位置的元素
*/
private static int minKWithQuickSortProcess(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[L];
}
// 在范围内随机选择一个基准值
int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
// 三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边界
int[] range = partition(arr, L, R, pivot);
// 根据index在哪个范围,就递归处理那一部分
if (index >= range[0] && index <= range[1]) {
// 在等于pivot的范围,因为等于pivot的位置已经排好序了,直接返回位置即可
return arr[index];
} else if (index < range[0]) {
// 如果index在小于pivot的范围,就递归处理小于pivot的范围
return minKWithQuickSortProcess(arr, L, range[0] - 1, index);
} else {
// 如果index在大于pivot的范围,就递归处理大于pivot的范围
return minKWithQuickSortProcess(arr, range[1] + 1, R, index);
}
}
/**
* 三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边
*/
private static int[] partition(int[] arr, int L, int R, int pivot) {
// 左边界,代表第一个小于pivot的下标(不是等于pivot)
int less = L - 1;
// 右边界,代表第一个大于pivot的下标(不是等于pivot)
int more = R + 1;
// 当前遍历的下标
int cur = L;
// 这里不能小于R,因为more在往左移动,我们只需要走到第一个大于pivot的位置即可,要不然会因为重复交换出错
while (cur < more) {
if (arr[cur] < pivot) {
// 小于pivot的元素,交换到less右边的位置,less往右移动一位
swap(arr, ++less, cur++);
} else if (arr[cur] > pivot) {
// 大于pivot的元素,交换到more左边的位置,more往左移动一位
swap(arr, cur, --more);
} else {
// 等于pivot的元素,直接跳过
cur++;
}
}
// 因为less和more都是不包含等于pivot的,所以返回等于pivot范围的边界的时候,要返回less+1和more-1
return new int[]{less + 1, more - 1};
}
private static void swap(int[] arr, int i1, int i2) {
int tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
2.2、bfprt算法
- 用bfprt的方法找到数组中第k小的元素
- 思路:
- 我们上面用快速排序的思路解决了这个问题,但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
- 为了避免这种情况,我们可以用bfprt的方法来解决这个问题。
- bfprt的方法是在快速排序的基础上,每次选取基准值pivot时,不是随机选取,而是根据数组的中位数来选取。
- 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
- 我们在每次获取基准值pivot的时候,用medianOfMedians方法来获取数组的中位数,
- 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
- bfprt的具体步骤,可以参考上面的介绍《bfprt算法的详细步骤》
java
/**
* 用bfprt的方法找到数组中第k小的元素
* 思路:
* 我们上面用快速排序的思路解决了这个问题,但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
* 为了避免这种情况,我们可以用bfprt的方法来解决这个问题。
* bfprt的方法是在快速排序的基础上,每次选取基准值pivot时,不是随机选取,而是根据数组的中位数来选取。
* 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
* 我们在每次获取基准值pivot的时候,用medianOfMedians方法来获取数组的中位数,
* 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
* bfprt的具体步骤,可以参考上面的介绍《bfprt算法的详细步骤》
*/
public static int minKWithBfprt(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 用bfprt的方法取k-1位置的值
return minKWithBfprtProcess(arr, 0, arr.length - 1, k - 1);
}
/**
* 用bfprt的方法取数组arr[L...R]中第index位置的元素
*/
private static int minKWithBfprtProcess(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[L];
}
// 用bfprt算法取pivot(包含步骤里面的1)-3))
int pivot = medianOfMedians(arr, L, R);
// 4)三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边界
int[] range = partition(arr, L, R, pivot);
// 5)根据index在哪个范围,就递归处理那一部分
if (index >= range[0] && index <= range[1]) {
// 在等于pivot的范围,因为等于pivot直位置已经排好序了,的接返回位置即可
return arr[index];
} else if (index < range[0]) {
// 如果index在小于pivot的范围,就递归处理小于pivot的范围
return minKWithBfprtProcess(arr, L, range[0] - 1, index);
} else {
// 如果index在大于pivot的范围,就递归处理大于pivot的范围
return minKWithBfprtProcess(arr, range[1] + 1, R, index);
}
}
/**
* 用bfprt的方法找到数组arr[L...R]的中位数
*/
private static int medianOfMedians(int[] arr, int L, int R) {
// 1)分组:将范围L...R的元素分成5个一组,不足5个的自成一组
int size = R - L + 1;
int offset = size % 5 == 0 ? 0 : 1;
// 中位数数组
int[] mArr = new int[size / 5 + offset];
// 根据分组来循环处理
for (int team = 0; team < mArr.length; team++) {
int teamFirst = L + team * 5;
int teamLast = Math.min(R, teamFirst + 4);
// 2)对每个组进行插入排序,并找到中位数
insertionSort(arr, teamFirst, teamLast);
// 3.1)将每个组的中位数收集起来,形成一个新的数组mArr
mArr[team] = arr[(teamFirst + teamLast) / 2];
}
// 3.2)递归调用BFPRT算法本身,找出这个中位数数组的中位数
// 在一个数组中找到中位数,其实就是找到其排好序后的中间位置的元素
return minKWithBfprtProcess(mArr, 0, mArr.length - 1, mArr.length / 2);
}
/**
* 对数组arr[L...R]进行插入排序
*/
private 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);
}
}
}
/**
* 三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边
*/
private static int[] partition(int[] arr, int L, int R, int pivot) {
// 左边界,代表第一个小于pivot的下标(不是等于pivot)
int less = L - 1;
// 右边界,代表第一个大于pivot的下标(不是等于pivot)
int more = R + 1;
// 当前遍历的下标
int cur = L;
// 这里不能小于R,因为more在往左移动,我们只需要走到第一个大于pivot的位置即可,要不然会因为重复交换出错
while (cur < more) {
if (arr[cur] < pivot) {
// 小于pivot的元素,交换到less右边的位置,less往右移动一位
swap(arr, ++less, cur++);
} else if (arr[cur] > pivot) {
// 大于pivot的元素,交换到more左边的位置,more往左移动一位
swap(arr, cur, --more);
} else {
// 等于pivot的元素,直接跳过
cur++;
}
}
// 因为less和more都是不包含等于pivot的,所以返回等于pivot范围的边界的时候,要返回less+1和more-1
return new int[]{less + 1, more - 1};
}
private static void swap(int[] arr, int i1, int i2) {
int tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
2.3、利用堆的解法
- 用堆的方法找到数组中第k小的元素
- 思路:
- 如果是一个有序数组,我们要找第k小的元素,如果将这个数组从k位置分开,这个位置就是前面k个元素组成数组中最大的那个,
- 同时也是后面n-k个元素中最小的那个。
- 我们可以用一个大根堆,先将0到k-1位置的k个元素放进去,大根堆自然就会排好序,此时的堆顶元素,就是放进去的k个元素中的最大值。
- 然后在看数组中剩下位置的元素,将其与堆顶元素进行比较,如果小于堆顶元素,说明我们此时堆里面的最大值不是最小的,就将其替换成小的那个数。
- 最后堆顶元素就是数组中第k小的元素。
- 这个方法的核心思路是先将前k个元素找到最大的,然后将这个最大值进行修正成后面数组中的最小的那个。
- 时间复杂度是O(nlogk),空间复杂度是O(k)。
java
/**
* 用堆的方法找到数组中第k小的元素
* 思路:
* 如果是一个有序数组,我们要找第k小的元素,如果将这个数组从k位置分开,这个位置就是前面k个元素组成数组中最大的那个,
* 同时也是后面n-k个元素中最小的那个。
* 我们可以用一个大根堆,先将0到k-1位置的k个元素放进去,大根堆自然就会排好序,此时的堆顶元素,就是放进去的k个元素中的最大值。
* 然后在看数组中剩下位置的元素,将其与堆顶元素进行比较,如果小于堆顶元素,说明我们此时堆里面的最大值不是最小的,就将其替换成小的那个数。
* 最后堆顶元素就是数组中第k小的元素。
* 这个方法的核心思路是先将前k个元素找到最大的,然后将这个最大值进行修正成后面数组中的最小的那个。
* 时间复杂度是O(nlogk),空间复杂度是O(k)。
*/
public static int minKWithHeap(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 新建一个大根堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 先将前k个元素放进去
for (int i = 0; i < k; i++) {
maxHeap.add(arr[i]);
}
// 然后遍历数组中剩下的元素
for (int i = k; i < arr.length; i++) {
// 如果当前元素小于堆顶元素,说明堆里面的最大值不是最小的,就将其替换成小的那个数
if (arr[i] < maxHeap.peek()) {
maxHeap.poll();
maxHeap.add(arr[i]);
}
}
// 最后堆顶元素就是数组中第k小的元素
return maxHeap.peek();
}
整体代码和测试如下:
java
import java.util.PriorityQueue;
/**
* bfprt算法:用于在未排序的列表中找到第k小(或第k大)的元素,并能保证在最坏情况下仍然具有线性时间复杂度O(n)。
* <br>
* bfprt算法的核心思想
* bfprt算法也常被称为中位数的中位数算法。它的核心目标是选择一个高质量的"主元"(pivot),
* 使得在后续的划分(Partition)过程中,数组能够被相对均匀地分成两部分。
* 这与快速选择算法思想类似,但bfprt通过一种确定性的方法来选择主元,避免了因主元选择不当而导致性能退化为O(n²)的最坏情况。
* 简单来说,bfprt算法通过分组寻找中位数的中位数来确定主元,从而确保每次划分至少能淘汰一定比例的元素,
* 使得问题规模以几何级数减少,最终保证线性时间复杂度。
* <br>
* bfprt算法的详细步骤:
* 1)分组
* 将原数组划分为若干小组,每组包含5个元素(最后不足5个的自成一组)。
* 选择5作为组大小是在算法效率上权衡的结果:组太小,中位数的质量可能不够高;组太大,组内排序的代价会增加。
* 2)组内排序并找中位数
* 对每个小组进行插入排序(因为数据量小,插入排序效率很高),然后找出每个小组的中位数。
* 如果小组元素个数为偶数,取上中位数或下中位数均可。
* 3)递归找中位数的中位数
* 将所有小组的中位数取出,组成一个新的"中位数数组"。
* 递归调用bfprt算法本身,找出这个中位数数组的中位数。这个中位数就是我们要找的主元(pivot)。
* 这一步是算法的精髓,它确保了最终选出的主元是整个数组中相对接近中心的值。
* 4)分区
* 使用上一步得到的主元对原始数组进行分区(Partition),操作与快速排序类似:将小于主元的元素放在左边,等于主元的放中间,大于主元的放右边。分区完成后,主元在数组中的最终位置就确定了。
* 5)判断与递归
* 将目标位置k与主元的位置进行比较:
* 如果k正好落在主元所在的"等于区",那么主元本身就是第k小的元素,直接返回。
* 如果k落在"小于区",则只在小于区内递归寻找第k小的元素。
* 如果k落在"大于区",则只在大于区内递归寻找第 (k - 大于区起始索引) 小的元素。
* <br>
* bfprt 算法的时间复杂度
* bfprt算法之所以强大,在于其严格证明了最坏情况下的时间复杂度为 O(n)。其时间复杂度递归式通常表示为:T(n) ≤ T(n/5) + T(7n/10) + O(n)。
* T(n/5):来自递归求解中位数数组的中位数(规模约为n/5)。
* O(n):包括分组、组内排序、分区等线性操作。
* T(7n/10):这是关键。由于主元是"中位数的中位数",可以证明它至少比数组中30%的元素大,也至少比30%的元素小。
* 因此,在分区后,每次递归需要处理的子问题规模最大不超过原规模的7/10。
* 通过递归树或主定理可以证明,这个递归式的解是T(n) = O(n)。
*/
public class Bfprt {
/**
* 快速排序的解法找到数组中第k小的元素
* 思路:
* 如果一个数组是从小到大有序的,要找到一个数组中第k小的元素,就是获取其k-1索引位置的元素。
* 所以要解决这个问题,就是要把数组进行排序。
* 快速排序的思路就是用一个基准值pivot将数组中的元素分为三部分(也可以是两个部分,分三个部分效率更高):
* 小于pivot的元素、等于pivot的元素、大于pivot的元素,然后分分别对大于和小于pivot的元素递归进行排序。
* 结合快速排序的思路,如果要取k-1位置的元素,只需要包含k-1位置的那一部分是有序的,另一部分不需要有序。
* 虽然这种效率已经很高了,但是因为基准值pivot是随机选取的,如果每次都选到的是这段里面的最大值,就会出现最坏的情况,
* 也就是说,利用快速排序的方法找到k的算法复杂度是O(n),但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
* 复杂度的具体值是概率性的,而不是确定性的。
*/
public static int minKWithQuickSort(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 用快速排序的方法取k-1位置的值
return minKWithQuickSortProcess(arr, 0, arr.length - 1, k - 1);
}
/**
* 用快速排序的方法取数组arr[L...R]中第index位置的元素
*/
private static int minKWithQuickSortProcess(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[L];
}
// 在范围内随机选择一个基准值
int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
// 三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边界
int[] range = partition(arr, L, R, pivot);
// 根据index在哪个范围,就递归处理那一部分
if (index >= range[0] && index <= range[1]) {
// 在等于pivot的范围,因为等于pivot的位置已经排好序了,直接返回位置即可
return arr[index];
} else if (index < range[0]) {
// 如果index在小于pivot的范围,就递归处理小于pivot的范围
return minKWithQuickSortProcess(arr, L, range[0] - 1, index);
} else {
// 如果index在大于pivot的范围,就递归处理大于pivot的范围
return minKWithQuickSortProcess(arr, range[1] + 1, R, index);
}
}
/**
* 三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边
*/
private static int[] partition(int[] arr, int L, int R, int pivot) {
// 左边界,代表第一个小于pivot的下标(不是等于pivot)
int less = L - 1;
// 右边界,代表第一个大于pivot的下标(不是等于pivot)
int more = R + 1;
// 当前遍历的下标
int cur = L;
// 这里不能小于R,因为more在往左移动,我们只需要走到第一个大于pivot的位置即可,要不然会因为重复交换出错
while (cur < more) {
if (arr[cur] < pivot) {
// 小于pivot的元素,交换到less右边的位置,less往右移动一位
swap(arr, ++less, cur++);
} else if (arr[cur] > pivot) {
// 大于pivot的元素,交换到more左边的位置,more往左移动一位
swap(arr, cur, --more);
} else {
// 等于pivot的元素,直接跳过
cur++;
}
}
// 因为less和more都是不包含等于pivot的,所以返回等于pivot范围的边界的时候,要返回less+1和more-1
return new int[]{less + 1, more - 1};
}
private static void swap(int[] arr, int i1, int i2) {
int tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
/**
* 用bfprt的方法找到数组中第k小的元素
* 思路:
* 我们上面用快速排序的思路解决了这个问题,但是因为基准值pivot是随机选取的,所以最坏情况下的时间复杂度是O(n²)。
* 为了避免这种情况,我们可以用bfprt的方法来解决这个问题。
* bfprt的方法是在快速排序的基础上,每次选取基准值pivot时,不是随机选取,而是根据数组的中位数来选取。
* 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
* 我们在每次获取基准值pivot的时候,用medianOfMedians方法来获取数组的中位数,
* 这样可以保证每次递归处理的子问题规模最大不超过原规模的7/10,所以时间复杂度是O(n)。
* bfprt的具体步骤,可以参考上面的介绍《bfprt算法的详细步骤》
*/
public static int minKWithBfprt(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 用bfprt的方法取k-1位置的值
return minKWithBfprtProcess(arr, 0, arr.length - 1, k - 1);
}
/**
* 用bfprt的方法取数组arr[L...R]中第index位置的元素
*/
private static int minKWithBfprtProcess(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[L];
}
// 用bfprt算法取pivot(包含步骤里面的1)-3))
int pivot = medianOfMedians(arr, L, R);
// 4)三分区数组,返回结果range的0和1位置是等于基准值pivot的左右边界
int[] range = partition(arr, L, R, pivot);
// 5)根据index在哪个范围,就递归处理那一部分
if (index >= range[0] && index <= range[1]) {
// 在等于pivot的范围,因为等于pivot直位置已经排好序了,的接返回位置即可
return arr[index];
} else if (index < range[0]) {
// 如果index在小于pivot的范围,就递归处理小于pivot的范围
return minKWithBfprtProcess(arr, L, range[0] - 1, index);
} else {
// 如果index在大于pivot的范围,就递归处理大于pivot的范围
return minKWithBfprtProcess(arr, range[1] + 1, R, index);
}
}
/**
* 用bfprt的方法找到数组arr[L...R]的中位数
*/
private static int medianOfMedians(int[] arr, int L, int R) {
// 1)分组:将范围L...R的元素分成5个一组,不足5个的自成一组
int size = R - L + 1;
int offset = size % 5 == 0 ? 0 : 1;
// 中位数数组
int[] mArr = new int[size / 5 + offset];
// 根据分组来循环处理
for (int team = 0; team < mArr.length; team++) {
int teamFirst = L + team * 5;
int teamLast = Math.min(R, teamFirst + 4);
// 2)对每个组进行插入排序,并找到中位数
insertionSort(arr, teamFirst, teamLast);
// 3.1)将每个组的中位数收集起来,形成一个新的数组mArr
mArr[team] = arr[(teamFirst + teamLast) / 2];
}
// 3.2)递归调用BFPRT算法本身,找出这个中位数数组的中位数
// 在一个数组中找到中位数,其实就是找到其排好序后的中间位置的元素
return minKWithBfprtProcess(mArr, 0, mArr.length - 1, mArr.length / 2);
}
/**
* 对数组arr[L...R]进行插入排序
*/
private 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);
}
}
}
/**
* 用堆的方法找到数组中第k小的元素
* 思路:
* 如果是一个有序数组,我们要找第k小的元素,如果将这个数组从k位置分开,这个位置就是前面k个元素组成数组中最大的那个,
* 同时也是后面n-k个元素中最小的那个。
* 我们可以用一个大根堆,先将0到k-1位置的k个元素放进去,大根堆自然就会排好序,此时的堆顶元素,就是放进去的k个元素中的最大值。
* 然后在看数组中剩下位置的元素,将其与堆顶元素进行比较,如果小于堆顶元素,说明我们此时堆里面的最大值不是最小的,就将其替换成小的那个数。
* 最后堆顶元素就是数组中第k小的元素。
* 这个方法的核心思路是先将前k个元素找到最大的,然后将这个最大值进行修正成后面数组中的最小的那个。
* 时间复杂度是O(nlogk),空间复杂度是O(k)。
*/
public static int minKWithHeap(int[] arr, int k) {
if (arr == null || arr.length == 0 || k < 1 || k > arr.length) {
return -1;
}
// 新建一个大根堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 先将前k个元素放进去
for (int i = 0; i < k; i++) {
maxHeap.add(arr[i]);
}
// 然后遍历数组中剩下的元素
for (int i = k; i < arr.length; i++) {
// 如果当前元素小于堆顶元素,说明堆里面的最大值不是最小的,就将其替换成小的那个数
if (arr[i] < maxHeap.peek()) {
maxHeap.poll();
maxHeap.add(arr[i]);
}
}
// 最后堆顶元素就是数组中第k小的元素
return maxHeap.peek();
}
public static void main(String[] args) {
int testTime = 1000000;
int maxSize = 100;
int maxValue = 100;
System.out.println("开始测试");
for (int i = 0; i < testTime; i++) {
int[] arr = generateRandomArray(maxSize, maxValue);
int k = (int) (Math.random() * arr.length) + 1;
int ans1 = minKWithQuickSort(copyArray(arr), k);
int ans2 = minKWithBfprt(copyArray(arr), k);
int ans3 = minKWithHeap(copyArray(arr), k);
if (ans1 != ans2 || ans2 != ans3) {
System.out.println("测试出错!");
printArray(arr);
System.out.printf("k=%d, ans1=%d, ans2=%d, ans3=%d\n", k, ans1, ans2, ans3);
break;
}
}
System.out.println("测试完成");
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) (Math.random() * maxSize) + 1];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * (maxValue + 1));
}
return arr;
}
public static int[] copyArray(int[] arr) {
int[] ans = new int[arr.length];
for (int i = 0; i != ans.length; i++) {
ans[i] = arr[i];
}
return ans;
}
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷