快速排序算法学习记录

快速排序算法学习记录

1. 算法简介

快速排序由 Tony Hoare 于 1959 年提出,是一种基于分治思想的高效排序算法。

核心步骤:

  1. 选择基准(Pivot):从数组中选取一个元素作为基准值
  2. 分区(Partition):将数组重新排列,使得小于基准的元素放在左侧,大于基准的放在右侧
  3. 递归排序:对左右两个子数组递归执行上述过程

简略示意图

复制代码
初始数组: [3, 6, 2, 7, 1, 5, 4]  选择 pivot = 4

分区过程:
         [3, 2, 1]   4   [6, 7, 5]
         小于 pivot   ↑   大于 pivot

递归排序:
         [1, 2, 3]   4   [5, 6, 7]
         ─────────   ↑   ─────────
           已排序    基准   已排序

最终结果: [1, 2, 3, 4, 5, 6, 7]

2. 算法优点

优点 说明
平均时间复杂度低 平均时间复杂度为 O(n log n),在实际应用中通常表现优异
原地排序 空间复杂度为 O(log n)(递归栈空间),不需要额外的数组空间
缓存友好 数据局部性好,CPU 缓存命中率高
实现简洁 核心逻辑(分区 + 递归)代码量很少,易于理解和实现
内排序 所有操作在内存中完成,不涉及磁盘 I/O

3. 算法缺点

尽管快速排序在平均情况下表现优异,但它并非完美无缺

3.1 最坏时间复杂度 O(n²)

当数组已经有序所有元素相等时,如果每次选取的 pivot 都是当前子数组的最大/最小值,递归树会退化成一条链:

复制代码
最坏情况示意图(已排序数组,每次选第一个作为 pivot):

[1, 2, 3, 4, 5, 6, 7]  pivot=1
 ↑
[1]  [2, 3, 4, 5, 6, 7]  pivot=2
      ↑
     [2]  [3, 4, 5, 6, 7]  pivot=3
           ↑
          [3]  [4, 5, 6, 7]
                ...

递归深度: n 层
时间复杂度: O(n²)

3.2 不稳定排序

相等元素的相对位置可能在分区过程中被打乱,属于不稳定排序算法。

3.3 对于小数组效率低

递归开销在数据量小时反而不如插入排序等简单算法。


4. 改进方案

针对上述缺点,尤其是最坏 O(n²) 的问题,引入两种经典优化:

4.1 随机取 Key(Randomized Pivot)

核心思想 :不再固定选取第一个或最后一个元素作为 pivot,而是随机选取

复制代码
原始数组: [1, 2, 3, 4, 5, 6, 7]  (已排序)

固定选首元素: 每次 pivot 都是最小值 → O(n²)
随机选 pivot: 可能选到 4 → 平衡分区 → O(n log n)

随机化后期望:
        [1, 2, 3]   4   [5, 6, 7]
        ─────────       ─────────
          左子树        右子树

期望递归深度: O(log n)
期望时间复杂度: O(n log n)

优点:从概率意义上消除了最坏情况,使得算法在期望意义上达到 O(n log n)。


4.2 三路划分(Three-way Partition)

核心思想 :将数组划分为三个区域:小于 pivot等于 pivot大于 pivot

复制代码
传统两路划分:        三路划分:

[ < pivot | > pivot ]    [ < pivot | = pivot | > pivot ]
    ↑   ↑                    ↑       ↑       ↑
   left right               lt      gt     right

三路划分过程示例:
数组: [2, 4, 3, 4, 1, 4, 5], pivot = 4

遍历中:
[2, 1, 3, 4, 4, 4, 5]
        ↑     ↑
       lt    gt

结果:
[2, 1, 3]  [4, 4, 4]  [5]
  < 4       = 4       > 4

为什么需要三路划分?

当数组中存在大量重复元素 时,传统快速排序会将所有等于 pivot 的元素分到一边,导致分区极度不平衡。三路划分将重复元素"挤"到中间,跳过对等于 pivot 区域的递归,大幅提升效率。

