TopK问题 —— 线性时间选择

TopK问题 —— 线性时间选择

一、线性时间选择TopK问题与快速排序的关联

相信计算机专业的同学应该都对快速排序有或多或少的了解。
设置此模块是因为,线性时间选择TopK与快速排序的思想有相通之处,可以辅助我们理解。

快速排序的思路:

  1. 设置一个瞭望元素(划分元素)。
  2. 以此元素为基础,将工作区间 [l, r] 内的所有元素分割成两部分。划分元素以左均比其小,划分元素以右均比其大。
  3. 对分割元素左右两侧递归快排。

我们可以将快速排序算法分为以上三步,概括来讲就是三个函数:

  1. Qsort (int l, int r),它表示给 [l, r] 范围内的数排序
  2. GetPartition (int l, int r),他表示获取 [l, r] 范围内的分割元素下标
  3. Partition (int l, int r, int ParElem),他表示在ParElem号元素为分割元素分割 [l, r]

我们也都知道的一件事是,快速排序的性能不稳定。
而导致其性能不稳定的关键因素就是分割元素的选取。
如果分割元素每次都可以将工作区间近乎等分的话,就可以有O(N * lgN)的优秀性能。
如果分割元素每次都取在工作区间的最大值或最小值的话,那就要付出O(N ^ 2)的代价。
到此为止,我们复习了快速排序的思路分割元素的选取对性能有关键影响的结论。
希望大家谨记。

带着刚刚的收获,我们来看TopK问题。此后以在所有元素中选取第k大的元素为例。
试想我们已经按照快速排序的方法,以分割元素ParElem将区间分成了两部分。
即ParElem左边都是比他小的元素,ParElem右边都是比他大的元素。
那整个[l, r] 区间就可以被分成两部分 [l, ParElem - 1][ParElem, r]

递归状态转移
我们可以确定地是,问题的解不是在左半部分区间,就是在右半部分区间,而且右半部分区间地数字都比左半部分的数字大。

  • 如果右半部分区间长度RightLen大于k,那答案必然在右半部分中,并且它是右半部分中第k大的数
  • 如果右半部分区间长度RightLen小于k,那答案必然在左半部分中,并且它是作伴区间中第k - RightLen大的数

发现联系了没有,快速排序的GetPartitionPartition两个函数可以为求解TopK服务,我们需要做的只是将Qsort换成的对应的SelectK即可。

二、GetPartition函数的工作方式

还记得我们在上一模块得出的重要结论吗?
分割元素的选取对性能有关键影响

那么怎么选取分割元素比较好呢?
分割元素将区间分成的两部分长度越接近越好。
也就等价于,分割元素的数值大小是区间内数值的中位数最好

那么我们如何才能实现这一需求呢?

  1. 传统选取分割元素的方式:选取区间内第一个元素为分割元素。
    优点:操作简单;缺点:不稳定
  2. 随机选取分割元素的方式:通过一个随机数种子选取分割元素。
    优点:随机选取稳定性比第一种好了很多;缺点:计算机实现的伪随机仍存在不稳定因素

我们之前得到的分割元素数值大小最好是中位数,所以最好的办法应该是和中位数有关的。
当然,选取中位数的算法和TopK的复杂度是一致的了,显然不值得。
所以我们在此使用的选取分割元素的方法是和中位数有关,但他选取的不是整个工作区间内的中位数。

GetPartition函数描述

  1. 将整个工作区间 [l, r] 按照每SIZE个元素一组,分成多组。(SIZE表示分组的大小,是一个正整数)
  2. 对每个组进行排序。(因为每个组的元素个数是SIZE,是一个常数,所以对其进行排序操作的时间复杂度也是常数,只会影响整个TopK算法复杂度的系数,是值得的的)
  3. 将每个小组的中位数放到一组。(排完序就有中位数喽)
  4. 对第三步形成的中位数小组进行排序,并选取中位数
  5. 以第四步选取的中位数作为分割元素

比较复杂对不对?但这就是我们线性选择TopK的关键所在了。
选取了合适的分割元素后,就可以快乐的按照我们之前提到的PartitionSelectK,递归调用求解了。

三、代码

num是存放数字的数组
SIZE是分组时的小组长度

MIN_LEN是指,当我们的区间长度缩小到该程度后,就不必费劲的去递归求解了。
可以直接排序求解
int GetPartition(int l, int r)
{
    int len = r - l - +1;
    // SIZE个元素一组,可以分成多少组
    int GroupNum = (len - SIZE - 1) / SIZE;

    for (int i = 0; i < GroupNum; i++)
    {
        // 对第i组的元素进行排序
        sort(num + l + SIZE * i, num + l + SIZE * i + SIZE);
        // 将第i组元素中的中位数取出
        swap(num[l + i], num[l + SIZE * i + SIZE / 2]);
    }

    // 选择所有组别中位数中的中位数,即分割元素
    int ParElem = SelectK(l, l + GroupNum, GroupNum / 2);
    // 返回分割元素下标
    return ParElem;
}
}
int Partition(int l, int r, int ParElem)
{
    // [l, r]范围内的元素,以第ParELem号元素做分割元素,完成分割
    swap(num[l], num[ParElem]);
    int x = num[l];
    int i = l, j = r + 1;

    while (true)
    {
        while (num[++i] < x && i < r)
            ;
        while (num[--j] > x && j > l)
            ;

        if (i >= j)
            break;

        swap(num[i], num[j]);
    }

    num[l] = num[j];
    num[j] = x;

    // 返回做完分割后的分割元素下标
    return j;
}
int SelectK(int l, int r, int k)
{
    int len = r - l + 1;
    if (len < MIN_LEN)
    {
        // 当前区间长度缩小到MIN_LEN后,直接排序求解即可
        sort(num + l, num + r);
        // 因为是求第k大元素,所以返回第r - k + 1个元素
        return num[r - k + 1];
    }

    // 获得分割元素,并以该分割元素完成分割
    int ParElem = GetPartition(l, r);
    ParElem = Partition(l, r, ParElem);

    // RL表示区间[ParElem, r]的长度
    // 如果求第k小就应该看左区间长度了
    int RL = r - ParElem + 1;
    if (k <= RL)
    {
        // 说明答案是在右区间的第k大元素
        return SelectK(ParElem, r, k);
    }
    else
    {
        // 说明答案是左区间的第k - RL个元素
        return SelectK(l, ParElem - 1, k - RL);
    }
}

四、线性复杂度的原因

我们已经知道该问题的解法与快排及其相似,PartitionGetPartirtion两个函数甚至是完全一样的。那为什么快排的复杂度是O(N * lgN),而本算法的复杂度是O(N)呢?
因为快排是分治,而本算法是减治

这两者有何分别呢?
在快排中,选出分割元素并完成分割后,我们要做什么呢?递归对分割元素左右两边求解。也就是说,在快排中我们完成分割后,分割元素左右两侧仍然是需要求解的,因为我们要进行的是排序。这就是分治,将整个问题集合分成小问题来逐一解决。
那在TopK中,选出分割元素并完成分割后,我们要做什么呢?递归对分割元素左侧或右侧求解。 也就是说,左右两侧我们只需求解一侧即可。因为这是选择性问题,我们可以确定答案在某一侧,那另一侧就不需要求解了。这就是减治,将问题集合分开后选择一个子集和求解。

  • 15
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值