どこを開始します
数日前、友人との問題を議論し、そのアプリケーションは、メンバーの数十万人が、対応するポイントを持っていて、今の順位を求めたい、とは良いソリューションがありません尋ねました。この問題は、一般的な考えることができる多くの場所で見ることができ、従来の一般的なアプローチは高いが真の順位を計算するためにランク付け、テーブルルックアップテーブルの真ん中にバッチデータのタイミングの計算を実行し、直接ライン上、または唯一のTOP Nのリーダーボードを表示することですこの名前は離れますxxxには比較的低い順位が直接表示されます。しかし、視点の探査のために、私はまだリアルタイム計算手段があるかどうかを知りたい、と効率が受け入れることができます。
庭では良いに検索をブログ記事、基本的なリスト一般的なシナリオは、各アルゴリズムは非常に良いプログラムであるバイナリツリーアルゴリズムに基づいて、詳細な特定のアイデアに記載されており、記事はだけなので、コード与えられた何の考えを与えませんでした私はそれらを達成するためにC#を使用することにしました。
ここでは唯一の要件は合理的である、ビジネスに関係なく、特定のアルゴリズムを議論します。
分析的思考
非常に詳細に書かれた記事の核となるアイデアであるアルゴリズムの面では、私が唯一の特定の例で、ここで説明を繰り返すことはしません、このプロセスを示しています。
仮定積分範囲は0〜5である、それはこれまでに分割することはできません我々は常に中央値のパーティションは、バイナリツリーとして形成されています。
各ツリーノードは、二つの情報含み:ノードの範囲range[min,max)
とヒット数カウンタをcount
、リーフノードが隣接している必要があり番号2の範囲を見ることができます。
あなたは今、3ポイントがツリーに挿入する必要がある場合は、それがどのように動作しますか?次のようにリーフノードまでトラバースされるまで決定されるが再び含まれている場合、左、右の子ノードに含まれる子ノードの現在のノード、カウンタがインクリメントされながら、次に同じ決意され、それぞれ、現在のルートノードから開始し、走査順序は次のとおりです。
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