前些天和朋友討論一個問題,他們的應用有幾十萬會員而後對應有積分,如今想作積分排名的需求,問有沒有什麼好方案。這個問題也算常見,不少地方都能看到,常規作法通常是數據定時跑批把計算結果到中間表而後直接查表就行,或者只顯示個TOP N的排行榜,名次高的計算真實名次,名次比較低的直接顯示在xxx名開外這種。可是出於探索問題的角度,我仍是想找一下有沒有實時計算的辦法,而且效率可以接受。
在博客園搜到一篇不錯的文章,基本羅列了經常使用的方案,每種算法詳細介紹了具體思路,其中基於二叉樹的算法是個很是不錯的方案,文章中只給了思路沒有給出代碼,因而我決定本身用C#實現出來。html
這裏只討論具體算法實現,不考慮業務需求是否合理。node
關於算法核心思想前面的文章中寫的很詳細,我再也不重複描述,這裏只用一個具體示例演示這個過程。
假設積分範圍是0-5,咱們對它不斷進行中位分區直到不能分爲止,造成以下一棵二叉樹:
git
其中每一個樹節點包含2個信息:節點範圍range[min,max)
和命中數量計數器count
,能夠看到葉子節點的range必定是相鄰的2個數。
假如如今有一個積分3要插入到樹中,該如何操做呢?當前節點從根節點開始,分別判斷是否包含於左右子節點,若是包含的話當前節點改成這個子節點,同時計數器加1,而後再次進行相同判斷,直到遍歷到葉子節點爲止,遍歷順序以下:
github
再依次插入1和4,二叉樹的演變狀況爲:
算法
數據放進去後怎麼判斷它是排名多少呢?仍是從根節點開始,判斷它是否包含於左子節點,若是包含的話說明它比右子節點中count個數小(在count名以外),而後再往下一級作一樣的判斷;若是包含於右子節點那就繼續往下判斷,直到碰到葉子節點爲止。依次累加count最後加上葉子節點佔的一位就獲得了它在這棵樹裏的排名,以1爲例演示判斷步驟(排名爲2+1=3):
c#
好了,一切就緒,只欠代碼。安全
樹結構由節點構成,那首先設計一個節點類:函數
/// <summary> /// 樹節點對象 /// </summary> public class TreeNode { /// <summary> /// 節點的最小值 /// </summary> public int ValueFrom { get; set; } /// <summary> /// 節點的最大值 /// </summary> public int ValueTo { get; set; } /// <summary> /// 在節點範圍內的數量 /// </summary> public int Count { get; set; } /// <summary> /// 節點高度(樹的層級) /// </summary> public int Height { get; set; } /// <summary> /// 父節點 /// </summary> public TreeNode Parent { get; set; } /// <summary> /// 左子節點 /// </summary> public TreeNode LeftChildNode { get; set; } /// <summary> /// 右子節點 /// </summary> public TreeNode RightChildNode { get; set; } }
樹節點的屬性主要包含範圍值ValueFrom、ValueTo
、計數器Count
、左子節點LeftChildNode
和右子節點RightChildNode
,由此組成一個有層次的樹結構。
而後就是定義咱們的樹對象了,它的核心字段就是表明源頭的根節點:測試
public class RankBinaryTree { /// <summary> /// 根節點 /// </summary> private TreeNode _root; }
根據前面的算法思想,建立樹的時候要用積分範圍初始化全部節點,這裏約定了最小積分爲0,經過構造函數傳入最大值並建立樹結構:線程
/// <summary> /// 構造函數初始化根節點 /// </summary> /// <param name="max"></param> public RankBinaryTree(int max) { _root = new TreeNode() { ValueFrom = 0, ValueTo = max+1, Height = 1 }; _root.LeftChildNode = CreateChildNode(_root, 0, max / 2); _root.RightChildNode = CreateChildNode(_root, max / 2, max); } /// <summary> /// 遍歷建立子節點 /// </summary> /// <param name="current"></param> /// <param name="min"></param> /// <param name="max"></param> /// <returns></returns> private TreeNode CreateChildNode(TreeNode current, int min, int max) { if (min == max) return null; var node = new TreeNode() { ValueFrom = min, ValueTo = max, Height = current.Height + 1 }; node.Parent = current; int center = (min + max) / 2; if (min < max - 1) { node.LeftChildNode = CreateChildNode(node, min, center); node.RightChildNode = CreateChildNode(node, center, max); } return node; }
有了樹之後下一步就是往裏面插入數據,根據前面介紹的邏輯:
/// <summary> /// 往樹中插入一個值 /// </summary> /// <param name="value"></param> public void Insert(int value) { InnerInsert(_root, value); _data.Add(value); } /// <summary> /// 子節點判斷範圍遍歷插入 /// </summary> /// <param name="node"></param> /// <param name="value"></param> private void InnerInsert(TreeNode node, int value) { if (node == null) return; //判斷是否在這個節點範圍內 if (value >= node.ValueFrom && value < node.ValueTo) { //更新節點總數信息 node.Count++; //更新左子節點 InnerInsert(node.LeftChildNode, value); //更新右子節點 InnerInsert(node.RightChildNode, value); } }
下一步提供方法獲取指定值在樹中的排名:
/// <summary> /// 從樹中獲取總排名 /// </summary> /// <param name="value"></param> /// <returns></returns> public int GetRank(int value) { if (value < 0) return 0; return InnerGet(_root, value); } /// <summary> /// 遍歷子節點獲取累計排名 /// </summary> /// <param name="node"></param> /// <param name="value"></param> /// <returns></returns> private int InnerGet(TreeNode node, int value) { if (node.LeftChildNode == null || node.RightChildNode == null) return 1; if (value >= node.LeftChildNode.ValueFrom && value < node.LeftChildNode.ValueTo) { //當這個值存在於左子節點中時,要累加右子節點的總數(表示這個數在多少名以後) return node.RightChildNode.Count + InnerGet(node.LeftChildNode, value); } else { //若是在右子節點中就繼續遍歷 return InnerGet(node.RightChildNode, value); } }
到這裏,核心功能已經實現了。考慮到有積分更新的狀況,咱們能夠加上節點更新和刪除的方法。刪除很容易,和插入逆向操做就行,更新就更容易了,把舊節點刪除再計算出新值插入便可,完整代碼已經上傳到Github。
這棵樹究竟效率如何,下面咱們跑個分看看。
在測試程序中,我模擬了積分範圍0-1000000的場景,這個範圍幾乎覆蓋了真實業務中90%的積分值,100萬積分以上的會員系統應該比較少見了。
而會員的積分值分佈也是不均勻的,通常來講擁有小額積分的用戶比例最大,積分值越高所佔用戶比例越小。
在程序中我假設有100萬個會員,其中50W用戶積分都在100之內,30W用戶積分在100-10000,15W用戶積分在10000-50000,5W用戶積分在50000以上。
下面是各個操做的耗時時間:
能夠看到,這個效率不是通常的快啊,其中獲取排名的查詢時間幾乎能夠忽略不計。
這時候有人問了,這麼多數據會不會很是吃內存,下面用任務管理器分別查看不使用樹和使用樹的內存狀況:
運行環境是.NetCore3.0 Console,測試主機配置狀況:
100萬數據只有130M內存佔用,對現代計算機來講簡直是灑灑水~
業務環境中使用務必注意線程安全問題!!!
以上的二叉樹算法處理排名問題確實比較巧妙,實現起來也不算特別複雜,若是上述代碼有缺陷或有其餘更好的方案,歡迎探討,也算拋磚引玉了~
完整代碼及測試用例請戳這裏https://github.com/hey-hoho/NetCoreDemo/tree/master/ConsoleApp/ScoreRank