

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