在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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 关于c#二叉树的实现

    本篇纯属娱乐,源于整理代码,发现还曾实现过遍历二叉树. 虽然.NET/C#中的各种集合类已经实现了最优的排序设计,但了解基本的算法实现有助于软件开发中的各种权衡和选择.比如,如果你实现过B+树排序和查找,并将树节点序列化至二进制文件块,则你应该已经了解了各种数据库索引的基本设计. 什么是二叉树?http://en.wikipedia.org/wiki/Binary_tree 二叉树节点类定义 复制代码 代码如下: View Code    /// <summary>   /// 二叉树节点  

  • C#非递归先序遍历二叉树实例

    本文实例讲述了C#非递归先序遍历二叉树的方法.分享给大家供大家参考.具体如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication5 { class Program { static void Main(string[] args) { Node treeRoo

  • C#使用前序遍历、中序遍历和后序遍历打印二叉树的方法

    本文实例讲述了C#使用前序遍历.中序遍历和后序遍历打印二叉树的方法.分享给大家供大家参考.具体实现方法如下: public class BinaryTreeNode { public BinaryTreeNode Left { get; set; } public BinaryTreeNode Right { get; set; } public int Data { get; set; } public BinaryTreeNode(int data) { this.Data = data;

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

    从何说起 前些天和朋友讨论一个问题,他们的应用有几十万会员然后对应有积分,现在想做积分排名的需求,问有没有什么好方案.这个问题也算常见,很多地方都能看到,常规做法一般是数据定时跑批把计算结果到中间表然后直接查表就行,或者只显示个TOP N的排行榜,名次高的计算真实名次,名次比较低的直接显示在xxx名开外这种.但是出于探索问题的角度,我还是想找一下有没有实时计算的办法,并且效率能够接受. 在博客园搜到一篇不错的文章,基本罗列了常用的方案,每种算法详细介绍了具体思路,其中基于二叉树的算法是个非常不错

  • Vue中使用方法、计算属性或观察者的方法实例详解

    熟悉 Vue 的都知道 方法methods.计算属性computed.观察者watcher 在 Vue 中有着非常重要的作用,有些时候我们实现一个功能的时候可以使用它们中任何一个都是可以的,但是它们之间又存在一些不同之处,每一个都有一些适合自己的场景,我们要想知道合适的场景,肯定先对它们有一个清楚的了解,先看一个小例子. <div id="app"> <input v-model="firstName" type="text"&

  • JavaScript中的普通函数和箭头函数的区别和用法详解

    最近被问到了一个问题: javaScript 中的箭头函数 ( => ) 和普通函数 ( function ) 有什么区别? 我当时想的就是:这个问题很简单啊~(flag),然后做出了错误的回答-- 箭头函数中的 this 和调用时的上下文无关,而是取决于定义时的上下文 这并不是很正确的答案--虽然也不是完全错误 箭头函数中的 this 首先说我的回答中没有错误的部分:箭头函数中的 this 确实和调用时的上下文无关 function make () { return ()=>{ consol

  • pytorch中的上采样以及各种反操作,求逆操作详解

    import torch.nn.functional as F import torch.nn as nn F.upsample(input, size=None, scale_factor=None,mode='nearest', align_corners=None) r"""Upsamples the input to either the given :attr:`size` or the given :attr:`scale_factor` The algorith

  • System.currentTimeMillis()计算方式与时间的单位转换详解

    一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s) 1秒=1,000,000 微秒(μs) 1微秒=1/1,000,000秒(s) 1秒=1,000,000,000 纳秒(ns) 1纳秒=1/1,000,000,000秒(s) 1秒=1,000,000,000,000 皮秒(ps) 1皮秒=1/1,000,000,000,000秒(s) 1分钟=60秒 1小时=60分钟=3600秒 二.System.currentTimeMillis()计算方式 在开发过程中,通常很

  • Python计算矩阵的和积的实例详解

    python的numpy库提供矩阵运算的功能,因此我们在需要矩阵运算的时候,需要导入numpy的包. 一.numpy的导入和使用 from numpy import *;#导入numpy的库函数 import numpy as np; #这个方式使用numpy的函数时,需要以np.开头. 二.矩阵的创建 由一维或二维数据创建矩阵 from numpy import *; a1=array([1,2,3]); a1=mat(a1); 创建常见的矩阵 data1=mat(zeros((3,3)));

  • C语言中的自定义类型之结构体与枚举和联合详解

    目录 1.结构体 1.1结构的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配 2.3位段的跨平台问题 2.4位段的应用 3.枚举 3.1枚举类型的定义 3.2枚举的优点 3.3枚举的使用 4.联合 4.1联合类型的定义 4.2联合的特点 4.3联合大小的计算 1.结构体 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量.结构

  • Vue计算属性与监视属性实现方法详解

    目录 一.计算属性 1.插值语法实现 2.通过方法实现 3.通过计算属性 二.监视属性 三.深度监视 一.计算属性 在开发中,可以有这样的需求,在属性(date)中,有fistName和lastName两个属性,需要将两个属性拼接起来,这种需求也很简单,有以下实现方式 1.插值语法实现 直接在body中将两个数据拼接 <div id="root"> 姓:<input type="text" v-model="fistName"&

  • vue面试created中两次数据修改会触发几次页面更新详解

    目录 面试题: 一.同步的 二.异步的 三.附加 总结 面试题: created生命周期中两次修改数据,会触发几次页面更新? 一.同步的 先举个简单的同步的例子: new Vue({ el: "#app", template: `<div> <div>{{count}}</div> </div>`, data() { return { count: 1, } }, created() { this.count = 2; this.coun

  • Linux 中(加、减、乘、除)实例详解

     Linux 中(加.减.乘.除)实例详解 实现代码: #!/bin/bash num1=10 num2=2 #两个数相加 add=$[$num1+$num2] echo $num1 + $num2 '=' $add #两个数相减 sub=$[$num1-$num2] echo $num1 - $num2 '=' $sub #两个数相乘 mut=$[$num1*$num2] echo $num1 '*' $num2 '=' $mut #两个数相除 div=$[$num1/$num2] echo

随机推荐