侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

在C#中使用二叉树实时计算海量用户积分排名的实现详解

技术  /  管理员 发布于 5年前   206

从何说起

前些天和朋友讨论一个问题,他们的应用有几十万会员然后对应有积分,现在想做积分排名的需求,问有没有什么好方案。这个问题也算常见,很多地方都能看到,常规做法一般是数据定时跑批把计算结果到中间表然后直接查表就行,或者只显示个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#执行系统命令的方法
    下一条:
    C# 中的IComparable和IComparer的使用及区别
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 2024.07.09日OpenAI将终止对中国等国家和地区API服务(0个评论)
    • 2024/6/9最新免费公益节点SSR/V2ray/Shadowrocket/Clash节点分享|科学上网|免费梯子(1个评论)
    • 国外服务器实现api.openai.com反代nginx配置(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-07
    • 2017-08
    • 2017-09
    • 2018-01
    • 2018-07
    • 2018-08
    • 2018-09
    • 2018-12
    • 2019-01
    • 2019-02
    • 2019-03
    • 2019-04
    • 2019-05
    • 2019-06
    • 2019-07
    • 2019-08
    • 2019-09
    • 2019-10
    • 2019-11
    • 2019-12
    • 2020-01
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-09
    • 2020-10
    • 2020-11
    • 2021-04
    • 2021-05
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-12
    • 2022-01
    • 2022-02
    • 2022-03
    • 2022-04
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-08
    • 2023-09
    • 2023-10
    • 2023-12
    • 2024-02
    • 2024-04
    • 2024-05
    • 2024-06
    • 2025-02
    • 2025-07
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客