29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
為什么評價算法性能是根據時間和空間復雜度,而不是別的參數?是因為計算機結構是馮諾依曼體系,除了輸入輸出設備和控制器,就剩下運算器和存儲器了
問題引入
搜索引擎的熱門搜索排行榜功能是如何實現的?搜索引擎每天會接收大量的用戶搜索請求,把這些用戶輸入的搜索關鍵詞記錄下來,然后再離線地統計分析,得到最熱門的 Top 10 搜索關鍵詞。假設現在我們有一個包含 10 億個搜索關鍵詞的日志文件,如何能快速獲取到熱門榜 Top 10 的搜索關鍵詞呢?
堆這種數據結構幾個非常重要的應用:優先級隊列、求 Top K 和求中位數。
堆的應用一:優先級隊列
| 堆 | 優先級隊列 |
| 堆的核心功能就是取出堆頂數據!(大頂堆--最大值;小頂堆--最小值)。 使用方法: 1.插入數據,與堆頂比較決定插入與否(比如小頂堆堆頂拿到最小值,若新插入數據比它大,就刪掉堆頂最小值,插入新數據,從而堆中保留了TopK); 2.取數據,取最大值/最小值(如優先隊列的優先級數值就是大頂堆堆頂即最大值,入隊后自下而上堆化得到最新的堆頂,出隊時直接出堆頂即可。) | 1、優先級隊列中,數據的出隊順序不是先進先出,而是按照優先級來,優先級最高的,最先出隊 2、用堆來實現是最直接、最高效的 3、往優先級隊列中插入一個元素,就相當于往堆中插入一個元素;從優先級隊列中取出優先級最高的元素,就相當于取出堆頂元素。 |
合并有序小文件
假設我們有 100 個小文件,每個文件的大小是 100MB,每個文件中存儲的都是有序的字符串。我們希望將這些 100 個小文件合并成一個有序的大文件。這里就會用到優先級隊列。
| 方案一 | 1、使用一個數組,從這100個文件中各自取出第一個字符串放入數組比較大小,然后將最小的放入合并后的文件中,并從數組中刪除。 2、假設最小的字符串來自A文件,那么再從A文件中取出第二個字符串放入比較數組中重新比較大小 3、重復執行步驟1,直到所有文件中數據都放入合并后的大文件 缺點:每次從數組中取最小字符串,都需要循環遍歷整個數組 |
| 方案二 | 使用優先級隊列(堆) 1、從100個文件中取出第一個元素放到小頂堆中,堆頂元素也即優先級隊列隊首就是最小字符串。將該字符串放入到合并后最終大文件中,并從堆頂部刪除 2、再從小文件中取出下一個元素放入堆頂,循環該過程 3、刪除堆頂元素和堆中插入元素的復雜度都是O(logn) |
?
高性能的定時器
假設有一個定時器,定時器中維護了很多定時任務,每個任務都設定了一個要觸發執行的時間點。定時器每過一個很小的單位時間(比如 1 秒),就掃描一遍任務,看是否有任務到達設定的執行時間。如果到達了,就拿出來執行
這種方式的缺點:每過 1 秒就掃描一遍任務列表的做法比較低效,第一,任務的約定執行時間離當前時間可能還有很久,這樣前面很多次掃描其實都是徒勞的;第二,每次都要掃描整個任務列表,如果任務列表很大的話,勢必會比較耗時。
優化方案:
- 按照任務設定的執行時間,將這些任務存儲在優先級隊列中,隊列首部(也就是小頂堆的堆頂)存儲的是最先執行的任務。
- 拿隊首任務的執行時間點,與當前時間點相減,得到一個時間間隔 T
- 定時器就可以設定在 T 秒之后,再來執行任務。從當前時間點到(T-1)秒這段時間里,定時器都不需要做任何事情
- ?T 秒時間過去之后,定時器取優先級隊列中隊首的任務執行。然后再計算新的隊首任務的執行時間點與當前時間點的差值,把這個值作為定時器執行下一個任務需要等待的時間
利用堆求Top K
一類:靜態數據集合
- 維護一個大小為 K 的小頂堆,順序遍歷數組,從數組中取出數據與堆頂元素比較
- 如果比堆頂元素大,把堆頂元素刪除,并且將這個元素插入到堆中;如果比堆頂元素小,則不做處理,繼續遍歷數組
- 最后堆中的數據就是前K大數據
遍歷數組需要 O(n) 的時間復雜度,一次堆化操作需要 O(logK) 的時間復雜度,所以最壞情況下,n 個元素都入堆一次,時間復雜度就是 O(nlogK)
二類:動態數據集合
一個數據集合中有兩個操作,一個是添加數據,另一個詢問當前的前 K 大數據。如果每次詢問前 K 大數據,我們都基于當前的數據重新計算的話,那時間復雜度就是 O(nlogK),n 表示當前的數據的大小;
方案:
- 實際上,可以一直維護一個 K 大小的小頂堆,當有數據被添加到集合中時,與堆頂的元素對比。
- 如果比堆頂元素大,把堆頂元素刪除,并且將這個元素插入到堆中;如果比堆頂元素小,則不做處理。
- 無論任何時候需要查詢當前的前 K 大數據,都可以立刻返回給他
利用堆求中位數
如何求動態數據集合中的中位數:中位數就是處在中間位置的那個數。如果數據的個數是奇數,把數據從小到大排列,那第 2n?+1 個數據就是中位數(注意:假設數據是從 0 開始編號的);如果數據的個數是偶數的話,那處于中間位置的數據有兩個,第 2n? 個和第 2n?+1 個數據,這個時候可以隨意取一個作為中位數,比如取兩個數中靠前的那個,就是第 2n? 個數據
靜態數據
對于靜態數據,中位數是固定的。可以先排序,第 2n? 個數據就是中位數。每次詢問中位數的時候,直接返回這個固定的值。所以,盡管排序的代價比較大,但是邊際成本會很小。
動態數據
動態數據集合,中位數在不停地變動。
需要維護兩個堆,一個大頂堆,一個小頂堆。大頂堆中存儲前半部分數據,小頂堆中存儲后半部分數據,且小頂堆中的數據都大于大頂堆中的數據。如果有 n 個數據,n 是偶數,我們從小到大排序,那前 2n? 個數據存儲在大頂堆中,后 2n? 個數據存儲在小頂堆中。這樣,大頂堆中的堆頂元素就是我們要找的中位數。如果 n 是奇數,情況是類似的,大頂堆就存儲 2n?+1 個數據,小頂堆中就存儲 2n? 個數據
當添加一個數據時候:如何調整兩個堆,使得大頂堆中的堆頂數據繼續是中位數呢?
- 如果新加入的數據小于等于大頂堆的堆頂元素,將這個新數據插入到大頂堆;否則,將這個新數據插入到小頂堆
- 可能情況:如果 n 是偶數,兩個堆中的數據個數都是 2n?;如果 n 是奇數,大頂堆有 2n?+1 個數據,小頂堆有 2n? 個數據
- 這時候需要從一個堆中不停地將堆頂元素移動到另一個堆,通過這樣的調整,來讓兩個堆中的數據滿足上面的約定
- 最終求中位數只需要返回大頂堆的堆頂元素就可以了,所以時間復雜度就是 O(1)。
如何快速求接口的 99% 響應時間?
99% 響應時間。如果有 100 個接口訪問請求,每個接口請求的響應時間都不同,比如 55 毫秒、100 毫秒、23 毫秒等,我們把這 100 個接口的響應時間按照從小到大排列,排在第 99 的那個數據就是 99% 響應時間,也叫 99 百分位響應時間。
利用兩個堆實現。一個大頂堆,一個小頂堆。假設當前總數據的個數是 n,大頂堆中保存 n*99% 個數據,小頂堆中保存 n*1% 個數據。大頂堆堆頂的數據就是我們要找的 99% 響應時間。方法類似前面求中位數
解答開頭
一個包含 10 億個搜索關鍵詞的日志文件,如何快速獲取到 Top 10 最熱門的搜索關鍵詞呢?
多臺機器可以采用map-reduce的解決方法
單機環境:
| 方案一 | 1、首先統計每個關鍵詞出現頻率 2、采用散列表,順序掃描10億個關鍵詞,存在次數+1,不存在記錄次數為1;遍歷完成后散列表中就存儲了不重復關鍵詞以及出現次數 3、利用堆求Top K的方法,建立一個小頂堆大小為10,遍歷散列表,然后從散列表匯中依次取出每個搜索關鍵詞以及對應的出現次數,與堆頂中的搜索關鍵詞進行對比。大就刪除堆頂關鍵詞替換為新的關鍵詞,小于則不作處理 缺點:如果每個搜索關鍵詞的平均長度是 50 個字節,那存儲 1 億個關鍵詞起碼需要 5GB 的內存空間,散列表因為要避免頻繁沖突,不會選擇太大的裝載因子,所以消耗的內存空間就更多了。如果機器只有1G內存空間,那么無法一次性將所有關鍵詞加入內存, |
| 方案二 | 1、創建 10 個空文件 00,01,02,……,09 2、遍歷這 10 億個關鍵詞,通過哈希算法對其求哈希值,然后哈希值同 10 取模,得到的就是搜索關鍵詞應該被分到的文件編號 3、10 億個關鍵詞分片之后,每個文件都只有 1 億的關鍵詞,去除掉重復的,可能就只有 1000 萬個,每個關鍵詞平均 50 個字節,所以總的大小就是 500MB。1GB 的內存完全可以放得下 4、每個包含 1 億條搜索關鍵詞的文件,利用散列表和堆,分別求出 Top 10,然后把這個 10 個 Top 10 放在一塊,然后取這 100 個關鍵詞中,出現次數最多的 10 個關鍵詞,這就是這 10 億數據中的 Top 10 最頻繁的搜索關鍵詞 |
總結
優先級隊列是一種特殊的隊列,優先級高的數據先出隊,而不再像普通的隊列那樣,先進先出。堆就可以看作優先級隊列,只是稱謂不一樣罷了。
求 Top K 問題又可以分為針對靜態數據和針對動態數據,只需要利用一個堆,就可以做到非常高效率地查詢 Top K 的數據。
求中位數實際上還有很多變形,比如求 99 百分位數據、90 百分位數據等,處理的思路都是一樣的,即利用兩個堆,一個大頂堆,一個小頂堆,隨著數據的動態添加,動態調整兩個堆中的數據,最后大頂堆的堆頂元素就是要求的數據。
思考
有一個訪問量非常大的新聞網站,我們希望將點擊量排名 Top 10 的新聞摘要,滾動顯示在網站首頁 banner 上,并且每隔 1 小時更新一次。如果你是負責開發這個功能的工程師,你會如何來實現呢?
| 方案一 | 1、實時建立散列表,key是新聞的摘要,value是點擊量; 2、建立一個10的小頂堆,每隔一個小時掃描一次散列表,根據點擊量大小放入到小頂堆中,掃描完散列表后即出現Top10 的新聞點擊量。 |
| 方案二 | 1,對每篇新聞摘要計算一個hashcode,并建立摘要與hashcode的關聯關系,使用map存儲,以hashCode為key,新聞摘要為值 2,按每小時一個文件的方式記錄下被點擊的摘要的hashCode 3,當一個小時結果后,上一個小時的文件被關閉,開始計算上一個小時的點擊top10 4,將hashcode分片到多個文件中,通過對hashCode取模運算,即可將相同的hashCode分片到相同的文件中 5,針對每個文件取top10的hashCode,使用Map<hashCode,int>的方式,統計出所有的摘要點擊次數,然后再使用小頂堆(大小為10)計算top10, 6,再針對所有分片計算一個總的top10,最后合并的邏輯也是使用小頂堆,計算top10 7,如果僅展示前一個小時的top10,計算結束 8,如果需要展示全天,需要與上一次的計算按hashCode進行合并,然后在這合并的數據中取top10 9,在展示時,將計算得到的top10的hashcode,轉化為新聞摘要顯示即可 |
| 方案三 | 1,維護兩個散列表,一個是一小時新增的點擊量的散列表,以新聞id為鍵,點擊次數為值。一個是全部點擊量的散列表。每隔一小時把新增的散列表的數據同步到全部點擊量的散列表。然后把這小時內有變化的全部點擊量的散列表的數據(即此小時有新增點擊量的新聞數據)和我們維護的10個元素小頂堆堆頂進行比較,比堆頂的點擊量大的,則使用該元素替換堆頂,再進行堆化。比堆頂點擊量小的則不做處理。然后比較完,根據堆頂的10個元素的id,從數據庫讀取相應的新聞摘要顯示在banner上。除此之外,還要把變化后的全部點擊量散列表同步到數據庫。因為保存的是新聞id,所以散列表長度不會很大,所占用的內存也不會很大。而每個小時新增的訪問量的新聞id數也不會很多,畢竟很多人只會閱讀熱門消息。所以新增的點擊量的新聞數據假設為k,則每小時同步小頂堆的時間負責度為o(klg 10); 2018-12-02 |
?
總結
以上是生活随笔為你收集整理的29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oauth样例项目【01】之 使用aut
- 下一篇: Excel之vlookup函数的模糊匹配