用于有效百分位查找的数据结构?

Posted

技术标签:

【中文标题】用于有效百分位查找的数据结构?【英文标题】:Data structure for efficient percentile lookups? 【发布时间】:2012-12-09 11:50:36 【问题描述】:

假设您有大量的键/值对,其中的值是任意实数。您有兴趣创建支持以下操作的数据结构:

插入,将新的键/值对添加到集合中, 删除,从集合中删除键/值对, 百分位,它告诉与给定键关联的值在哪个百分位,并且 Tell-Percentile,它接受一个百分位数并返回其值为至少在给定百分位数处的最小值的键。

此数据结构可用于,例如,在接收全国范围的考试成绩流时有效地确定给定学生的百分位数,或识别服务质量异常好或异常差的医院。

有没有办法让这些操作高效运行(例如,亚线性时间?)

【问题讨论】:

如果你想每次都查找 same 百分位,你可以维护一对堆来保存高于/低于所需百分位的值。见***.com/questions/3738349/… 【参考方案1】:

实现此数据结构的一种可能方法是使用order statistic tree 和hash table 的混合。

顺序统计树是一种平衡二叉搜索树,除了正常的二叉搜索树操作外,还支持另外两种操作:

Rank(key),返回树中小于给定元素的元素个数,以及 Select(k),返回树中第 k 个最小的元素。

可以通过在旋转过程中保留额外信息来扩充正常的平衡二叉搜索树(例如,red/black tree 或 AVL tree)来构建顺序统计树。这样一来,订单统计树上的所有正常 BST 操作都可以在 O(log n) 时间内运行,而额外的操作也可以在 O(log n) 时间内运行。

现在,假设您纯粹存储值分数,而不是关键/百分位分数。在这种情况下,如下实现百分位查找将非常简单。将所有值存储在订单统计树中。要确定给定值的百分位分数,请使用顺序统计树上的 rank 操作来查找该值出现在哪个索引处。这给出了一个数字,范围从 0 到 n - 1(其中 n 是树中元素的数量),表示该分数在顺序统计树中的位置。然后,您可以将该数字乘以 99 / (n - 1),根据需要获得在 0 到 99 范围内的值的百分位数。

要确定大于某个百分位的最小值,您可以使用 select 操作,如下所示。给定一个介于 0 和 99 之间的百分位数,将该百分位数乘以 99 / (n - 1) 得到一个介于 0 和 n - 1 之间的实数,包括 0 和 n - 1。取该数的上限会产生 0 到 n - 1 范围内的自然数,包括 0 到 n - 1。然后可以使用顺序统计树上的 select 操作来查找范围内等于或高于给定百分位数的第一个值。

但是,这些操作假设我们在数据结构中有纯值,而不是键/值对。为了使这个操作适用于键/值对,我们将扩充我们的数据结构如下:

    我们将在每个节点中存储键/值对,而不仅仅是存储值。顺序统计树将纯粹按值对键/值对进行排序,并将键作为卫星数据携带。 我们将存储一个辅助哈希表,将键映射到它们的关联值。

这两项更改使我们可以为我们的数据结构实现所需的功能。为了让数据结构通过键进行百分位查找,我们首先使用给定键查询哈希表以查找其关联值。然后,我们像以前一样对值进行百分位查找。为了让数据结构告诉我们一个键的值是第一个或高于给定百分位数的键,我们如上所述在顺序统计树上执行正常的 find-percentile 操作,然后查找与给定值关联的键。

如果我们假设哈希表使用链式哈希,那么每个操作所需的时间如下:

插入:将值/键对插入订单统计树的 O(log n) 时间,加上将键/值对插入哈希表的 O(1) 分摊时间。总时间为 O(log n) 摊销。 删除:从订单统计树中删除值/键对的 O(log n) 时间,加上 (1) 从哈希表中删除键/值对的摊销时间。总时间为 O(log n) 摊销。 Percentile:O(1) 预期时间来查找与键关联的值,O(log n) 时间来执行 rank 操作,以及 O(1) 额外时间是时候将排名映射到百分位数了。预计总时间为 O(log n)。 Find-Percentile:将百分位数映射到排名所需的时间为 O(1),执行 select 操作所需的时间为 O(log n)。总时间是 O(log n) 最坏情况。

