1 产生背景
topk是一个典型的业务场景,除了最优商品,包括推荐排名、积分排名所有涉及到排名前k的地方都是该算法的应用场合。
topk即得到一个集合后,筛选里面排名前k个数值。问题看似简单,但是里面的数据结构和算法体现着对解决方案性能的思索和深度挖掘。到底有几种方法,这些方案里蕴含的优化思路究竟是怎么样的?这节来讨论
2 解决方案
2.1 方案一:全局排序
全局排序,将集合整体排序后,取出最大的k个值就是需要的结果。
这种方案最糟糕,我只需要排名前k的元素,其他n-k个的顺序我并不关心,但是运算过程中,都得跟着做了没用的排序操作。
2.2 方案二:局部排序
局部排序,既然全局没必要,那我只取前k个,后面的就没必要理会了。
冒泡排序在排序算法中可以胜任该操作。我们按最大值往上冒泡为例,只要执行k次冒泡,那前k名就可以确定。但是这种方案依然不是最优办法。因为我们需要的是前k名,那至于这k个,谁大谁小并不需要关心,排序依然是个浪费。
2.3 方案三:最小堆
最小堆,既然没必要排序,那我们就不排序。
先将前k个元素形成一个最小堆,后面的n-k个元素依次与堆顶比较,小则丢弃大则替换堆顶并调整堆。直到n个全部完成为止。最小堆是topk的经典解决方案。
3 实现
下面就用最小堆实现topk
package com.oldlu.busi; import java.util.Arrays; public class Topk { //堆元素下沉,形成最小堆,序号从i开始 static void down(int[] nodes,int i) { //顶点序号遍历,只要到1半即可,时间复杂度为O(log2n) while ( i << 1 < nodes.length){ //左子,为何左移1位?回顾一下二叉树序号 int left = i<<1; //右子,左+1即可 int right = left+1; //标记,指向 本节点,左、右子节点里最小的,一开始取i自己 int flag = i; //判断左子是否小于本节点 if (nodes[left] < nodes[i]){ flag = left; } //判断右子 if (right < nodes.length && nodes[flag] > nodes[right]){ flag = right; } //两者中最小的与本节点不相等,则交换 if (flag != i){ int temp = nodes[i]; nodes[i] = nodes[flag]; nodes[flag] = temp; i = flag; }else { //否则相等,堆排序完成,退出循环即可 break; } } } public static void main(String[] args) { //原始数据 int[] src={3,6,2,7,4,8,1,9,2,5}; //要取几个 int k = 5; //堆,为啥是k+1?请注意,最小堆的0是无用的,序号从1开始 int[] nodes = new int[k+1]; //取前k个数,注意这里只是个二叉树,还不满足最小堆的要求 for (int i = 0; i < k; i++) { nodes[i+1]=src[i]; } System.out.println("before:"+Arrays.toString(nodes)); //从最底的子树开始,堆顶下沉 //这里才真正的形成最小堆 for (int i = k>>1; i >= 1; i‐‐) { down(nodes,i); } System.out.println("create:"+Arrays.toString(nodes)); //把余下的n‐k个数,放到堆顶,依次下沉,topk堆算法的开始 for (int i = src.length ‐ k;i<src.length;i++){ if (nodes[1] < src[i]){ nodes[1] = src[i]; down(nodes,1); } } System.out.println("topk:"+Arrays.toString(nodes)); } }
4 结果分析
最终获取k个值成功,符合要求
中间不涉及排序问题