某海量使用者**,使用者擁有積分,積分可能會在使用過程中隨時更新。現在要為該**設計一種演算法,在每次使用者登入時顯示其當前積分排名。使用者最大規模為2億;積分為非負整數,且小於100萬。
儲存結構
首先,我們用一張使用者積分表user_score來儲存使用者的積分資訊:
表結構:
示例資料:
下面的演算法會基於這個基本的表結構來進行。
演算法1:簡單sql查詢
首先,我們很容易想到用一條簡單的sql語句查詢出積分大於該使用者積分的使用者數量:
select 1 + count(t2.uid) as rank
from user_score t1, user_score t2
where t1.uid = @uid and t2.score > t1.score
優點:簡單,利用了sql的功能,不需要複雜的查詢邏輯,也不引入額外的儲存結構,對小規模或效能要求不高的應用不失為一種良好的解決方案。
缺點:需要對user_score表進行全表掃瞄,還需要考慮到查詢的同時若有積分更新會對錶造成鎖定,在海量資料規模和高併發的應用中,效能是無法接受的。
演算法2:均勻分割槽設計
在許多應用中快取是解決效能問題的重要途徑,我們自然會想能不能把使用者排名用memcached快取下來呢?不過再一想發現快取似乎幫不上什麼忙,因為使用者排名是乙個全域性性的統計性指標,而並非使用者的私有屬性,其他使用者的積分變化可能會馬上影響到本使用者的排名。然而,真實的應用中積分的變化其實也是有一定規律的,通常乙個使用者的積分不會突然暴增暴減,一般使用者總是要在低分割槽混跡很長一段時間才會慢慢公升入高分割槽,也就是說使用者積分的分布總體說來是有區段的,我們進一步注意到高分割槽使用者積分的細微變化其實對低分段使用者的排名影響不大。於是,我們可以想到按積分區段進行統計的方法,引入一張分割槽積分表score_range。
表結構:
![請輸入描述]
資料示例:
表示[from_score, to_score)區間有count個使用者。若我們按每1000分劃分乙個區間則有[0, 1000), [1000, 2000), …, [999000, 1000000)這1000個區間,以後對使用者積分的更新要相應地更新score_range表的區間值。在分割槽積分表的輔助下查詢積分為s的使用者的排名,可以首先確定其所屬區間,把高於s的積分區間的count值累加,然後再查詢出該使用者在本區間內的排名,二者相加即可獲得使用者的排名。
乍一看,這個方法貌似通過區間聚合減少了查詢計算量,實則不然。最大的問題在於如何查詢使用者在本區間內的排名呢?如果是在演算法1中的sql中加上積分條件:
select 1 + count(t2.uid) as rank
from user_score t1, user_score t2
where t1.uid = @uid and t2.score > t1.score and t2.score
在理想情況下,由於把t2.score的範圍限制在了1000以內,如果對score欄位建立索引,我們期望本條sql語句將通過索引大大減少掃瞄的user_score表的行數。不過真實情況並非如此,t2.score的範圍在1000以內並不意味著該區間內的使用者數也是1000,因為這裡有積分相同的情況存在!二八定律告訴我們,前20%的低分割槽往往集中了80%的使用者,這就是說對於大量低分割槽使用者進行區間內排名查詢的效能遠不及對少數的高分割槽使用者,所以在一般情況下這種分割槽方法不會帶來實質性的效能提公升。
演算法2總結
優點:注意到了積分區間的存在,並通過預先聚合消除查詢的全表掃瞄
缺點:積分非均勻分布的特點使得效能提公升並不理想
演算法3:樹形分割槽設計
均勻分割槽查詢演算法的失敗是由於積分分布的非均勻性,那麼我們自然就會想,能不能按二八定律,把score_range表設計為非均勻區間呢?比如,把低分區劃密集一點,10分乙個區間,然後逐漸變成100分,1000分,10000分 … 當然,這不失為一種方法,不過這種分法有一定的隨意性,不容易把握好,而且整個系統的積分分布會隨著使用而逐漸發生變化,最初的較好的分割槽方法可能會變得不適應未來的情況了。我們希望找到一種分割槽方法,既可以適應積分非均勻性,又可以適應系統積分分布的變化,這就是樹形分割槽。
我們可以把[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)。這實際上是把區間組織成了一種平衡二叉樹結構,根結點代表一級區間,每個非葉子結點有兩個子結點,左子結點代表低分區間,右子結點代表高分區間。樹形分割槽結構需要在更新時保持一種不變數(invariant):非葉子結點的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等其他儲存方式,而基於鍵的操作也很容易引入快取機制進一步優化效能。
演算法3總結
優點:結構穩定,不受積分分布影響;每次查詢或更新的複雜度為積分最大值的log(n)級別,且與使用者規模無關,可以應對海量規模;不依賴於sql,容易改造為nosql等其他儲存方式
缺點:演算法相對更複雜
海量使用者積分排名演算法
使用者 使用者擁有積分,積分可能會在使用過程中隨時更新,設計一種演算法,在使用者登入時顯示其當前基本排名,積分為非負整數,且小於100萬。海量使用者積分排名演算法 1.利用資料庫,建立表結構為使用者id和積分的表,用sql查詢得到排名。優點是簡單,利用sql功能,無需複雜查詢邏輯,不引入額外的儲存結...
球隊比賽積分排序問題
題目 輸入球隊數量,球隊之間將進行比賽,勝利者積分 3,打平則 1,失敗則 0。最終按球隊積分以及淨勝球進行排序。思路 定義結構體,成員有球隊名 球隊積分 球隊淨勝球。利用隨機數生成比賽得分,隨後根據比分計算雙方球隊積分 淨勝球情況。最後按照積分 淨勝球進行排序。include stdafx.h i...
海量積分排名 簡化後的具體實現
首先積分值有個最大值,積分由n變化到m 排名變化的只是在積分n到積分m的客戶排名會發生變化故此 有如下實現 使用rankandnum來表示處在該積分的排名和數值 使用rankandnums數值來儲存所有積分排名 注意 rankandnums n 中rankandnum中的rank和num則表示積分為...