【算法笔记】bfprt算法

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();
    }
}

后记

个人学习总结笔记,不能保证非常详细,轻喷

相关推荐
番石榴AI2 小时前
java版的ocr推荐引擎——JiaJiaOCR 2.0重磅升级!纯Java CPU推理,新增手写OCR与表格识别
java·python·ocr
中屹指纹浏览器2 小时前
指纹浏览器抗检测进阶:绕过深度风控的技术实践
服务器·网络·经验分享·笔记·媒体
youngee112 小时前
hot100-47岛屿数量
算法
思成不止于此2 小时前
【MySQL 零基础入门】DQL 核心语法(四):执行顺序与综合实战 + DCL 预告篇
数据库·笔记·学习·mysql
鸽鸽程序猿3 小时前
【项目】【抽奖系统】抽奖
java·spring
无限进步_3 小时前
深入理解 C/C++ 内存管理:从内存布局到动态分配
c语言·c++·windows·git·算法·github·visual studio
长安er3 小时前
LeetCode 34排序数组中查找元素的第一个和最后一个位置-二分查找
数据结构·算法·leetcode·二分查找·力扣
GoogleDocs3 小时前
基于[api-football]数据学习示例
java·学习
卓码软件测评3 小时前
第三方软件验收评测机构【Gatling安装指南:Java环境配置和IDE插件安装】
java·开发语言·ide·测试工具·负载均衡