关于最小的k个数的讨论(top-k问题)

From: http://blog.csdn.net/pennyliang/article/details/4642641

给定一个长度为n的序列,不妨设为L1,L2,L3,….,Ln。这个序列可以是任意一种排列,可能的排列有n!种,我们要找到最小的k个数,即找到这样的k个数{ Li(1),Li(2),Li(3)…,Li(k)},并满足Li(1)<=Li(2)<=Li(3)…<=Li(k);且对任意的j:k+1<=j<=n,有Li(k)<=Li(j),。

    例如:有这样一个长度为8的序列{1,4,1,5,6,7,4*,6},找出最小的3个数,则结果为{1,1,4}或者{1,1,4*},而不是最小的k个排序码{1,4,5}。

        我们只讨论n较大的情况,不妨设n至少百万数量级(M),下面对k进行逐一的分析:

    首先,回顾一下几大类排序

    冒泡排序

    选择排序:{简单选择排序,锦标赛排序,堆排序}

    快速排序,快速排序也可以看作一种选择排序。

    插入排序:{直接插入排序,希尔排序}

    归并排序:

    基数排序:

    计数排序:

    其中,适合topk问题的排序,只有冒泡排序,选择排序和快速排序,其余的排序都必须在排序最后一刻才能知道谁是最小的k个元素。

    如果k = 1,此时很显然只能在简单选择排序和冒泡排序中考虑,因为锦标赛排序和堆排序初始化的成本太大。而冒泡排序在最坏情况下需要有3(N-1)次移动,即如果初始排列正好是降序的情况。而简单选择排序,只需要扫描一遍,无需移动数据。但冒泡排序有一个其他排序都不具备的性质就是,如果初始排列恰好是有序的(升序),则一趟冒泡就可以知道这个信息。因此在k > 1时,可以考虑第一趟用冒泡的方法,既能判断出初始序列是否有序,也能够在O(n)的时间内找到最小值,一举两得。

如果1 < k < c1(某个小常数c1),则继续使用简单选择排序也是理想的,这个c1的临界点恰好是锦标赛排序和堆排序这种复杂排序初始化的时间开销引起的,当越过了c1的临界点后,锦标赛排序和堆排序的优势就发挥了出来。在这种情况下,简单选择排序,需要比较的次数为n-1+n-2+…+n-c1次。从内存的层次结构的角度看,复杂选择排序(非线性)和简单选择排序(线性)相比,缓存的命中率更低,换入换出的代价较大,且堆排序的初始化过程虽然复杂度也为O(N),但在n很大的情况下,最坏情况下,系数接近4。

 

    如果c1 <= k < c2,此时锦标赛排序会是更理想的选择,和堆排序相比,锦标赛排序树是一个完全二叉树(有些教材认为是满二叉树,这是不够好的),需要n-1个辅助空间,但锦标赛排序的初始化比较次数很少,只有n-1次(没有最好最坏之分)和堆排序的4n(最坏情况下)相比,在选出最小的元素后,选择后续的元素,堆排序和锦标赛排序都需要调整,锦标赛从底向上调整,堆排序从上向下调整,但锦标赛排序每上升一格只需要比较1次,堆排序需要比较2次(据称采用加速堆的方法,可以把这个系数降到1,但需要付出lglgn的代价,同时付出代码的复杂性,本文不深入讨论这一点)。锦标赛排序总需要从叶子到根,而堆排序可能不需要,比如n个元素都相同的情况下,后者其他恰好符合堆性质而不需调整,或不需调整到叶子,总体情况看,锦标赛排序占据初始化的优势,在排序的早期应该能够胜出堆排序。当然锦标赛排序和堆排序都有优化提高的空间,就优化后的比较本文不作探讨。

 

    如果c2 <= k < n/2,堆排序将会是更理想的选择,堆排序是一种原地排序,辅助空间为O(1),因此空间局部性更好,特别是如果把堆看做一个数组,那么随着排序的进行,主要的计算都集中在数组的一段,而且局部性越来越好,因为数组的尾部已经是排好序的,没有访问的必要了。为了找出最小的k个数,堆排序将会使用小根堆,输出时从数组的尾部反序输出。

 

    如果 n/2 < k,此时可以考虑用快速排序和堆排序结合的方法。前几趟用快速排序,快速压缩问题空间。使得问题转化为在l长度序列的排序 + m个序列中找top-k’个元素的问题或者在L’长度序列中找top-k的问题。

    举个例子,例如{4,2,1,5,6,7,4*,6,8,3}中找前5个元素,则通过4的划分后得到{3,2,1} 4 {5,6,7,4*,6,8},由于已知4是第4大的元素,则只需要将前一段全部排序输出,再输出4,在输出后一段的第5 – 4 = 1个元素即可。如果是找前2个元素,问题就归结为在{3,2,1}中找最小的2个元素,则问题将大大化简。

 

    当k接近n的时候,毫无疑问使用快排序应该是最理想的了。

 

    最后,我们再讨论一下,当n足够大,以至于不能使用内排的情况。由于这时问题的复杂性主要取决于读盘,因此我们希望的是找出最小的k个数的代价是只读一遍磁盘,同时考虑排序码还有其他卫星数据的情况。

   在这种情况下,直接选择排序,只有在k = 1时,才是最理想的。

   当k > 1时,选择堆排序时很理想的,因为锦标赛排序的辅组空间不能接受。可以设置一个大小为k的最大堆,该元素是整个堆最大的,如果在扫描磁盘的过程中,有排序码比这个更大则pass,如果更小,则把这个值淘汰掉,插入这个更小的值后,恢复成一个最大堆。扫描完毕后留在堆中的k个值,即为所求。如果有其他卫星数据的情况下,堆的结点只需要增加相应的数据域或指针域即可。

 

    但问题是,假定实际的问题是需要在100亿网页URL中,找到PV最大的top100时(我们讨论的是最小,最大也可以用一样的方法)。我们使用了堆的方法,找到了结果,但是,领导突然需要看top 1000的URL时,还得做个大小为1000的堆再跑一遍,如果topk中的某些条件发生变化,比如URL长度在512以上的排除。。。可见,用堆的方法没有保存有价值的中间结果,这是很不理想的。

 

        什么才是理想的中间结果呢?我们考虑使用计数排序,例如这样一个整数序列{4,2,1,5,6,7,4*,6,8,3},我们可以申请从1到8的8个计数器,组成一个数组,pennyliang_counter[]={1,1,1,2,1,2,1,1},其中pennyliang_counter[0] = 1,表示1出现了1次,penny_counter[3] = 2,表示4出现了2次。输出时,按照计数器数组,输出即为有序序列,计数排序是线性的,而且计数器数组是理想的排序中间结果。

 

    对于top-k PV的问题,申请一个阈值以上的计数器,例如1024以上的PV数值才能有可能申请计数器,对10k以上的PV数值,才申请存储URL的空间,(后续的处理也有很多优化,本文不再展开)。扫描一趟磁盘,就可以生成这样的计数器数组,对于任意的处理要求,可以通过这个计数器数组直接得到,最坏情况也可以通过这个计数器数组加上再一次的磁盘扫描线性地得到。

        昨夜,我一夜难眠,就在想怎么把这个问题讲清楚,临到写的时候,还是感到很多内容无法展开,同时感到自己对这个问题的理解还不能定量的分析,因此甚是遗憾


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
04-26
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值