时间复杂度优化

  • 大量重复元素场景下:从 O(n²) 优化到 O(n)

5. 总结

特性 基础快排 随机化快排 三路快排
平均时间 O(n log n) O(n log n) O(n log n)
最坏时间 O(n²) O(n²)(概率极低) O(n²)
重复元素处理 较差 一般 优秀
适用场景 通用 通用(防攻击) 重复元素多

实践建议 :现代标准库(如 C++ std::sort、Java Arrays.sort)通常采用混合策略------小数组用插入排序,中等数组用随机化快排,大量重复元素时切换到三路划分,以达到最优的综合性能。


练习题目

LeetCode 75. 颜色分类

题目链接:75. 颜色分类

这道题是三路划分的经典应用。数组中只有 0、1、2 三种元素,本质上就是把数组划分为三个区域:

元素值 对应三路划分 区域含义
0 < pivot 左侧区域
1 = pivot(pivot 取 1) 中间区域
2 > pivot 右侧区域
执行过程示例

nums = [2, 0, 2, 1, 1, 0] 为例,演示三路划分过程:

复制代码
初始: [2, 0, 2, 1, 1, 0]   left=-1, right=6, i=0
       ↑
       i

步骤1: nums[0]=2 → 与 right-1=5 交换
      [0, 0, 2, 1, 1, 2]   right=5
       ↑              ↑
       i            right

步骤2: nums[0]=0 → 与 left+1=0 交换,left++, i++
      [0, 0, 2, 1, 1, 2]   left=0, i=1
          ↑
          i

步骤3: nums[1]=0 → 与 left+1=1 交换,left++, i++
      [0, 0, 2, 1, 1, 2]   left=1, i=2
             ↑
             i

步骤4: nums[2]=2 → 与 right-1=4 交换,right--
      [0, 0, 1, 1, 2, 2]   right=4
             ↑
             i

步骤5: nums[2]=1 → i++
      [0, 0, 1, 1, 2, 2]   i=3
                ↑
                i

步骤6: nums[3]=1 → i++
      [0, 0, 1, 1, 2, 2]   i=4
                   ↑
                   i

结束: i == right,停止
边界条件说明
区间类型 初始化 已确认区域 特点
开区间 left=-1, right=n [0..left] 是 0,[right..n-1] 是 2 代码简洁,无需处理越界
闭区间 left=0, right=n-1 [0..left-1] 是 0,[right+1..n-1] 是 2 与快排递归接口更统一

两种写法本质上等价,闭区间版本在与其他算法(如快排递归)衔接时更自然。

代码实现

开区间版本(推荐用于本题):

cpp 复制代码
class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left = -1;               // [0..left] 区域存放 0
        int right = nums.size();      // [right..n-1] 区域存放 2
        int i = 0;                    // 当前扫描指针,指向待处理区域

        while (i < right) {
            if (nums[i] == 0) {
                // 遇到 0:扩展到左侧区域
                // i 右移,因为交换过来的元素来自 [left+1..i-1],只可能是 0 或 1,都合法
                swap(nums[i], nums[++left]);
                i++;
            } else if (nums[i] == 1) {
                // 遇到 1:属于中间区域,直接跳过
                i++;
            } else {
                // 遇到 2:扩展到右侧区域
                // i 不能右移!因为交换过来的元素来自未处理区域 [i+1..right-1],需要重新判断
                swap(nums[i], nums[--right]);
            }
        }
    }
};

闭区间版本(与快排递归风格统一):

cpp 复制代码
class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left = 0;                    // [0..left-1] 区域存放 0
        int right = nums.size() - 1;      // [right+1..n-1] 区域存放 2
        int i = 0;                        // 当前扫描指针

        while (i <= right) {
            if (nums[i] == 0) {
                swap(nums[i], nums[left]);
                left++;
                i++;
            } else if (nums[i] == 1) {
                i++;
            } else {
                swap(nums[i], nums[right]);
                right--;
                // i 不移动,重新判断交换过来的元素
            }
        }
    }
};
复杂度分析
  • 时间复杂度:O(n),每个元素最多被交换两次(一次移到边界,一次可能被交换到中间),指针单向扫描。
  • 空间复杂度:O(1),仅使用常数额外空间,原地完成划分。

