快速选择算法

快速选择算法的核心是基于 分治策略三路划分,通过不断缩小问题规模来定位目标元素。无论是找第k大还是前k小,其底层逻辑均依赖于:

  • 基准值的划分:将数组划分成小于、等于、大于基准值的三部分
  • 递归方向控制:根据目标位置(k)与当前划分区间的关系,决定递归处理左或右数组

215. 数组中的第K个最大元素 - 力扣(LeetCode)

直接来看题目:

给定整数数组 nums 和整数 k,请返回数组中k 个最大的元素

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现**时间复杂度为 O(n)**的算法解决此问题。

示例 1:

复制代码
输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:

复制代码
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

刚看到题目时,我想到的是冒泡排序,通过重复遍历数组,比较相邻元素,若顺序错误则交换,每一轮遍历后,当前未排序部分的最大元素会"冒泡"到末尾

java 复制代码
public class BubbleSortKthLargest {
    public static int findKthLargest(int[] nums, int k) {
        // 边界检查
        if (k < 1 || k > nums.length) {
            throw new IllegalArgumentException("k is out of bounds");
        }
        
        // 冒泡排序(升序)
        bubbleSort(nums);
        
        // 返回第k大元素(倒数第k个元素)
        return nums[nums.length - k];
    }
    
    private static void bubbleSort(int[] nums) {
        int n = nums.length;
        boolean swapped;
        for (int i = 0; i < n - 1; i++) {
            swapped = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    // 交换相邻元素
                    swap(nums, j, j + 1);
                    swapped = true;
                }
            }
            // 若未发生交换,说明数组已有序,提前终止
            if (!swapped) break;
        }
    }
    
    private static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
    public static void main(String[] args) {
        int[] nums = {3, 2, 1, 5, 6, 4};
        int k = 2;
        System.out.println("第" + k + "大元素是: " + findKthLargest(nums, k)); // 输出 5
    }
}

但是仔细看题目,其中要求了时间复杂度为O(n),但是冒泡排序的时间复杂度为O(n^2)

这时候就可以用到快速选择算法,核心是基于 分治策略三路划分

分治策略,就是分而治之,把这个数组分成几部分来处理,三路划分如下所示

里面的参数意思分别是:

l是数组的左端点,即为0,r是数组的右端点,即为nums.length-1。

key是从数组里随机选的基准,通过key来对数组中的元素进行比较,加上三指针方法,划分除三块区域

这样就可以通过k与三路中的元素个数进行比较,同时三路的区间范围也可以表述出来:

<key : left - l + 1,定义为a

=key : right - left - 1 ,计算过程为right -1 -(left + 1) + 1, 定义为b

>key : r - right - 1,定义为c

其实只需要上面两个变量,就能得到最后一个变量

可以先把这部分代码写出来:

java 复制代码
class Solution {
    public int findKthLargest(int[] nums, int k) {
        return qsort(nums, 0, nums.length - 1, k);
    }

    public int qsort(int[] nums, int l, int r, int k){
        //处理边界情况
        if(r == l) return nums[l];

        
        //1.选定基准
        int key = nums[new Random().nextInt(r - l + 1) + l];

        //2.三指针划分三块范围
        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

       
    }

        
    //元素交换
    public void swap(int[] nums, int i, int j){
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t; 
    }
}

关于划分三块范围这部分代码,单独拿出来说一下,也算是对自己的一个小复习

java 复制代码
        //2.三指针划分范围
        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

left的作用是:标记小于基准值key的区域的右边界(闭区间)

i的作用:当前遍历指针,用于扫描数组元素

为什么需要++left和i++ ==> 同步拓展小于区

当 numsi < key 时,需要将numsi交换到小于区的末尾。

  • ++left:将 left 向右移动一位,扩展小于区的右边界。
  • i++:将 i 向右移动一位,继续扫描下一个元素。
  • 目的 :确保交换后的 nums[left] 属于小于区,且 i 不会重复处理已交换的元素。

同样--righ也是这个道理,但是交换过后i不需要++,因为此时i位置上的元素是 与right交换来的元素,还没有与key比较过

后面的步骤,就是拿k与区间内的元素个数进行比较,思路如下:

对应的代码部分为:

java 复制代码
    public int qsort(int[] nums, int l, int r, int k){
        if(r == l) return nums[l];

        int key = nums[new Random().nextInt(r - l + 1) + l];

        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        int c = r - right + 1, b = right - left - 1;
        if(k <= c) return qsort(nums, right, r, k);
        else if(k <= c + b) return key;
        else return qsort(nums, l, left, k - b - c);
    }

完整代码如下:

java 复制代码
class Solution {
    public int findKthLargest(int[] nums, int k) {
        return qsort(nums, 0, nums.length - 1, k);
    }

    public int qsort(int[] nums, int l, int r, int k){
        if(r == l) return nums[l];

        int key = nums[new Random().nextInt(r - l + 1) + l];

        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        int c = r - right + 1, b = right - left - 1;
        if(k <= c) return qsort(nums, right, r, k);
        else if(k <= c + b) return key;
        else return qsort(nums, l, left, k - b - c);
    }

    public void swap(int[] nums, int i, int j){
        int t = nums[i];
        nums[i] = nums[j];
        nums[j] = t; 
    }
}

面试题 17.14. 最小K个数 - 力扣(LeetCode)

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例:

复制代码
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]

这道题目,没有对时间复杂度进行要求,所以暴力解法就可以把数组进行排序,然后返回前k个元素。不过为了更方便一些,可以使用快速选择算法,因为题目并没有要求一定要按顺序返回,快速选择算法可以不用对每一块里的元素都排序,就可以返回结果

思路和前面一样:通过 分治策略三路划分来进行,先把数组划分成三部分,不过因为要返回的是数组,所以要创建一个数组,来进行返回

java 复制代码
class Solution {
    public int[] smallestK(int[] nums, int k){
        qsort(nums, 0, nums.length - 1, k);

        int[] ret = new int[k];
        for(int i = 0; i < k; i++){
            ret[i] = nums[i];
        }
        return ret;
    }

    public void qsort(int[] nums, int l, int r, int k){
        if(l > r) return;

        int key = nums[new Random().nextInt(r - l + 1) + l];

        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

      
    }

    public void swap(int[] nums, int j ,int k){
        int t = nums[j];
        nums[j] = nums[k];
        nums[k] = t;
    }
}

这里要求返回前k个小的元素,判定条件要进行修改

对应的完整代码为:

java 复制代码
class Solution {
    public int[] smallestK(int[] nums, int k){
        qsort(nums, 0, nums.length - 1, k);

        int[] ret = new int[k];
        for(int i = 0; i < k; i++){
            ret[i] = nums[i];
        }
        return ret;
    }

    public void qsort(int[] nums, int l, int r, int k){
        if(l > r) return;

        int key = nums[new Random().nextInt(r - l + 1) + l];

        int left = l - 1, right = r + 1, i = l;
        while(i < right){
            if(nums[i] < key) swap(nums, ++left, i++);
            else if(nums[i] == key) i++;
            else swap(nums, --right, i);
        }

        int a = left - l + 1, b = right - left - 1;
        if(k < a) qsort(nums, l, left, k);
        else if(k <= a + b) return;
        else qsort(nums, right, r, k - a - b);
    }

    public void swap(int[] nums, int j ,int k){
        int t = nums[j];
        nums[j] = nums[k];
        nums[k] = t;
    }
}
相关推荐
江畔柳前堤7 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore9 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考10 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX11 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面11 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号12 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽12 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI12 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者14 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者14 小时前
【征程6】校准量化中HistogramObserver解析
算法