以前在**做過一些實時投票,積分排名;單曲、**等排行榜;遊戲中也有類似的戰鬥力排行;sns的遊戲又有好友排行等,對於此類的排行演算法在此做個總結。
需求背景:
檢視前top n的排名使用者
檢視自己的排名
使用者積分變更後,排名及時更新
方案一:
利用mysql來實現,存放一張使用者積分表user_score,結構如下:
取前top n,自己的排名都可以通過簡單的sql語句搞定。
演算法簡單,利用sql的功能,不需要其他複雜邏輯,對於資料量比較少、效能要求不高,可以使用。但是對於海量資料,效能是無法接受的。
方案二:
積分排名陣列實現
如有1百萬使用者進行排名,就用乙個大小為1,000,000的陣列表示積分和排名的對應關係,其中rank[ s ]表示積分s所對應的排名。初始化時,rank陣列可以由user_score表在o(n)的複雜度內計算而來。使用者排名的查詢和更新基於這個陣列來進行。查詢積分s所對應的排名直接返回rank[ s ]即可,複雜度為o(1);當使用者積分從s變為s+n,只需要把rank[ s ]到rank[s+n-1]這n個元素的值增加1即可,複雜度為o(n)。
方案三:
用gcc的pb_ds庫中有assoc_container來進行實現。
訪問效率都可以達到o(log(n)),不足就是程式重啟後資料會丟失。
方案四:
自己實現排序樹
大致實現思路如下:
我們可以把[0, 1,000,000)作為一級區間;再把一級區間分為兩個2級區間[0, 500,000), [500,000, 1,000,000),然後把二級區間二分為4個3級區間[0, 250,000), [250,000, 500,000), [500,000, 750,000), [750,000, 1,000,000),依此類推,最終我們會得到1,000,000個21級區間[0,1), [1,2) … [999,999, 1,000,000)。這實際上是把區間組織成了一種平衡二叉樹結構,根結點代表一級區間,每個非葉子結點有兩個子結點,左子結點代表低分區間,右子結點代表高分區間。樹形分割槽結構需要在更新時保持一種不變數,非葉子結點的count值總是等於其左右子結點的count值之和。
以後,每次使用者積分有變化所需要更新的區間數量和積分變化量有關係,積分變化越小更新的區間層次越低。總體上,每次所需要更新的區間數量是使用者積分變數的log(n)級別的,也就是說如果使用者積分一次變化在百萬級,更新區間的數量在二十這個級別。在這種樹形分割槽積分表的輔助下查詢積分為s的使用者排名,實際上是乙個在區間樹上由上至下、由粗到細一步步明確s所在位置的過程。比如,對於積分499,000,我們用乙個初值為0的排名變數來做累加;首先,它屬於1級區間的左子樹[0, 500,000),那麼該使用者排名應該在右子樹[500,000,
1,000,000)的使用者數count之後,我們把該count值累加到該使用者排名變數,進入下一級區間;其次,它屬於3級區間的[250,000, 500,000),這是2級區間的右子樹,所以不用累加count到排名變數,直接進入下一級區間;再次,它屬於4級區間的…;直到最後我們把使用者積分精確定位在21級區間[499,000, 499,001),整個累加過程完成,得出排名!
雖然,本演算法的更新和查詢都涉及到若干個操作,但如果我們為區間的from_score和to_score建立索引,這些操作都是基於鍵的查詢和更新,不會產生表掃瞄,因此效率更高。另外,本演算法並不依賴於關係資料模型和sql運算,可以輕易地改造為nosql等其他儲存方式,而基於鍵的操作也很容易引入快取機制進一步優化效能。進一步,我們可以估算一下樹形區間的數目大約為2,000,000,考慮每個結點的大小,整個結構只占用幾十m空間。所以,我們完全可以在記憶體建立區間樹結構,並通過user_score表在o(n)的時間內初始化區間樹,然後排名的查詢和更新操作都可以在記憶體進行。一般來講,同樣的演算法,從資料庫到記憶體演算法的效能提公升常常可以達到10^5以上;因此,本演算法可以達到非常高的效能。
演算法特點
優點:結構穩定,不受積分分布影響;每次查詢或更新的複雜度為積分最大值的o(log(n))級別,且與使用者規模無關,可以應對海量規模;不依賴於sql,容易改造為nosql或記憶體資料結構。
缺點:演算法相對更複雜。
方案五:
skiplist的實現
於是就想到的跳表,發現用這個實現起來比較簡單;用hashmap來儲存具體的物件;用skiplist用來排序。也可以簡單的用乙個map和set來實現。map內面存具體物件,set用來排序。
關於skip list這裡簡單介紹下:skip list是鍊錶的一種特殊形式,對鍊錶的一種優化;保證insert和remove操作是o(logn),而通用鍊錶的複雜度為o(n);
優點:實現較簡單,效率基本上o(log(n))
缺點:當達到億級別時的資料時,效能會急劇下降
方案六:
基於redis的 sort set的實現
後來看redis發現redis的zset天生是用來做排行榜的、好友列表, 去重, 歷史記錄等業務需求。介面使用非常簡單。介面非常豐富,基本上需要的實現都能滿足,說明如下:
zadd/zrem是o(log(n)),zrangebyscore/zremrangebyscore是o(log(n)+m),n是set大小,m是結果/操作元素的個數。
zset的實現用到了兩個資料結構:hash table 和 skip list(跳躍表),其中hash table是具體使用redis中的dict來實現的,主要是為了保證查詢效率為o(1) ,而skip list(跳躍表)主要是保證元素有序並能夠保證insert和remove操作是o(logn)的複雜度。
**現在的通用投票排名系統就是基於redis來實現的,執行還不錯。
優點:基於redis開發,速度快;使用redis相關特性
缺點:當達到億級別時的資料時,效能會急劇下降
來實現排行榜的方法很多,可以根據自己的具體需求,參考選用。
遊戲服務端開發 排行榜
排行榜幾乎是每個網路遊戲都有的系統,以下用erlang以例,分享一種排行榜實現方式。每個排行榜對應乙個actor,state使用如下結構 通用排行榜結構 record rank list,已經排好序的列表 ready list one rank 待排序的列表 sort time 0 排行榜的重新整理...
遊戲服務端開發 一
資料儲存伺服器 遊戲中的資料大致分為靜態配置資料和動態的玩家資料。這裡主要討論玩家資料儲存的解決方案。雖然遊戲應用的寫操作要多於讀操作,但是加入快取層仍然有其必要性。多個應用伺服器啟動時從資料庫讀取資料會在瞬間給資料庫造成巨大壓力,如果將相對靜態的資料以檔案的形式放在應用伺服器本地,可以避免這個問題...
遊戲服務端開發 二
應用伺服器的設計 上 應用伺服器的工作有 0 同步廣播玩家的行為 1作為第三方對玩家個體和玩家之間互動行為計算,並將計算結果推送到資料儲存系統 2驅動遊戲中的 npc 3作為乙個特殊的遊戲參與者,與玩家相互作用。應用伺服器最重要的工作莫過於同步廣播玩家之間的行為,使玩家之間能夠互視,多人同時遊戲才有...