LeetCode 912. 排序数组

题目链接:LeetCode 912. 排序数组

这道题是快速排序的标准模板题 ,要求对数组进行排序。由于数组中元素取值范围很大(包含负数),无法使用计数排序,因此需要借助基于比较的排序算法。这里采用随机化 + 三路划分的快排实现,既保证期望 O(n log n) 的时间复杂度,又能处理大量重复元素的情况。

代码实现
cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        qsort(nums, 0, nums.size() - 1);
        return nums;
    }

    void qsort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return;

        // 随机取 key
        int ra = rand();
        int key = nums[ra % (right - left + 1) + left];

        int i = left;
        int l = left, r = right;  // 保存原始边界,供递归使用

        // 三路划分
        while (i <= right)
        {
            if (nums[i] < key)
            {
                swap(nums[left], nums[i]);
                i++, left++;
            }
            else if (nums[i] == key)
            {
                i++;
            }
            else if (nums[i] > key)
            {
                swap(nums[right], nums[i]);
                right--;
            }
        }

        // 递归排序左右子区间
        qsort(nums, l, left - 1);
        qsort(nums, right + 1, r);
    }
};
要点一:随机数取 Key
cpp 复制代码
int ra = rand();
int key = nums[ra % (right - left + 1) + left];

这行代码是随机化快排的核心。拆解来看:

  • right - left + 1:当前子数组的长度(偏移量) ,即区间 [left, right] 内共有多少个元素。
  • ra % (right - left + 1):对长度取模,得到一个 [0, 长度-1] 范围内的随机偏移。
  • + left:将偏移量平移 到以 left 为起始点的实际索引上。

例如,若 left = 3, right = 7,则区间长度为 5rand() % 5 得到 0~4 的随机偏移,加上 left = 3 后,实际索引范围就是 3~7,恰好覆盖整个子数组。

为什么不用 rand() % nums.size() 因为那样会取到子数组范围之外的元素,破坏分治的正确性。必须在当前子区间 [left, right] 内随机选取。

要点二:闭区间传入与边界转换

外层调用传入的是闭区间

cpp 复制代码
qsort(nums, 0, nums.size() - 1);

但在 qsort 函数内部,三路划分过程中 leftright 这两个参数变量被当作指针移动 ,其语义已经不再是"原始边界",而是三路划分中 < key> key 的区域边界。因此:

  • 划分结束后,left 指向的是 = key 区域的左边界(第一个等于 key 的位置)
  • right 指向的是 = key 区域的右边界(最后一个等于 key 的位置)

所以真正的左右待排序子区间是:

  • 左侧:[原始 left, left - 1],即 [l, left - 1]
  • 右侧:[right + 1, 原始 right],即 [right + 1, r]

这就是为什么递归调用要写成:

cpp 复制代码
qsort(nums, l, left - 1);      // 左子区间
qsort(nums, right + 1, r);    // 右子区间

而不是直接复用被修改后的 leftright

要点三:易踩的坑------递归边界不是向 0 和 size 靠拢

初学者容易有一种直觉:快排的递归是"从两边向中间收拢",所以左右递归应该分别向 0nums.size() 方向推进。于是可能写出这样的错误代码

cpp 复制代码
// ❌ 错误写法
qsort(nums, 0, left - 1);
qsort(nums, right + 1, nums.size());

错误原因 :快排是分治 算法,每次递归只处理当前子数组的左右两个子区间,而不是每次都从整个数组的两端开始。如果写成 0nums.size(),会导致:

  1. 左递归每次都从数组开头开始,严重重复处理已经排好序的前半部分;
  2. 右递归越界(nums.size() 已经超出有效索引范围)。

正确的做法是用临时变量 lr 保存原始边界,递归时严格限定在当前子区间内:

