算法專題 - TopN問題

TopN問題

利用堆求 Top K

靜態數據集合 | 動態數據集合

針對靜態數據,如何在一個包含 n 個數據的數組中,查找前 K 大數據呢?我們可以維護一個大小爲 K 的小頂堆,順序遍歷數組,從數組中取數據與堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理,繼續遍歷數組。這樣等數組中的數據都遍歷完之後,堆中的數據就是前 K 大數據了。遍歷數組需要 O(n) 的時間複雜度,一次堆化操作需要 O(logK) 的時間複雜度,所以最壞情況下,n 個元素都入堆一次,所以時間複雜度就是 O(nlogK)。

針對動態數據求得 Top K 就是實時 Top K。怎麼理解呢?我舉一個例子。一個數據集合中有兩個操作,一個是添加數據,另一個詢問當前的前 K 大數據。

如果每次詢問前 K 大數據,我們都基於當前的數據重新計算的話,那時間複雜度就是 O(nlogK),n 表示當前的數據的大小。實際上,我們可以一直都維護一個 K 大小的小頂堆,當有數據被添加到集合中時,我們就拿它與堆頂的元素對比。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理。這樣,無論任何時候需要查詢當前的前 K 大數據,我們都可以裏立刻返回給他。

如何快速獲取到Top 10最熱門的搜索關鍵詞

如果我們將處理的場景限定爲單機,可以使用的內存爲 1GB。那這個問題該如何解決呢?

  • 散列表:來記錄關鍵詞及其出現的次數
  • 用堆求 Top K 的方法:建立一個大小爲 10 的小頂堆,遍歷散列表,依次取出每個搜索關鍵詞及對應出現的次數,然後與堆頂的搜索關鍵詞對比。如果出現次數比堆頂搜索關鍵詞的次數多,那就刪除堆頂的關鍵詞,將這個出現次數更多的關鍵詞加入到堆中。

上面的解決思路其實存在漏洞:消耗的內存空間就更多。

解決:相同數據經過哈希算法得到的哈希值是一樣的。我們可以哈希算法的這個特點,將 10 億條搜索關鍵詞先通過哈希算法分片到 10 個文件中。具體可以這樣做:我們創建 10 個空文件 00,01,02,……,09。我們遍歷這 10 億個關鍵詞,並且通過某個哈希算法對其求哈希值,然後哈希值同 10 取模,得到的結果就是這個搜索關鍵詞應該被分到的文件編號。

對這 10 億個關鍵詞分片之後,每個文件都只有 1 億的關鍵詞,去除掉重複的,可能就只有 1000 萬個,每個關鍵詞平均 50 個字節,所以總的大小就是 500MB。1GB 的內存完全可以放得下。

我們針對每個包含 1 億條搜索關鍵詞的文件,利用散列表和堆,分別求出 Top 10,然後把這個 10 個 Top 10 放在一塊,然後取這 100 個關鍵詞中,出現次數最多的 10 個關鍵詞,這就是這 10 億數據中的 Top 10 最頻繁的搜索關鍵詞了。

有一個訪問量非常大的新聞網站,我們希望將點擊量排名 Top 10 的新聞摘要,滾動顯示在網站首頁 banner 上,並且每隔 1 小時更新一次

  • 對每篇新聞摘要計算一個hashcode,並建立摘要與hashcode的關聯關係,使用map存儲,以hashCode爲key,新聞摘要爲值
  • 按每小時一個文件的方式記錄下被點擊的摘要的hashCode
  • 當一個小時結束後,上一個小時的文件被關閉,開始計算上一個小時的點擊top10
  • 將hashcode分片到多個文件中,通過對hashCode取模運算,即可將相同的hashCode分片到相同的文件中
  • 針對每個文件取top10的hashCode,使用Map<hashCode,int>的方式,統計出所有的摘要點擊次數,然後再使用小頂堆(大小爲10)計算top10
  • 再針對所有分片計算一個總的top10,最後合併的邏輯也是使用小頂堆,計算top10
  • 如果僅展示前一個小時的top10,計算結束
  • 如果需要展示全天,需要與上一次的計算按hashCode進行合併,然後在這合併的數據中取top10
  • 在展示時,將計算得到的top10的hashcode,轉化爲新聞摘要顯示即可

Leetcode

  • 703 數據流中的第K大元素
import heapq

class KthLargest(object):

    def __init__(self, k, nums):
        """
        :type k: int
        :type nums: List[int]

	    https://love.ranshy.com/heapq-%E5%A0%86%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
        """
        self.k = k
        self.heap = nums
        heapq.heapify(self.heap)
        while len(self.heap) > k:
            heapq.heappop(self.heap)

    def add(self, val):
        """
        :type val: int
        :rtype: int
        """        
        if len(self.heap) < self.k:
            heapq.heappush(self.heap, val)
        else:
            if self.heap[0] < val:
                heapq.heapreplace(self.heap, val)
                
        return self.heap[0]
            


# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章