<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何解決TOP-K問題

          共 5900字,需瀏覽 12分鐘

           ·

          2021-09-06 07:58

          最近在開發(fā)一個功能:動態(tài)展示的訂單數(shù)量排名前10的城市,這是一個典型的Top-k問題,其中k=10,也就是說找到一個集合中的前10名。實際生活中Top-K的問題非常廣泛,比如:微博熱搜的前100名、抖音直播的小時榜前50名、百度熱搜的前10條、博客園點贊最多的blog前10名,等等如何解決這類問題呢?初步的想法是將這個數(shù)據(jù)集合排序,然后直接取前K個返回。這樣解法可以,但是會存在一個問題:排序了很多不需要去排序的數(shù)據(jù),時間復(fù)雜度過高.假設(shè)有數(shù)據(jù)100萬,對這個集合進行排序需要很長的時間,即便使用快速排序,時間復(fù)雜度也是O(nlogn),那么這個問題如何解決呢?解決方法就是以空間換時間,使用優(yōu)先級隊列

          一:認識PriorityQueue

          1.1:PriorityQueue位于java.util包下,繼承自Collection,因此它具有集合的屬性,并且繼承自Queue隊列,擁有add/offer/poll/peek等一系列操作元素的能力,它的默認大小是11,底層使用Object[] 來保存元素,數(shù)組的話肯定會有擴容,當(dāng)添加元素的時候大小超過數(shù)組的容量,就會擴容,擴容的大小為原數(shù)組的大小加上,如果元素的數(shù)量小于64,則每次加2,如果大于64,則每次增加一半的容量。

           1.2:PriorityQueue的構(gòu)造方法


          public PriorityQueue(Comparator<? super E> comparator) {    this(DEFAULT_INITIAL_CAPACITY, comparator);}



          public PriorityQueue(int initialCapacity,                         Comparator<? super E> comparator) {    // Note: This restriction of at least one is not actually needed,    // but continues for 1.5 compatibility    if (initialCapacity < 1)        throw new IllegalArgumentException();    this.queue = new Object[initialCapacity];    this.comparator = comparator;}


          比較常用的就是這兩個構(gòu)造方法,其中第一個構(gòu)造方法中需要構(gòu)造一個比較器,第二個構(gòu)造方法添加初始容量和比較器,比較器可以自定義任何元素的優(yōu)先級,按照需要增加元素的優(yōu)先級展示

           1.3:PriorityQueue的常用API

          1.3.1:offer方法和add方法用于添加元素,本質(zhì)上offer方法和add方法是相同的:


          public boolean add(E e) {    return offer(e);}


          offer方法主要步驟就是判空、擴容、添加元素,添加元素的話,siftup方法里會根據(jù)構(gòu)造方法,如果有比較器就進行比較,沒有比較器的話就給元素賦予比較能力,并且根據(jù)構(gòu)造的大小,也就是

          initialCapacity進行比較,如果比較器的compare方法不符合定義的規(guī)則,直接break;符合的話會給數(shù)組的元素進行賦值


          public boolean offer(E e) {    if (e == null)        throw new NullPointerException();    modCount++;    int i = size;    if (i >= queue.length)        grow(i + 1);    size = i + 1;    if (i == 0)        queue[0] = e;    else        siftUp(i, e);    return true;}


          1.3.2:poll方法和peek方法都是返回頭元素,不同之處在于poll方法會返回頭頂元素并且移除元素,peek方法不會移除頭頂元素:


          public E poll() {    if (size == 0)        return null;    int s = --size;    modCount++;    E result = (E) queue[0];    E x = (E) queue[s];    queue[s] = null;    if (s != 0)        siftDown(0, x);    return result;}public E peek() {    return (size == 0) ? null : (E) queue[0];}


           二:PriorityQueue解決問題

          2.1:數(shù)組的前K大值

          代碼:


          import java.util.PriorityQueue;
          public class TopK {
          //找出前k個最大數(shù),采用小頂堆實現(xiàn) public static int[] findKMax(int[] nums, int k) {
          PriorityQueue<Integer> pq = new PriorityQueue<>(k);//隊列默認自然順序排列,小頂堆,不必重寫compare
          for (int num : nums) { if (pq.size() < k) { pq.offer(num); //如果堆頂元素 < 新數(shù),則刪除堆頂,加入新數(shù)入堆,保持堆中存儲著大值 } else if (pq.peek() < num) { pq.poll(); pq.offer(num); } }
          int[] result = new int[k]; for (int i = 0; i < k && !pq.isEmpty(); i++) { result[i] = pq.poll(); } return result; }}


           測試:


          public static void main(String[] args) {    int[] arr = new int[]{1, 6, 2, 3, 5, 4, 8, 7, 9};    System.out.println(Arrays.toString(findKMax(arr, 3)));}


          //輸出:

          優(yōu)先級隊列是如何解決這個問題的呢?

          PriorityQueue默認是小頂堆,那么什么是小頂堆?什么是大頂堆?假設(shè)有3、7、8三個數(shù),需要存儲在優(yōu)先級隊列里,畫個圖大家理解下:


          可以看出小頂堆的頭頂元素存儲著整個數(shù)據(jù)集合中數(shù)字最小的元素,而大頂堆存儲著整個數(shù)據(jù)集合中數(shù)字最大的元素,也就是一個按照升序排列,一個按照降序排列:


          //小頂堆的構(gòu)建方法:PriorityQueue<Integer> queue = new PriorityQueue<>(k);//這種寫法等價于:PriorityQueue<Integer> queue = new PriorityQueue<>(k, new Comparator<Integer>() {    @Override    public int compare(Integer o1, Integer o2) {        return o1-o2;    }});//同時等價于(lamda表達式的寫法)PriorityQueue<Integer> queue = new PriorityQueue<>(k, (o1, o2) -> o1-o2);
          //大頂堆的構(gòu)建方法: PriorityQueue<Integer> queue = new PriorityQueue<>(k, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2-o1; }});


          拿測試用例這個例子來說:

          構(gòu)建的是指定容量的小頂堆,因此每次queue.peek()返回的是最小的數(shù)字,在遍歷數(shù)組的過程中,如果遇到比該數(shù)字大的元素就將最小的數(shù)字poll(移除掉),然后將較大的元素添加到堆中,在添加進去堆中的時候,堆同時會按照優(yōu)先級比較,將最小的元素再次放到堆頂,這樣的做法就是會一直保持堆中的元素是相對較大的,同時堆頂元素是堆中最小的。

          按照測試用例給出的例子,{1, 6, 2, 3, 5, 4, 8, 7, 9} 優(yōu)先級隊列將會是這樣轉(zhuǎn)變的:(注意:本質(zhì)上優(yōu)先級隊列的實現(xiàn)方式是數(shù)組,這里只是用二叉樹的方式表現(xiàn)出來)

           

          假如該題換個角度,求出現(xiàn)頻率最低的元素怎么做呢?

          相信你根據(jù)上面的講述應(yīng)該也明白了:直接構(gòu)建一個大頂堆,這樣元素最大的值在堆頂,每次去和數(shù)組的元素的值去做比較,只要堆頂元素比數(shù)組的值小,就將堆頂元素poll出來,然后將數(shù)組的值添加進去,這樣就可以一直保持集合數(shù)組中一直是最小的k個數(shù)字。

           2.2:前k個高頻元素

          當(dāng)k = 1 時問題很簡單,線性時間內(nèi)就可以解決,只需要用哈希表維護元素出現(xiàn)頻率,每一步更新最高頻元素即可。當(dāng) k > 1 就需要一個能夠根據(jù)出現(xiàn)頻率快速獲取元素的數(shù)據(jù)結(jié)構(gòu),這里就需要用到優(yōu)先隊列

          首先建立一個元素值對應(yīng)出現(xiàn)頻率的哈希表,使用 HashMap來統(tǒng)計,然后構(gòu)建優(yōu)先級隊列,這里依舊是構(gòu)建小頂堆,不過因為該題是計算元素出現(xiàn)的頻率,因此我們需要將每個元素的頻率值做對比,

          需要重寫優(yōu)先級隊列的comparator,需要手工填值:這個步驟需要 O(N)O(N) 時間其中 NN 是列表中元素個數(shù)。

          第二步建立堆,堆中添加一個元素的復(fù)雜度是 O(\log(k))O(log(k)),要進行 NN 次復(fù)雜度是 O(N)O(N)。

          最后一步是輸出結(jié)果,復(fù)雜度為 O(k\log(k))O(klog(k)):


          public int[] topK(int[] nums, int k) {  //統(tǒng)計字符出現(xiàn)的頻率的map  Map<Integer, Integer> count = new HashMap();  for (int num : nums) {      count.put(num, count.getOrDefault(num, 0) + 1);  }
          //根據(jù)出現(xiàn)頻率的map來構(gòu)建k個元素的優(yōu)先級隊列 PriorityQueue<Integer> heap = new PriorityQueue<>(k, (o1, o2) -> count.get(o1) - count.get(o2));
          for (int n : count.keySet()) { heap.add(n); if (heap.size() > k) heap.poll(); }
          int[] result = new int[heap.size()]; for (int i = 0; i < result.length; i++) { result[i] = heap.poll(); } return result;
          }


           測試:


          public static void main(String[] args) {    int[] nums = {1, 1, 1, 3, 5, 6, 5, 6, 5, 4, 7, 8};    int[] res = new TOPK().topK(nums, 3);    System.out.println(Arrays.toString(res));}


          //輸出

          對于這樣的問題需要先對原數(shù)組進行處理,比如在計算前3頻率這個問題上,我們需要先計算數(shù)組中數(shù)字出現(xiàn)的頻率,然后維護一個哈希表用來存儲元素的頻率。對于類似問題:微博熱搜的前10名,那么肯定需要統(tǒng)計搜索頻次,抖音小時榜前10名,那么肯定要統(tǒng)計要計算時段的觀看人數(shù),優(yōu)先隊列只不過是一個存儲K元素的一個容器,它不負責(zé)統(tǒng)計,只負責(zé)維護一個K元素的最大或者最小堆頂,對于數(shù)據(jù)采用什么樣的優(yōu)先級順序需要自定義。

          如果用一句話來總結(jié)top-k問題:小頂堆用來求最大值,堆頂保存著最小值,判斷如果堆頂?shù)脑匦∮诖闅v數(shù)組的元素,把當(dāng)前元素poll出去,然后把待遍歷數(shù)組元素添加進去;大頂堆用來求最小值,堆頂保存著最大元素,如果堆頂元素大于待遍歷數(shù)組的值,就把當(dāng)前元素poll出去,把待遍歷數(shù)組的元素添加進去,這便是優(yōu)先級隊列的精髓。

          三:總結(jié)

          在實際中遇見的TOP-K問題有哪些,以及優(yōu)先級隊列PriorityQueue的基本原理介紹,接著由易到難的講解了如何通過優(yōu)先級隊列PriorityQueue來解決TOP-k問題,這兩個問題都比較經(jīng)典。對于理解優(yōu)先級隊列的含義、以及為什么它能解決該問題,想明白這點很重要。希望大家能夠做到舉一反三,下次面對同等問題的時候,能順序解決。起碼棘手的topk問題對于我們來說,有個PriorityQueue這個神兵利器,就顯得很簡單。

          瀏覽 26
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产色视频 | 黄色一级片日韩学生妹无套无码内射视频 | 蜜臀av在线免费观看 | 在线观看高清无码视频 | 亚洲精品酒店在线观看视频成人电影 |