快速选择算法

分治介绍:

分治思想:

①将大问题划分为两个到多个子问题。

②子问题可以继续拆分成更小的问题,直到能够简单求解。

③如果有必要,将子问题的解进行合并,得到原始问题的解。

其实我们之前有学过带有分治思想的一些算法,比如二分查找,二分查找就是将一个大问题拆分为

两个子问题,在子问题中求解。

还有快速排序算法,选定基准点后分区。还有归并排序等等。。。

分治和动态规划有点相似,他们求解时都需要拆分子问题,但是动态规划的子问题有重叠,因此需

要记录之前子问题的解,避免重复运算。然而分治的问题没有重叠。

接下来介绍一种新的算法:

快速选择算法:

它运用了分治的思想

题目:求排在第 i 名的元素, i 从 0 开始,由小到大排

6        5        1        2        4

分析:

求解这道题我们可以借鉴快速排序的思想,先随机选取一个基准点,把小于基准点的元素放在基准点左边,大于基准点的元素放在基准点右边。

如果基准点所在的位置刚好是我们需要求的位置,那直接返回即可,效率得到提升。

如果不是我们需要的位置,基准点的索引值大于我们需要的名次,那么基准点右边的元素的顺序就不用管了,我们只需关心基准点左边的元素就行,接着重复以上的过程即可。

假设我们选了4为基准点,我们需要把小于4的元素放在4左边,大于4的放在4右边。这时我们是知道基准点的索引值的,如果正好我们需要求的是第2名(从第0名开始)的元素,索引值为2恰好等于 i ,我们可以直接返回。


代码实现:
import java.util.concurrent.ThreadLocalRandom;

/**
 * 快速选择算法 - 分而治之
 */
public class QuickSelect {

    public static int quick(int[] a, int left, int right, int i){
        int p = partition(a, left, right); //基准点索引
        if(i == p){
            return a[p];
        }
        if(p < i){
            return quick(a, p + 1, right, i);
        }else{
            return quick(a, left, p - 1, i);
        }
    }

    public static void main(String[] args) {
        int[] array = {6, 5, 1, 2, 4};
        System.out.println(quick(array, 0, array.length - 1, 0));
        System.out.println(quick(array, 0, array.length - 1, 1));
        System.out.println(quick(array, 0, array.length - 1, 2));
        System.out.println(quick(array, 0, array.length - 1, 3));
        System.out.println(quick(array, 0, array.length - 1, 4));
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, idx, left);
        int pv = a[left]; //创建基准点
        int i = left + 1;
        int j = right;
        while (i <= j) {
            //1.i 从左向右找到比基准点大的或者相等的
            while (i <= j && a[i] < pv) {
                i++;
            }
            //2.j 从右向左找到比基准点小的或者相等的
            while (i <= j && pv < a[j]) {
                j--;
            }
            if (i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        //基准数归位
        swap(a, j, left);
        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

快速选择算法的应用:

我们先来看一道例题:

求数组中第 k 大的元素:

注: k 从 1 开始

例题分析:

之前这个题目是用小顶堆做的,但是时间复杂度是O(nlogn),不是很好,我们希望实现O(n)的时间复杂度。

这道题和前面的求排名第几的元素有点像,只不过前面那道题是从小到大来排的,我们只需找到本题和前面那道题的对应关系即可。

由题知,我们要求解数组中第 1 大的元素 ,只需求原数组(从小到大排列)中排名第 4 (第 4 小) 的元素,要求数组中第 2 大的元素,只需求原数组中排名第 3 的元素,以此类推。

因此,我们要求解数组中第 k 大的元素,只需求原数组中排名第 (数组长度 - 1) 的元素。

代码使用之前的就行了。

代码实现:

/**
 * 215. 数组中的第K个最大元素
 */
public class FindKthLargestLeetcode215 {

    /*
     *   由大到小
     *   5   4   3   2   1
     *   由小到大
     *   0   1   2   3   4
     *   1   2   4   5   6
     *
     * */
    public int findKthLargest(int[] nums, int k) {
        return QuickSelect.quick(nums, 0, nums.length - 1, nums.length - k);
    }

    public static void main(String[] args) {
        // 应为5
        FindKthLargestLeetcode215 code = new FindKthLargestLeetcode215();
        System.out.println(code.findKthLargest(new int[]{3, 2, 1, 5, 6, 4}, 2));
        // 应为4
        System.out.println(code.findKthLargest(new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6}, 4));
    }
}

注意:这并不代表小顶堆这个算法不好,小顶堆在数据流的场景下会用到,而快速选择算法只用于数组。


我们再来看一道题目:


数组中位数:

现在任意给出一组数组,比如说 1, 2, 4, 5, 6, 求数组中的中位数。

分析:

这道题就是前面求数组中第 i 小的元素的一个变式, 如果数组内的元素个数为奇数时, 那就是求 第 (数组长度) /  2 个元素。

如果数组内的元素个数是偶数,需要求两个元素, 一个是 arr.length / 2,  另一个是 arr.length / 2 - 1(这里指的都是索引)

代码实现:  

/**
 * 数组中的中位数 - 快速选择
 */
public class FindMedian {

    /*
    *                           中位数 (索引)
    *   0   1   2   3   4   ==> arr.length / 2
    *   1   2   4   5   6
    *
    *   0   1   2   3     ==>
    *   1   2   4   5           arr.length / 2,   arr.length / 2 - 1
    *
    * */
    public static double findMedian(int[] nums){
        if(nums.length % 2 == 1){  //奇数
            return QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
        }else{ //偶数
            int p = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2);
            int q = QuickSelect.quick(nums, 0, nums.length - 1, nums.length / 2 - 1);
            return (p + q) / 2.0;
        }
    }

    public static void main(String[] args) {
        System.out.println("偶数");
        System.out.println(findMedian(new int[]{3, 1, 5, 4}));
        System.out.println(findMedian(new int[]{3, 1, 5, 4, 7, 8}));
        System.out.println("奇数");
        System.out.println(findMedian(new int[]{4, 5, 1}));
        System.out.println(findMedian(new int[]{4, 5, 1, 6, 3}));
    }
}

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值