cpp 复制代码
// ✅ 正确写法
int l = left, r = right;  // 先保存原始边界
// ... 三路划分,left 和 right 被移动 ...
qsort(nums, l, left - 1);      // 只在左子区间递归
qsort(nums, right + 1, r);     // 只在右子区间递归
复杂度分析
  • 时间复杂度:期望 O(n log n)。随机化 pivot 使得递归树期望高度为 O(log n),每层 O(n) 的划分工作量。
  • 空间复杂度:O(log n),递归栈深度。三路划分在大量重复元素时可将栈深度控制在 O(log n)(因为等于 key 的元素不再递归)。

堆排序回顾

除了快速排序,堆排序(Heap Sort) 也是一道非常经典的 O(n log n) 排序算法,且最坏情况下时间复杂度稳定为 O(n log n),不受数据分布影响。

核心思想

堆排序基于二叉堆(通常用大根堆)这一数据结构:

  1. 建堆:将无序数组构建成一个大根堆,使得堆顶元素是最大值。
  2. 交换:将堆顶元素(最大值)与堆尾元素交换,此时最大值已归位。
  3. 调整:堆大小减 1,对新的堆顶元素执行**下沉(Heapify)**操作,恢复大根堆性质。
  4. 重复:重复步骤 2~3,直到堆中只剩一个元素。
简略示意图
复制代码
初始数组: [4, 10, 3, 5, 1]

建堆(大根堆):
        10
      5    3
    4        1

交换堆顶与末尾: [1, 5, 3, 4, 10]  (10 已归位)
调整堆(大小=4):
        5
      4   3
    1

交换堆顶与末尾: [1, 4, 3, 5, 10]  (5 已归位)
调整堆(大小=3):
        4
      1   3

交换堆顶与末尾: [1, 3, 4, 5, 10]  (4 已归位)
调整堆(大小=2):
        3
       /
      1

交换堆顶与末尾: [1, 3, 4, 5, 10]  (3 已归位)

最终结果: [1, 3, 4, 5, 10]
代码框架
cpp 复制代码
void heapSort(vector<int>& nums) {
    int n = nums.size();
    // 1. 建堆(从最后一个非叶子节点开始下沉)
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(nums, n, i);

    // 2. 逐个提取最大值
    for (int i = n - 1; i > 0; i--) {
        swap(nums[0], nums[i]);     // 堆顶移到末尾
        heapify(nums, i, 0);         // 对剩余元素重新调整堆
    }
}

void heapify(vector<int>& nums, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    if (left < n && nums[left] > nums[largest])
        largest = left;
    if (right < n && nums[right] > nums[largest])
        largest = right;

    if (largest != i) {
        swap(nums[i], nums[largest]);
        heapify(nums, n, largest);   // 递归下沉
    }
}
与快排对比
特性 快速排序 堆排序
平均时间 O(n log n) O(n log n)
最坏时间 O(n²) O(n log n)
空间复杂度 O(log n) O(1)
稳定性 不稳定 不稳定
缓存友好 优秀(局部访问) 一般(跳跃访问)
适用场景 通用场景 内存极度敏感、需严格保证最坏复杂度

总结:快排在实践中通常更快(缓存友好),堆排序在最坏复杂度上更有保证。LeetCode 912 这道题两种方法都能通过,掌握快排的同时,堆排序作为"保底方案"也值得熟练掌握。

LeetCode 215. 数组中第 K 大的元素

题目链接:LeetCode 215. 数组中第 K 大的元素

这道题要求在无序数组中找出第 K 大的元素。最直观的想法是先排序再取倒数第 K 个,时间复杂度 O(n log n)。但其实我们不需要完全排序 ------只需要确定第 K 大元素在排序后的位置即可。这正是 快速选择(Quick Select) 算法的用武之地。

为什么能用快速选择?

快速选择是快速排序的变种,由 Tony Hoare 发明。核心洞察是:

