在C#中使用二叉樹實(shí)時(shí)計(jì)算海量用戶積分排名的實(shí)現(xiàn)詳解
從何說起
前些天和朋友討論一個(gè)問題,他們的應(yīng)用有幾十萬會(huì)員然后對(duì)應(yīng)有積分,現(xiàn)在想做積分排名的需求,問有沒有什么好方案。這個(gè)問題也算常見,很多地方都能看到,常規(guī)做法一般是數(shù)據(jù)定時(shí)跑批把計(jì)算結(jié)果到中間表然后直接查表就行,或者只顯示個(gè)TOP N的排行榜,名次高的計(jì)算真實(shí)名次,名次比較低的直接顯示在xxx名開外這種。但是出于探索問題的角度,我還是想找一下有沒有實(shí)時(shí)計(jì)算的辦法,并且效率能夠接受。
在博客園搜到一篇不錯(cuò)的文章,基本羅列了常用的方案,每種算法詳細(xì)介紹了具體思路,其中基于二叉樹的算法是個(gè)非常不錯(cuò)的方案,文章中只給了思路沒有給出代碼,于是我決定自己用C#實(shí)現(xiàn)出來。
這里只討論具體算法實(shí)現(xiàn),不考慮業(yè)務(wù)需求是否合理。
思路解析
關(guān)于算法核心思想前面的文章中寫的很詳細(xì),我不再重復(fù)描述,這里只用一個(gè)具體示例演示這個(gè)過程。
假設(shè)積分范圍是0-5,我們對(duì)它不斷進(jìn)行中位分區(qū)直到不能分為止,形成如下一棵二叉樹:
其中每個(gè)樹節(jié)點(diǎn)包含2個(gè)信息:節(jié)點(diǎn)范圍range[min,max)
和命中數(shù)量計(jì)數(shù)器count
,可以看到葉子節(jié)點(diǎn)的range一定是相鄰的2個(gè)數(shù)。
假如現(xiàn)在有一個(gè)積分3要插入到樹中,該如何操作呢?當(dāng)前節(jié)點(diǎn)從根節(jié)點(diǎn)開始,分別判斷是否包含于左右子節(jié)點(diǎn),如果包含的話當(dāng)前節(jié)點(diǎn)改為這個(gè)子節(jié)點(diǎn),同時(shí)計(jì)數(shù)器加1,然后再次進(jìn)行相同判斷,直到遍歷到葉子節(jié)點(diǎn)為止,遍歷順序如下:
再依次插入1和4,二叉樹的演變情況為:
數(shù)據(jù)放進(jìn)去后怎么判斷它是排名多少呢?還是從根節(jié)點(diǎn)開始,判斷它是否包含于左子節(jié)點(diǎn),如果包含的話說明它比右子節(jié)點(diǎn)中count個(gè)數(shù)?。ㄔ赾ount名之外),然后再往下一級(jí)做同樣的判斷;如果包含于右子節(jié)點(diǎn)那就繼續(xù)往下判斷,直到碰到葉子節(jié)點(diǎn)為止。依次累加count最后加上葉子節(jié)點(diǎn)占的一位就得到了它在這棵樹里的排名,以1為例演示判斷步驟(排名為2+1=3):
好了,一切就緒,只欠代碼。
擼碼實(shí)現(xiàn)
樹結(jié)構(gòu)由節(jié)點(diǎn)構(gòu)成,那首先設(shè)計(jì)一個(gè)節(jié)點(diǎn)類:
/// <summary> /// 樹節(jié)點(diǎn)對(duì)象 /// </summary> public class TreeNode { /// <summary> /// 節(jié)點(diǎn)的最小值 /// </summary> public int ValueFrom { get; set; } /// <summary> /// 節(jié)點(diǎn)的最大值 /// </summary> public int ValueTo { get; set; } /// <summary> /// 在節(jié)點(diǎn)范圍內(nèi)的數(shù)量 /// </summary> public int Count { get; set; } /// <summary> /// 節(jié)點(diǎn)高度(樹的層級(jí)) /// </summary> public int Height { get; set; } /// <summary> /// 父節(jié)點(diǎn) /// </summary> public TreeNode Parent { get; set; } /// <summary> /// 左子節(jié)點(diǎn) /// </summary> public TreeNode LeftChildNode { get; set; } /// <summary> /// 右子節(jié)點(diǎn) /// </summary> public TreeNode RightChildNode { get; set; } }
樹節(jié)點(diǎn)的屬性主要包含范圍值ValueFrom、ValueTo
、計(jì)數(shù)器Count
、左子節(jié)點(diǎn)LeftChildNode
和右子節(jié)點(diǎn)RightChildNode
,由此組成一個(gè)有層次的樹結(jié)構(gòu)。
然后就是定義我們的樹對(duì)象了,它的核心字段就是代表源頭的根節(jié)點(diǎn):
public class RankBinaryTree { /// <summary> /// 根節(jié)點(diǎn) /// </summary> private TreeNode _root; }
根據(jù)前面的算法思想,創(chuàng)建樹的時(shí)候要用積分范圍初始化所有節(jié)點(diǎn),這里約定了最小積分為0,通過構(gòu)造函數(shù)傳入最大值并創(chuàng)建樹結(jié)構(gòu):
/// <summary> /// 構(gòu)造函數(shù)初始化根節(jié)點(diǎn) /// </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> /// 遍歷創(chuàng)建子節(jié)點(diǎn) /// </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; }
有了樹以后下一步就是往里面插入數(shù)據(jù),根據(jù)前面介紹的邏輯:
/// <summary> /// 往樹中插入一個(gè)值 /// </summary> /// <param name="value"></param> public void Insert(int value) { InnerInsert(_root, value); _data.Add(value); } /// <summary> /// 子節(jié)點(diǎn)判斷范圍遍歷插入 /// </summary> /// <param name="node"></param> /// <param name="value"></param> private void InnerInsert(TreeNode node, int value) { if (node == null) return; //判斷是否在這個(gè)節(jié)點(diǎn)范圍內(nèi) if (value >= node.ValueFrom && value < node.ValueTo) { //更新節(jié)點(diǎn)總數(shù)信息 node.Count++; //更新左子節(jié)點(diǎn) InnerInsert(node.LeftChildNode, value); //更新右子節(jié)點(diǎn) 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> /// 遍歷子節(jié)點(diǎn)獲取累計(jì)排名 /// </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) { //當(dāng)這個(gè)值存在于左子節(jié)點(diǎn)中時(shí),要累加右子節(jié)點(diǎn)的總數(shù)(表示這個(gè)數(shù)在多少名之后) return node.RightChildNode.Count + InnerGet(node.LeftChildNode, value); } else { //如果在右子節(jié)點(diǎn)中就繼續(xù)遍歷 return InnerGet(node.RightChildNode, value); } }
到這里,核心功能已經(jīng)實(shí)現(xiàn)了??紤]到有積分更新的情況,我們可以加上節(jié)點(diǎn)更新和刪除的方法。刪除很容易,和插入逆向操作就行,更新就更容易了,把舊節(jié)點(diǎn)刪除再計(jì)算出新值插入即可,完整代碼已經(jīng)上傳到Github。
這棵樹究竟效率如何,下面我們跑個(gè)分看看。
測(cè)試走起來
在測(cè)試程序中,我模擬了積分范圍0-1000000的場(chǎng)景,這個(gè)范圍幾乎覆蓋了真實(shí)業(yè)務(wù)中90%的積分值,100萬積分以上的會(huì)員系統(tǒng)應(yīng)該比較少見了。
而會(huì)員的積分值分布也是不均勻的,一般來說擁有小額積分的用戶比例最大,積分值越高所占用戶比例越小。
在程序中我假設(shè)有100萬個(gè)會(huì)員,其中50W用戶積分都在100以內(nèi),30W用戶積分在100-10000,15W用戶積分在10000-50000,5W用戶積分在50000以上。
下面是各個(gè)操作的耗時(shí)時(shí)間:
可以看到,這個(gè)效率不是一般的快啊,其中獲取排名的查詢時(shí)間幾乎可以忽略不計(jì)。
這時(shí)候有人問了,這么多數(shù)據(jù)會(huì)不會(huì)非常吃內(nèi)存,下面用任務(wù)管理器分別查看不使用樹和使用樹的內(nèi)存情況:
運(yùn)行環(huán)境是.NetCore3.0 Console,測(cè)試主機(jī)配置情況:
100萬數(shù)據(jù)只有130M內(nèi)存占用,對(duì)現(xiàn)代計(jì)算機(jī)來說簡直是灑灑水~
業(yè)務(wù)環(huán)境中使用務(wù)必注意線程安全問題?。?!
寫在最后
以上的二叉樹算法處理排名問題確實(shí)比較巧妙,實(shí)現(xiàn)起來也不算特別復(fù)雜,如果上述代碼有缺陷或有其他更好的方案,歡迎探討,也算拋磚引玉了~
完整代碼及測(cè)試用例請(qǐng)戳這里https://github.com/hey-hoho/NetCoreDemo/tree/master/ConsoleApp/ScoreRank
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持我們。
上一篇:詳解C#中對(duì)于接口的實(shí)現(xiàn)方式(隱式接口和顯式接口)
欄 目:C#教程
下一篇:C#中HttpWebRequest、WebClient、HttpClient的使用詳解
本文標(biāo)題:在C#中使用二叉樹實(shí)時(shí)計(jì)算海量用戶積分排名的實(shí)現(xiàn)詳解
本文地址:http://mengdiqiu.com.cn/a1/C_jiaocheng/4565.html
您可能感興趣的文章
- 01-10C#通過反射獲取當(dāng)前工程中所有窗體并打開的方法
- 01-10C#實(shí)現(xiàn)Winform中打開網(wǎng)頁頁面的方法
- 01-10C#實(shí)現(xiàn)由四周向中心縮小的窗體退出特效
- 01-10Extjs4如何處理后臺(tái)json數(shù)據(jù)中日期和時(shí)間
- 01-10C#使用Dispose模式實(shí)現(xiàn)手動(dòng)對(duì)資源的釋放
- 01-10C#3.0使用EventLog類寫Windows事件日志的方法
- 01-10C#實(shí)現(xiàn)將窗體固定在顯示器的左上角且不能移動(dòng)的方法
- 01-10C#中DataGridView常用操作實(shí)例小結(jié)
- 01-10C#編程獲取資源文件中圖片的方法
- 01-10C#使用windows服務(wù)開啟應(yīng)用程序的方法


閱讀排行
本欄相關(guān)
- 01-10C#通過反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁無法打開的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過重寫Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 04-02jquery與jsp,用jquery
- 01-10delphi制作wav文件的方法
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文