關於實時TopN排名算法的思考
0.引言
實時排名是網絡應用中常見的功能。根據需求不同,大概可以分爲以下幾類:
- i. TopN排名
- ii. 全數據排名
作爲通用需求,我們必須做如下假設:
- a. 用戶基數較大
- b. 排名數據更新較頻繁
- c. 用於排序的數據(score)範圍不確定
- d. 用作排名的score只會增加,不會減少
基於以上假設,全數據排名就是海量數據處理問題,一般認爲內存難以勝任,一般通過數據庫(如redis)實現,本文暫不討論。
1.TopN實時排名算法
這裏,我們假設:
- N的範圍有限(如1000),榜單數據內存完全可以處理
那麼,問題來了。如何設計數據結構和算法,來滿足大批量用戶頻繁更新榜單情況下的實時排名需求?
1.1 一個失敗的方案
曾經看到一個實現,方案如下:
- a. 定義一個長度爲N+1的數組
- b. 更新數據時,將新數據放到數組尾部
- c. 對數組排序,如果數組長度爲N+1,則移除尾部元素
咋一看,由於榜單數據較小,每次更新排序好像可行,但是隨着用戶基數和更新頻度增加,
這個算法無疑跟DB設計把query建立在沒有index的table上一樣可怕。
隨着更新頻度提升,這個sort操作無疑將成爲CPU的噩夢。
低效率的算法不會出bug,但是隱藏在角落偷偷的吃CPU,還很難被發現。
因此,該方案不可取。
1.2 現成的數據結構?
經典的有序數據結構,如
- 紅黑樹
- Heap
貌似可以滿足這種需求。
但是,我們知道,平衡二叉樹的高效操作是在於快速查找,然而維護一棵樹的平衡(插入,刪除元素),卻是成本很高的操作。
因此,不可取。
1.3 合理的方案
這個功能裏有一個令人頭疼的要求,就是客戶端要實時能夠查看到排名情況,那麼問題來了:
- 這裏的實時是否意味着服務端數據必須實時有序?
答案是未必。我們知道,排序算法是計算機裏面一個比較耗時的操作,頻繁的排序更是無法忍受。
比較可行的方案是服務端不排序,只保證榜單記錄TopN的數據,把實時排序的任務交給客戶端執行。
- 服務器如何保證榜單上只記錄TopN的數據?
數據結構定義如下:
type RankInfo struct{
name string // name of this rank element
score int // rank score
}
type RankList struct {
list []*RankInfo // unordered top N rank list
min *RankInfo // the last one who own the minimum score in list
nameMap map[string]*RankInfo // name->rankInfo
}
更新積分榜UdateRankList(name, score)的操作流程如下(假設score只會增加不會減少):
- a. 從nameMap查找指定的name是否已在榜單上,如果存在,更新積分。如果該對象==min,則重新查找積分最低對象賦值給min,返回。
- b. 如果list長度<N, 則直接將新數據插入list尾部,如果score<min.score,則將新對象賦值給min, 返回。
- c. 如果score>min.score, 則頂替min對象數據,更新min.name,min.score爲name,score,重新查找積分最低對象賦值給min, 返回。
- d. 所給score不能上榜。
至此,排行榜更新完成。不需要排序,不需要支持排序的數據結構,高效的完成榜單維護,perfect。