一次三路划分后,pivot 的最终位置是确定的。 如果这个位置恰好就是第 K 大元素应该在的位置,直接返回即可;否则只需要去包含目标位置的那一侧继续查找。

换句话说,快排需要递归处理左右两个子区间,而快速选择只需要递归处理一个子区间 ,从而将平均时间复杂度从 O(n log n) 优化到 O(n)

快速选择对快排的优化
对比项 快速排序 快速选择
目的 完全排序整个数组 只找到第 K 小/大的元素
递归方向 左右两个子区间都递归 只递归包含目标位置的一侧
平均时间 O(n log n) O(n)
最坏时间 O(n²) O(n²)(概率极低)
空间复杂度 O(log n) O(log n)

时间复杂度 O(n) 的直观理解:第一次 partition 遍历 n 个元素,第二次最坏遍历 n/2,第三次 n/4......总和为 n + n/2 + n/4 + ... < 2n,即 O(n)。

要点一:第 K 大 → 坐标转换(易踩的坑)

题目要求"第 K 大",但我们的三路划分是基于升序逻辑进行的。因此需要做一个转换:

第 K 大元素 = 升序排列后索引为 n - k 的元素(0-based)

复制代码
示例:nums = [3, 2, 1, 5, 6, 4], k = 2

升序排列后:[1, 2, 3, 4, 5, 6]
                    ↑
                 索引 4(0-based)

第 2 大元素是 5,在升序数组中的索引 = 6 - 2 = 4

代码中这一转换体现为:

cpp 复制代码
int ret = qsort(nums, 0, size - 1, size - k);  // 转换成坐标

踩坑警示 :如果直接用 k 作为目标索引去判断,逻辑会完全相反(变成找第 K 小),导致答案错误。

代码实现
cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        int size = nums.size();
        int ret = qsort(nums, 0, size - 1, size - k); // 转换成坐标
        return ret;
    }

    int qsort(vector<int>& nums, int left, int right, int k)
    {
        if (left >= right)
            return nums[left];

        // 随机取 key
        int ra = rand();
        int key = nums[ra % (right - left + 1) + left];

        int i = left;
        int l = left, r = right;  // 保存原始边界

        // 三路划分
        while (i <= right)
        {
            if (nums[i] < key)
            {
                swap(nums[i], nums[left]);
                left++, i++;
            }
            else if (nums[i] == key)
            {
                i++;
            }
            else if (nums[i] > key)
            {
                swap(nums[i], nums[right]);
                right--;
            }
        }

        // 判断目标位置 k 落在哪个区域
        if (k > right)
            return qsort(nums, right + 1, r, k);   // 在 >key 的右侧区域
        else if (k < left)
            return qsort(nums, l, left - 1, k);    // 在 <key 的左侧区域
        else
            return key;                             // 在 =key 的中间区域,直接命中

        return -99;  // 永远不会执行到这里
    }
};
要点二:判断逻辑详解------为什么只看一侧?

三路划分结束后,数组被清晰地分成三个区域(以升序视角):

复制代码
[l     ->   left-1] [left  ->  right] [right+1   ->    r]
      < key              = key                > key
  • left=key 区域的左边界(第一个等于 key 的位置)
  • right=key 区域的右边界(最后一个等于 key 的位置)

目标索引 k 只可能落在三个区域之一:

情况 1:k > right

复制代码
目标索引在 right 的右边 → 落在 >key 区域

说明第 K 大元素比当前 key 更大,需要去右侧未排序区域继续查找。递归范围:[right + 1, r]

情况 2:k < left

复制代码
目标索引在 left 的左边 → 落在 <key 区域

说明第 K 大元素比当前 key 更小,需要去左侧未排序区域继续查找。递归范围:[l, left - 1]

情况 3:left <= k <= right

复制代码
目标索引恰好落在 [left, right] 之间 → 落在 =key 区域

说明第 K 大元素就是当前 key!因为这一整段区域的值都等于 key,无论 k 具体指向哪个位置,值都是 key。直接返回即可,无需再递归

