在C#中使用二叉树实时计算海量用户积分排名的实现
从何说起
前些天和朋友讨论一个问题,他们的应用有几十万会员然后对应有积分,现在想做积分排名的需求,问有没有什么好方案。这个问题也算常见,很多地方都能看到,常规做法一般是数据定时跑批把计算结果到中间表然后直接查表就行,或者只显示个TOP N的排行榜,名次高的计算真实名次,名次比较低的直接显示在xxx名开外这种。但是出于探索问题的角度,我还是想找一下有没有实时计算的办法,并且效率能够接受。
在博客园搜到一篇不错的文章,基本罗列了常用的方案,每种算法详细介绍了具体思路,其中基于二叉树的算法是个非常不错的方案,文章中只给了思路没有给出代码,于是我决定自己用C#实现出来。
这里只讨论具体算法实现,不考虑业务需求是否合理。
思路解析
关于算法核心思想前面的文章中写的很详细,我不再重复描述,这里只用一个具体示例演示这个过程。
假设积分范围是0-5,我们对它不断进行中位分区直到不能分为止,形成如下一棵二叉树:
其中每个树节点包含2个信息:节点范围 range[min,max)
和命中数量计数器 count
,可以看到叶子节点的range一定是相邻的2个数。
假如现在有一个积分3要插入到树中,该如何操作呢?当前节点从根节点开始,分别判断是否包含于左右子节点,如果包含的话当前节点改为这个子节点,同时计数器加1,然后再次进行相同判断,直到遍历到叶子节点为止,遍历顺序如下:
再依次插入1和4,二叉树的演变情况为:
数据放进去后怎么判断它是排名多少呢?还是从根节点开始,判断它是否包含于左子节点,如果包含的话说明它比右子节点中count个数小(在count名之外),然后再往下一级做同样的判断;如果包含于右子节点那就继续往下判断,直到碰到叶子节点为止。依次累加count最后加上叶子节点占的一位就得到了它在这棵树里的排名,以1为例演示判断步骤(排名为2+1=3):
好了,一切就绪,只欠代码。
撸码实现
树结构由节点构成,那首先设计一个节点类:
/// <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
在C#中使用二叉树实时计算海量用户积分排名的实现的更多相关文章
- storm中DAU实时计算方案
所就职的公司是一家互联网视频公司,存在大量的实时计算需求,计算uv,pv等一些经典的实时指标统计.由于要统计当天的实时 UV,当天的uv由于要存储当天的所有的key,面临本地内存不够用的问题,异常重启 ...
- Java使用极小的内存完成对超大数据的去重计数,用于实时计算中统计UV
Java使用极小的内存完成对超大数据的去重计数,用于实时计算中统计UV – lxw的大数据田地 http://lxw1234.com/archives/2015/09/516.htm Java使用极小 ...
- 【转】Spark Streaming 实时计算在甜橙金融监控系统中的应用及优化
系统架构介绍 整个实时监控系统的架构是先由 Flume 收集服务器产生的日志 Log 和前端埋点数据, 然后实时把这些信息发送到 Kafka 分布式发布订阅消息系统,接着由 Spark Streami ...
- Apache Flink 如何正确处理实时计算场景中的乱序数据
一.流式计算的未来 在谷歌发表了 GFS.BigTable.Google MapReduce 三篇论文后,大数据技术真正有了第一次飞跃,Hadoop 生态系统逐渐发展起来. Hadoop 在处理大批量 ...
- 《storm实战-构建大数据实时计算读书笔记》
自己的思考: 1.接收任务到任务的分发和协调 nimbus.supervisor.zookeeper 2.高容错性 各个组件都是无状态的,状态 ...
- Storm 实战:构建大数据实时计算
Storm 实战:构建大数据实时计算(阿里巴巴集团技术丛书,大数据丛书.大型互联网公司大数据实时处理干货分享!来自淘宝一线技术团队的丰富实践,快速掌握Storm技术精髓!) 阿里巴巴集团数据平台事业部 ...
- 克拉克拉(KilaKila):大规模实时计算平台架构实战
克拉克拉(KilaKila):大规模实时计算平台架构实战 一.产品背景:克拉克拉(KilaKila)是国内专注二次元.主打年轻用户的娱乐互动内容社区软件.KilaKila推出互动语音直播.短视频配音. ...
- 《大数据实时计算引擎 Flink 实战与性能优化》新专栏
基于 Flink 1.9 讲解的专栏,涉及入门.概念.原理.实战.性能调优.系统案例的讲解. 专栏介绍 扫码下面专栏二维码可以订阅该专栏 首发地址:http://www.54tianzhisheng. ...
- Storm大数据实时计算
大数据也是构建各类系统的时候一种全新的思维,以及架构理念,比如Storm,Hive,Spark,ZooKeeper,HBase,Elasticsearch,等等 storm,在做热数据这块,如果要做复 ...
随机推荐
- 解决移动端1px边框问题的几种方法
1.边框粗细原因 在移动端下设置border为1px,在某些设备上看比1px粗. 这些由于不同的手机有不同的像素密度.在window对象中有一个devicePixelRatio属性,他可以反应css中 ...
- @codeforces - 1205E@ Expected Value Again
目录 @description@ @solution@ @part - 1@ @part - 2@ @part - 3@ @solution@ @details@ @description@ 给定两个 ...
- Python进阶04函数的参数对应
我们已经接触过函数(function)的参数(arguments)传递.当时我们根据位置,传递对应的参数.我们将接触更多的 参数传递方式. 回忆一下位置传递: def f(a,b,c): return ...
- 2019南昌网络赛-I. Yukino With Subinterval 线段树套树状数组,CDQ分治
TMD...这题卡内存卡的真优秀... 所以以后还是别用主席树的写法...不然怎么死的都不知道... 树套树中,主席树方法开权值线段树...会造成空间的浪费...这道题内存卡的很紧... 由于树套树已 ...
- Android Studio(十二):打包多个发布渠道的apk文件
Android Studio相关博客: Android Studio(一):介绍.安装.配置 Android Studio(二):快捷键设置.插件安装 Android Studio(三):设置Andr ...
- 解决margin塌陷和margin合并
<!doctype html> <html> <head> <meta charset="UTF-8"> <title> ...
- H3C 10BASE-T线缆和接口
- PHP利用纯真IP数据库在本地实现IP地址信息查询
https://blog.csdn.net/myweishanli/article/details/45098693 准备工作: 建议本地IP地址数据库,请到http://www.cz88.net/这 ...
- 原生js用div实现简单的轮播图
文章地址 https://www.cnblogs.com/sandraryan/ 原生js实现轮播图. 打开页面图片自动轮播,点击prev next按钮切换到上/下一张图片,点击1-5切换到对应图片. ...
- codeforces 1214
D 比赛的时候居然看漏了条件... 若在(x, y)格子,那么只能移动到(x+1, y)或(x, y+1) 这样的话就好做了,直接dp,然后统计每一种路径长度经过的点数. #include<cs ...