海量数据的TopK问题几乎是后台开发面试必备题,本文从排序算法从0优化的角度分析TopK问题的优化。
什么是TopK问题:给定一个很大的数据量n,要求从n中提取出最大/最小/重复频度最高的K个数(K相对于n较小,如n为10亿量级,而K为100)。
解决这个问题,很容易想到要使用排序算法,首先,使用方法1笨办法 – 全部排序,解出来再说。
-
将n个数全部排序
使用普通排序,将n个数全部排序之后,取出最大的k个,即为所得。
时间复杂度:O(n*lg(n))
分析 & 优化思路:明明只需要TopK,却将全局都排序了,这也是这个方法复杂度非常高的原因。那能不能不全局排序,而只局部排序呢?这就引出了第二个优化方法。
-
只将TopK个数排序
使用冒泡排序
时间复杂度:O(n*k)
分析:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。
-
把TopK和非TopK分为两类,均不排序
使用堆排序,只找到TopK,不排序TopK
时间复杂度:O(n*lg(k))
分析:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法
最小的K个用最大堆,最大的K个用最小堆。
JDK中
PriorityQueue
实现了数据结构堆,通过指定comparator
字段来表示小顶堆或大顶堆,默认为null,表示自然序(natural ordering)。public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> minQueue = new PriorityQueue<>(k); for (int num : nums) { if (minQueue.size() < k || num > minQueue.peek()) minQueue.offer(num); if (minQueue.size() > k) minQueue.poll(); } return minQueue.peek(); }
-
快排 - 随机选择算法 Quick - Select
对只分两类不排序的进一步优化 – 堆排序需要遍历,而partition只需要遍历其中一个分支。
TopK是希望求出arr[1,n]中最大的k个数,那如果找到了第k大的数,做一次partition,不就一次性找到最大的k个数了么?
画外音:即partition后左半区的k个数。
问题变成了arr[1, n]中找到第k大的数。
再回过头来看看第一次partition,划分之后:
i = partition(arr, 1, n);
- 如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可;
- 如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可;
这就是随机选择算法randomized_select,RS,其伪代码如下:
int RS(arr, low, high, k){ if(low== high) return arr[low]; i= partition(arr, low, high); temp= i-low; //数组前半部分元素个数 if(temp>=k) return RS(arr, low, i-1, k); //求前半部分第k大 else return RS(arr, i+1, high, k-i); //求后半部分第k-i大 }
Quick - Select的Java实现
public int findKthLargest(int[] nums, int k) { return quickSelect(nums, k, 0, nums.length - 1); } // quick select to find the kth-largest element public int quickSelect(int[] arr, int k, int left, int right) { if (left == right) return arr[right]; int index = partition(arr, left, right); if (index - left + 1 > k) return quickSelect(arr, k, left, index - 1); else if (index - left + 1 == k) return arr[index]; else return quickSelect(arr, k - index + left - 1, index + 1, right); }