这正是快速选择比快排快的原因:一旦命中目标区域,直接结束,不需要像快排那样继续递归左右子区间。

要点三:踩坑------递归调用不加 return

在快速选择中,递归调用后必须立即 return,否则函数会继续向下执行,导致逻辑错误。

cpp 复制代码
// ❌ 错误写法(曾经踩过的坑)
if (k > right)
    qsort(nums, right + 1, r, k);  // 忘加 return!
else if (k < left)
    qsort(nums, l, left - 1, k);
else
    return key;

// 如果进入第一个 if,执行完 qsort 后会继续往下走,
// 可能进入 else if 或 else,逻辑完全混乱
cpp 复制代码
// ✅ 正确写法
if (k > right)
    return qsort(nums, right + 1, r, k);  // 必须加 return
else if (k < left)
    return qsort(nums, l, left - 1, k);
else
    return key;

原因 :快速选择函数有返回值(int),每个分支都必须返回一个确定的值。如果不加 return,递归调用完成后会继续执行后续代码,导致返回值不确定或逻辑错误。

复杂度分析
  • 时间复杂度
    • 平均:O(n) 。每次 partition 后只处理一半数据:n + n/2 + n/4 + ... < 2n
    • 最坏:O(n²)。极端情况下每次 partition 都极不平衡(概率极低,随机化可有效避免)
  • 空间复杂度:O(log n),递归栈深度。

对比:如果用排序(快排/堆排)再做选择,时间复杂度是 O(n log n)。快速选择通过"只走一侧"将复杂度降为 O(n),是求 TopK 问题的经典解法。

LeetCode 面试题 17.14. 最小 K 个数

题目链接:面试题 17.14. 最小 K 个数

这道题要求返回数组中最小的 K 个数,且不要求这 K 个数有序 。基于这个特点,我们可以用快速选择(Quick Select)+ 三路划分来优化:不需要像完整快排那样递归排序左右两个子区间,只需要定位到第 K 小元素所在的索引位置,即可确定最小 K 个数的集合。

代码实现
cpp 复制代码
class Solution {
public:
    vector<int> smallestK(vector<int>& arr, int k) {
        if (k == 0)
            return vector<int>();
        // 传入 k-1 作为目标索引(0-based),即第 k 小元素的索引
        vector<int> ret = qsort(arr, 0, arr.size() - 1, k - 1);
        return ret;
    }

    vector<int> qsort(vector<int>& arr, int left, int right, int k) {
        if (left >= right)
            return vector<int>(arr.begin(), arr.begin() + k + 1);

        // 随机取 key
        int ra = rand();
        int key = arr[ra % (right - left + 1) + left];

        int l = left, r = right, i = left;

        // 三路划分
        while (i <= right) {
            if (arr[i] < key) {
                swap(arr[i], arr[left]);
                i++, left++;
            } else if (arr[i] == key) {
                i++;
            } else {
                swap(arr[i], arr[right]);
                right--;
            }
        }

        // 根据目标索引 k 落在哪个区域,决定下一步
        if (left > k)
            return qsort(arr, l, left - 1, k);
        else if (k >= left && k <= right)
            return vector<int>(arr.begin(), arr.begin() + k + 1);
        else
            return qsort(arr, right + 1, r, k);
    }
};
要点:三路划分后的三种情况处理

三路划分结束后,数组被清晰地分为三个区域(以升序视角):

复制代码
[l      ->     left-1] [left    ->    right] [right+1     ->      r]
      < key                  = key                  > key

设目标索引为 k(即第 k+1 小的元素,0-based),leftright 分别是 =key 区域的左右边界。根据 k 落在哪个区域,有三种处理方案:


情况 1:left > k(目标在 <key 区域)

复制代码
目标索引 k 位于 left 的左侧,即落在 <key 区域内

说明第 k+1 小的元素比当前 key 更小。此时所有 =key>key 的元素都不属于最小 K 个数,只需要在左侧未排序区间继续查找:

cpp 复制代码
return qsort(arr, l, left - 1, k);