希望这会有所帮助!

【讨论】:

【参考方案2】:

有一种简单高效的可能性:

如果您只能在最终填满的学生结构中搜索百分位数,那么:

当您不知道元素的数量时,使用 ArrayList 动态构建。 如果您知道它们,则直接从数组开始,否则从动态数组创建数组。 (例如 java 中的 ArrayList)。

插入:不需要,替换为在末尾添加,然后排序一次。删除:不需要,如果你能忍受的话。 tell-percentile:更简单:非常接近:元素[长度 * 百分位数]:O(1)

在实践中,数组方法会比平衡树方法快得多,至少在 java 中是这样, 当您的应用程序可以构建一次数组时(例如每日学生评估,每天构建它)

我已经使用自写的 ArrayListInt 实现了(我的)上述算法,它与 ArrayList 相同,但使用原始类型(double、int)而不是对象类型。当所有数据都被读取后,我对其进行了排序。

您还想要键值: 我只想添加一个树图(平衡树)。现在有点怀疑 TreeMap 和附加的百分位数数组是否有意义:这取决于您必须搜索的频率,以及内存使用量与搜索时间的关系。

更新:

结果:treeset vs sorted array(动态构建数组,然后最终排序一次:

num elements: 1000 treeSet: 4.55989 array=0.564159
num elements: 10000 treeSet: 2.662496 array=1.157591
num elements: 100000 treeSet: 31.642027 array=12.224639
num elements: 1000000 treeSet: 1319.283703 array=140.293312
num elements: 10000000 treeSet: 21212.307545 array=3222.844045

这个数量的元素(1e7)现在接近极限(1GB 堆空间),在下一步中内存将耗尽(已经发生在 1e7,但是在树集之后清理内存,测量 1e7 的运行有效,也是。

缺少的是搜索时间,但是带有 binsearch 的排序数组只能被哈希表击败

最后: 如果您可以建立学生集一次,例如每天,那么使用数组方法可以提供更简单的百分位数搜索。

【讨论】:

-1:哎呀——看来你可能从根本上误解了复杂性分析。对于 N 的特定(小)值,O(N) 方法可能比 O(log(N)) 方法更快这一事实并不意味着O(N) 方法总是 更好,正如你所说。 Big O 分析的重点是渐近性能 - 因为N 倾向于更大的值,所以O(log(N)) 方法将胜过O(N),无论常数/低阶因素如何。 你确定 memcopy 移动 10000 个元素的速度比 BST 查找 10000 个元素树的速度快吗?随着现代内存分配器针对局部性进行优化,我非常怀疑这是真的。你能用一些数据或参考来备份吗? @Yikes:我写道:在实践中总是赢,这意味着真正的应用程序,而不是你提供 MAX_INT 元素的基准。进一步不要忘记内存使用情况,如果计算机在 O(ld N) 胜过 O(N) 之前内存不足,那么您可能会处于 O(ld N) 因更高的内存 usgae 永远无法受益的情况从它的渐近优势。 @AlexWien- 我可能错了,但我相当有信心您看到的减速是因为链表必须遍历列表的一半才能找到插入点。换句话说,数组和链表都必须做 O(n) 的工作:数组用于洗牌,列表用于搜索。我严重怀疑这是否意味着一个巨大的 BST 会比一个巨大的数组慢。我所有的实践经验都与你的说法相矛盾。 @templatetypedef 我的旧方法是错误的,保持数组排序不是一个好主意,所以我更改为构建数组并对其进行一次排序,如果您与不可变结构的限制一旦建立。 (对于高达 100k 的值,即使对于动态结构,我仍然会使用数组方法,因为它实现起来更便宜):答案已更新

以上是关于用于有效百分位查找的数据结构?的主要内容,如果未能解决你的问题,请参考以下文章

查找名为 mag(地震震级)的列的百分位数

Redshift - 超过百分位的第一个值

查找数据的百分比

环境空气质量监测30,50,90百分位是啥意思

计算数据集之间相似性百分比的有效方法

滚动百分位函数在列中输出 0?