在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

原文地址:https://www.cnblogs.com/hohoa/p/12145689.html

时间: 2024-09-30 08:55:41

在C#中使用二叉树实时计算海量用户积分排名的实现的相关文章

海量用户积分排名算法探讨

这篇可参考 http://www.cnblogs.com/weidagang2046/archive/2012/03/01/massive-user-ranking.html#!comments 问题 某海量用户网站,用户拥有积分,积分可能会在使用过程中随时更新.现在要为该网站设计一种算法,在每次用户登录时显示其当前积分排名.用户最大规模为2亿:积分为非负整数,且小于100万. PS: 据说这是迅雷的一道面试题,不过问题本身具有很强的真实性,所以本文打算按照真实场景来考虑,而不局限于面试题的理想

海量用户积分排名算法探讨【转载】

本文内容 问题 存储结构 算法1:简单SQL查询 算法2:均匀分区设计 算法3:树形分区设计 算法4:积分排名数组 该问具体出自哪里,不是很确定,而我是在某个微信公众号上看到的~文中的内容比较有启发性的~ 问题 某海量用户网站,用户拥有积分,积分可能会在使用过程中随时更新.现在要为该网站设计一种算法,在每次用户登录时显示其当前积分排名.用户最大规模为2亿:积分为非负整数,且小于100万. PS:据说这是迅雷的一道面试题,不过问题本身具有很强的真实性,所以本文打算按照真实场景来考虑,而不局限于面试

Spark Streaming实时计算海量用户UV

提出需求 实时统计业务系统(web,APP之类)的访问人数,即所谓UV,或者DAU指标. 这个需求怕是流计算最最最常见的需求了. 计算UV的关键点就在于去重,即同一个人访问两次是只计一个UV的.在离线计算中统计UV比较容易想到的方法就是用group或distinct机制来去重.但是在实时计算场景,还用group就不太科学了,一个是全量数据的group是比较费时的,第二个是全量数据的group是很费内存和CPU的.特别是当用户量巨大的时候,还要做到秒级更新就更难了. 总结起来,需求就是:海量用户场

ARMS: 原来实时计算可以这么简单!

摘要: 业务实时监控服务( ARMS)是一款阿里云应用性能管理(APM)类监控产品.借助本产品,您可以基于前端.应用.业务自定义等服务,迅速便捷地为企业构建秒级响应的业务监控能力. 业务实时监控服务( ARMS)是一款阿里云应用性能管理(APM)类监控产品.借助本产品,您可以基于前端.应用.业务自定义等服务,迅速便捷地为企业构建秒级响应的业务监控能力.其中自定义监控作为该产品的时序计算和存储的基础,整合和封装了数据收集,消息通道,实时计算,时序存储,以及在线报表等多种先进互联网技术组件.本文主要

实时计算平台中的弹性集群资源管理

本文系微博运维数据平台(DIP)在实时计算平台的研发过程中集群资源管理方面的一些经验总结和运用,主要关注以下几个问题: 异构资源如何整合? 实时计算应用之间的物理资源如何隔离? 集群资源利用率如何提高? 集群运维成本如何降低? 1. 背景 这是我们初期的一个实时计算架构,大致划分为三个部分: (1)日志收集: 使用Rsynlog.Flume.Scribe汇聚各个业务方发送过来的日志数据:如果条件允许,业务方也可以直接将数据写入Kafka. (2)日志传输: 使用Kafka作为日志收集组件与实时应

【转】Spark Streaming 实时计算在甜橙金融监控系统中的应用及优化

系统架构介绍 整个实时监控系统的架构是先由 Flume 收集服务器产生的日志 Log 和前端埋点数据, 然后实时把这些信息发送到 Kafka 分布式发布订阅消息系统,接着由 Spark Streaming 消费 Kafka 中的消息,同时消费记录由 Zookeeper 集群统一管理,这样即使 Kafka 宕机重启后也能找到上次的消费记录继而进行消费.在这里 Spark Streaming 首先从 MySQL 读取规则然后进行 ETL 清洗并计算多个聚合指标,最后将结果的一部分存储到 Hbase

权威详解 | 阿里新一代实时计算引擎 Blink,每秒支持数十亿次计算

王峰,淘宝花名"莫问",2006年毕业后即加入阿里巴巴集团,长期从事搜索和大数据基础技术研发工作,目前在计算平台事业部,负责实时计算北京研发团队. 在阿里巴巴的11年工作期间,持续专注大数据计算与存储技术领域,基于Hadoop开源生态打造的数据基础设施一直服务于搜索.推荐等阿里核心电商业务场景,最近一年带领团队对Apache Flink进行了大量架构改进.功能完善和性能提升,打造出了阿里新一代实时计算引擎: Blink.目前数千台规模的Blink生产集群已经开始在线支持搜索.推荐.广告

Storm大数据实时计算

大数据也是构建各类系统的时候一种全新的思维,以及架构理念,比如Storm,Hive,Spark,ZooKeeper,HBase,Elasticsearch,等等 storm,在做热数据这块,如果要做复杂的热数据的统计和分析,亿流量,高并发的场景下,最合适的技术就是storm,没有其他 举例说明: Storm:实时缓存热点数据统计->缓存预热->缓存热点数据自动降级 Hive:Hadoop生态栈里面,做数据仓库的一个系统,高并发访问下,海量请求日志的批量统计分析,日报周报月报,接口调用情况,业务

实时计算,流数据处理系统简介与简单分析

转自:http://www.csdn.net/article/2014-06-12/2820196-Storm 摘要:实时计算一般都是针对海量数据进行的,一般要求为秒级.实时计算主要分为两块:数据的实时入库.数据的实时计算.今天这篇文章详细介绍了实时计算,流数据处理系统简介与简单分析. 编者按:互联网领域的实时计算一般都是针对海量数据进行的,除了像非实时计算的需求(如计算结果准确)以外,实时计算最重要的一个需求是能够实时响应计算结果,一般要求为秒级.实时计算的今天,业界都没有一个准确的定义,什么