快速选择算法

快速选择算法的核心是基于 分治策略三路划分,通过不断缩小问题规模来定位目标元素。无论是找第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++ ==> 同步拓展小于区

当 nums[i] < key 时,需要将nums[i]交换到小于区的末尾。

  • ++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;
    }
}
相关推荐
酌沧2 小时前
大模型量化技术全解析
人工智能·python·算法
ULTRA??2 小时前
QT向量类实现GJK碰撞检测算法3d版本
c++·qt·算法
仰泳的熊猫2 小时前
1092 To Buy or Not to Buy
数据结构·c++·算法·pat考试
罗湖老棍子2 小时前
【深基16.例3】二叉树深度(洛谷P4913)
数据结构·算法·二叉树
Charlo2 小时前
Open-AutoGLM Windows 安装部署教程
算法·设计模式·github
君义_noip2 小时前
信息学奥赛一本通 4017:【GESP2309三级】小杨的储蓄 | 洛谷 B3867 [GESP202309 三级] 小杨的储蓄
c++·算法·gesp·信息学奥赛
高山上有一只小老虎3 小时前
判断是否为数独数组
java·算法
宝贝儿好3 小时前
【强化学习】第二章:老虎机问题、ε-greedy算法、指数移动平均
人工智能·python·算法