https://blog.csdn.net/2601_95366422/article/details/159013900
上节课链接
一.题目
LCR 159. 库存管理 III - 力扣(LeetCode)

二.思路讲解
2.1 思路讲解
本题同样是TopK问题 ,只不过要求的是最小的 cnt 个元素 ,而不是最大的。因此,我们可以沿用上一章学习的快速选择算法 ,但需要将比较逻辑调整为升序划分 。快速选择的核心思想是:通过一次三路划分 ,将数组分为小于基准 、等于基准 、大于基准 三个区域,然后根据 cnt 落在哪个区域,只递归处理那个区域 ,从而在期望 O(n) 时间内找到第 cnt 小的元素,并使得前 cnt 个位置恰好包含所有最小的 cnt 个元素(顺序不限)。
具体来说,在每一次递归中,随机选择一个基准值 key,然后使用三个指针将当前区间划分为:
-
左区间
[l, left]:所有小于key的元素 -
中区间
[left+1, right-1]:所有等于key的元素 -
右区间
[right, r]:所有大于key的元素
接下来,计算左区间长度 a 和等于区间长度 b。根据 cnt 与 a、a+b 的关系:
-
如果
cnt ≤ a,说明第 cnt 小的元素在左区间,递归处理左区间,cnt 不变。 -
如果
a < cnt ≤ a+b,说明第 cnt 小的元素就是基准值key,此时最小的 cnt 个元素已经确定------它们由左区间全部元素和等于区间的前cnt-a个元素组成,可以直接返回(由于顺序不限,我们只需确保前 cnt 个位置是这些元素即可)。 -
如果
cnt > a+b,说明第 cnt 小的元素在右区间,递归处理右区间,但需要更新 cnt = cnt - a - b。
通过这种分而治之 的策略,每次递归只处理一个子区间,平均时间复杂度为 O(n)。由于我们只关心最小的 cnt 个元素,而不需要整体排序。
三.代码演示
cpp
class Solution {
public:
vector<int> inventoryManagement(vector<int>& stock, int cnt)
{
int n = stock.size();
srand(time(NULL));
qsort(stock,0,n-1,cnt);
return {stock.begin(),stock.begin()+cnt};
}
void qsort(vector<int>& stock,int l,int r,int cnt)
{
if(l >= r) return;
//三路划分
int key = getRandom(stock,l,r);
int i = l,left = l - 1,right = r + 1;
while(i < right)
{
if(stock[i] < key)
swap(stock[++left],stock[i++]);
else if(stock[i] == key)
i++;
else
swap(stock[--right],stock[i]);
}
int leftlen = left - l + 1;//左区间长度
int midlen = right - left - 1;//中间长度
if(cnt <= leftlen)
{
qsort(stock,l,left,cnt);
}
else if(cnt <= leftlen + midlen)
{
return;
}
else
{
qsort(stock,right,r,cnt -leftlen - midlen);
}
}
int getRandom(vector<int>& stock,int l,int r)
{
int p = rand();
return stock[p % (r - l + 1) + l];
}
};
四.代码讲解
一、随机基准值的选择
为了避免快速选择算法在特定输入下退化为 O(n²) ,我们采用随机化策略 。在 getRandom 函数中,通过 rand() % (r - l + 1) + l 生成一个位于当前区间 [l, r] 内的随机下标,并将该位置的元素作为基准值 key。这样,每次划分的基准都是随机的,使得算法在期望时间复杂度上达到 O(n) 。在排序前调用 srand(time(NULL)) 设置随机种子,确保每次运行产生的随机数不同。
二、三路升序划分
为了高效处理重复元素,我们使用三路划分 ,将数组划分为小于基准 、等于基准 、大于基准三个区域(即升序排列)。定义三个指针:
-
left:初始为l - 1,指向小于区域的最后一个元素(即小于区域为空)。 -
right:初始为r + 1,指向大于区域的第一个元素(即大于区域为空)。 -
i:初始为l,用于遍历当前元素。
循环条件为 i < right,遍历过程中根据 stock[i] 与 key 的关系分三种情况:
-
当
stock[i] < key时 :该元素应放入小于区域 。先将left右移一位(++left),然后交换stock[left]与stock[i],接着i++。此时left指向新放入的小于元素,而i继续向后。 -
当
stock[i] == key时 :该元素属于等于区域 ,无需移动,直接i++。 -
当
stock[i] > key时 :该元素应放入大于区域 。先将right左移一位(--right),然后交换stock[right]与stock[i]。注意,此时i不能增加 ,因为交换过来的元素来自right位置,尚未被处理,需要在下一次循环中重新判断。
循环结束后,数组被划分为:
-
[l, left] :全部小于
key的元素 -
[left+1, right-1] :全部等于
key的元素 -
[right, r] :全部大于
key的元素
三、根据区域大小确定最小 cnt 个元素的所在区间
由于我们要找的是最小的 cnt 个元素,而当前数组是升序划分,因此小于区域 在前,等于区域 居中,大于区域在后。我们需要计算各个区域的长度:
-
左区间(小于区域)长度 :
leftlen = left - l + 1 -
中间区间(等于区域)长度 :
midlen = right - left - 1
接下来,根据 cnt 与这些长度的关系,决定下一步操作:
-
如果
cnt <= leftlen:说明最小的 cnt 个元素全部在小于区域 中,递归处理左区间[l, left],且 cnt 保持不变。 -
如果
leftlen < cnt <= leftlen + midlen:说明最小的 cnt 个元素由整个小于区域和等于区域的一部分组成,此时基准值key就是第 cnt 小的元素,且前 cnt 个位置已经包含了所有需要的元素(因为等于区域的部分元素就在中间),因此无需继续递归,直接返回即可。 -
如果
cnt > leftlen + midlen:说明最小的 cnt 个元素中还包含了大于区域中的一部分,需要递归处理右区间[right, r],但此时需要更新 cnt ,减去前面两个区域的长度,即新的cnt = cnt - leftlen - midlen。
四、递归处理对应区间
根据上述判断,只对包含目标元素的区间进行递归,其他区间被舍弃。这样每次递归的规模平均减半,保证了线性期望时间复杂度。
五、返回结果
在 inventoryManagement 函数中,我们调用 qsort 对整个数组进行快速选择。当递归结束时,数组的前 cnt 个元素(即下标 0 到 cnt-1)就是所有最小的 cnt 个元素(顺序不限)。因此,我们直接返回 {stock.begin(), stock.begin()+cnt} 即可。
六、关键细节
-
随机化:随机选择基准值是避免最坏情况的关键,使得算法在概率上高效。
-
三路划分:将等于基准值的元素集中在一起,避免了重复元素带来的冗余递归,同时简化了区间判断。
-
升序划分:代码采用小于在左、大于在右的方式,直接对应最小元素的概念。
-
cnt 的更新:当递归进入大于区域时,需要减去前面区域的大小,因为最小元素的位置在全局中发生了变化。
-
递归终止:当 cnt 落在等于区域时直接返回,无需进一步递归,这是快速选择比快排更高效的原因之一。