情况 2:k >= left && k <= right(目标在 =key 区域)

复制代码
目标索引 k 恰好落在 [left, right] 之间

这是本题最关键的处理。此时意味着:

  • 所有 < key 的元素(共 left 个)已经位于数组最前面
  • 从索引 leftright 的元素都等于 key
  • 目标索引 k 落在这个区间内,说明第 k+1 小的元素就是 key

为什么可以直接返回 arr[0..k]

因为三路划分保证了所有小于 key 的元素都在索引 left 之前 ,而索引 leftright 的元素都等于 key。既然 k 落在 [left, right] 内,那么索引 0k 的所有元素必然都 <= key,且数组中其余元素都 >= key。因此,arr[0..k]k+1 个元素恰好就是整个数组中最小的 k+1 个元素(虽然它们内部不一定有序)。

此时直接返回即可:

cpp 复制代码
return vector<int>(arr.begin(), arr.begin() + k + 1);

这正是利用题目"返回任意顺序"的关键 :我们不需要关心这 K 个数内部谁前谁后,也不需要它们有序,只需要确保这 K 个数"正确"(即确实是全局最小的 K 个)。三路划分已经通过值的大小关系保证了这一点,因此命中 =key 区域时可以直接截断返回,无需继续递归。


情况 3:k > right(目标在 >key 区域)

复制代码
目标索引 k 位于 right 的右侧,即落在 >key 区域内

说明第 k+1 小的元素比当前 key 更大,需要在右侧未排序区间继续查找:

cpp 复制代码
return qsort(arr, right + 1, r, k);

与"返回任意顺序"的深层关联

题目明确说明返回的 K 个数可以是任意顺序。这个条件使得快速选择成为最优解法之一:

  1. 无需完全排序 :我们不需要像 std::sort 那样保证这 K 个数内部有序,只需要保证"前 K 个位置上的元素就是全局最小的 K 个"即可。
  2. =key 区域可以直接截断返回 :当 k 落在 =key 区域时,<key 区域和 =key 区域的一部分共同构成了最小 K 个数。虽然 <key 区域内部可能无序,=key 区域也只是值相等,但题目不要求顺序,所以无需继续递归排序。
  3. 平均时间复杂度 O(n):每次 partition 后只处理包含目标索引的一侧,期望时间复杂度为 O(n),优于完全排序的 O(n log n)。

如果题目要求这 K 个数必须有序 ,则命中 =key 区域时仍需要对 <key 区域继续递归排序,时间复杂度将退化为 O(n log n)。因此,"任意顺序"的要求是快速选择在此题中发挥 O(n) 优势的前提。

复杂度分析
  • 时间复杂度 :期望 O(n)。每次三路划分后只递归一个子区间,期望递归深度 O(log n),但每层工作量递减,总期望时间为 O(n)。
  • 空间复杂度:O(log n),递归栈深度。
相关推荐
AAA@峥2 小时前
系统化学习 MySQL:数据类型、库表管理、增删改查全解析
数据库·学习·mysql
梓䈑7 小时前
【算法题攻略】BFS 解决FloodFill 算法、最短路问题、多源BFS 和 拓扑排序
c++·算法·宽度优先
Adios7949 小时前
设置交集大小至少为2
数据结构·算法·leetcode
GeekArch14 小时前
第28讲:避坑——AI堆栈分配错误、栈溢出BUG
c语言·人工智能·stm32·mcu·学习·bug
weixin_4280053015 小时前
C#调用 AI学习从0开始-第3阶段RAG向量数据库-文档切分与入库第15天
人工智能·学习·c#·向量数据库·rag·qdrant·文档切分与入库
来一碗刘肉面15 小时前
栈的应用(表达式求值)
数据结构·算法
xiaowang1234shs16 小时前
怪兽轻断食技术深度测评:从断食计时引擎到AI识别算法的工程实践解析
数据库·人工智能·算法·macos·机器学习·p2p·visual studio
小果因子实验室16 小时前
量化研究--策略迁移算法1研究
算法