【问题标题】:Data structure / algorithm for query: filter by A, sort by B, return N results查询的数据结构/算法:按A过滤,按B排序,返回N个结果
【发布时间】:2011-12-15 20:55:09
【问题描述】:

假设您有一大组具有AB 属性的#m 对象。您可以使用哪种数据结构作为索引(或哪种算法)来提高以下查询的性能?

find all objects where A between X and Y, order by B, return first N results;

即按范围A 过滤并按B 排序,但只返回前几个结果(例如,最多1000 个)。插入非常罕见,因此可以接受繁重的预处理。我对以下选项感到满意:

  1. 记录(或索引)按 B 排序:按B 顺序扫描记录/索引,返回第一个N,其中A 匹配X-Y。在最坏的情况下(很少有对象与范围 XY 匹配,或者匹配位于记录/索引的末尾),这将变为 O(m),这对于大小为 m 的大型数据集来说还不够好。

  2. 记录(或索引)按 A 排序:进行二分查找,直到找到与 X-Y 范围匹配的第一个对象。扫描并创建一个对与范围匹配的所有 k 对象的引用数组。按 B 对数组进行排序,返回第一个 N。那是O(log m + k + k log k)。如果k 很小,那么这确实是O(log m),但如果k 很大,那么排序的成本甚至比线性扫描所有mobjects 的成本还要糟糕。

  3. 自适应 2/1:对 X-Y 范围内的第一个匹配项进行二分搜索(使用 A 上的索引);对范围的最后一个匹配项进行二进制搜索。如果范围很小,继续算法 2;否则恢复到算法 1。这里的问题是我们恢复到算法 1 的情况。虽然我们检查了“很多”对象通过了过滤器,这对于算法 1 来说是很好的情况,但这个“很多”最多是一个常数 (渐近地,O(n) 扫描将始终胜过O(k log k) 排序)。所以我们仍然有一个O(n) 算法来处理一些查询。

是否有允许在亚线性时间内回答此查询的算法/数据结构?

如果不是,为了达到必要的性能,有什么好的折衷办法?例如,如果我不保证返回对象的B 属性的最佳排名(召回

【问题讨论】:

  • 你在使用一些数据库吗?还是在硬文件中序列化?或者它在内存中的对象数组中
  • 数据适合内存,所以假设。没有数据库(即在某种意义上,应用程序就是数据库,问题是如何计划/回答这个查询:-)
  • AB 整数吗?或者AB 可以翻译成整数吗?
  • 是的,A 和 B 是/可以是整数。此外,在我想到的更复杂和更具体的问题中,可以将 A 减少到少数可能的范围(例如 0-100、101-200、...)——但实际问题要多得多复杂。

标签: algorithm search data-structures indexing


【解决方案1】:

你问的问题本质上是一个更一般的版本:

问。你有一个排序的单词列表,每个单词都有一个权重,你想要所有与给定查询 q 共享前缀的单词,你希望这个列表按关联的 权重排序.

我说的对吗?

如果是这样,您可能想查看这篇论文,该论文讨论了如何在 O(k log n) 时间内完成此操作,其中 k 是所需输出集中的元素数,n 是原始输入集中的记录数。我们假设 k > log n.

http://dhruvbird.com/autocomplete.pdf

(我是作者)。

更新:我可以添加的进一步改进是,您所问的问题与二维范围搜索有关,您希望在给定 X 范围内的所有内容和前一组中的前 K,按 Y 排序-范围。

2D 范围搜索可让您找到 X/Y 范围内的所有内容(如果您的两个范围都已知)。在这种情况下,您只知道 X 范围,因此您需要重复运行查询并在 Y 范围上进行二进制搜索,直到获得 K 个结果。如果使用分数级联,每个查询都可以使用 O(log n) 时间执行,如果使用朴素方法,则可以使用 O(log2n) 时间。它们中的任何一个都是亚线性的,所以你应该没问题。

此外,列出所有条目的时间会为您的运行时间增加一个额外的 O(k) 因素。

【讨论】:

  • 我错过了这个答案。谢谢,我会尽快检查的。
【解决方案2】:

假设N << k < n,可以在O(logn + k + NlogN)中完成,类​​似于你在选项2中建议的,但是节省一些时间,你不需要对所有k个元素进行排序,而只对N个,这是很多的更小!

数据库按A排序。

(1) find the first element and last element, and create a list containing these
    elements.
(2) find the N'th biggest element, using selection algorithm (*), and create a new 
    list of size N, with a second iteration: populate the last list with the N highest 
    elements.
(3) sort the last list by B.

Selection algorithm: 找到第 N 个最大的元素。这里是O(n),或者O(k),因为列表的大小是k。

复杂性
第一步是微不足道的O(logn + k)
第 2 步是 O(k) [selection],另一个迭代也是 O(k),因为这个列表只有 k 个元素。
第3步是O(NlogN),简单排序,最后一个列表只包含N个元素。

【讨论】:

  • 啊,你是对的,我不需要完整的排序。通过部分排序,我可以扩展选项 2 的用处,很好的答案。尽管如此,当过滤器使 k 接近 n(原始中的 m)时,它仍然退化为 O(m) - 具有更差的常数。对这种情况有什么建议吗? (我对你的答案投了赞成票,稍后我会接受最好的)。
  • Jim Mischel:是的,剩下的问题是,如果 k 不小,如果它可以任意逼近 m,那么这仍然是线性的。有什么想法吗? (不过,amit 的解决方案扩大了选项 2 可能涵盖的范围)
【解决方案3】:

如果您要返回的项目数量很少(最多约为项目总数的 1%),那么简单的堆选择算法就可以很好地工作。见When theory meets practice。但它不是亚线性的。

对于预期的次线性性能,您可以按A 对项目进行排序。查询时,使用二分搜索找到A >= X 所在的第一个项目,然后使用我在该博客文章中概述的堆选择技术顺序扫描项目直到A > Y

这应该给您O(log n) 用于初始搜索,然后是O(m log k),其中m 是项目数,X <= A <= Yk 是您想要返回的项目数。是的,对于某些查询,它仍然是O(n log k)。决定因素将是m 的大小。

【讨论】:

  • 嗨,吉姆。如果我理解,您的建议类似于我概述的自适应选项(加上 amit 的改进):当您的 k 较小时,让算法以 O(n log k) 排序为界,否则只需对 B 的索引进行线性扫描.但是您依靠堆选择来获取详细信息。所以你认为没有保证的亚线性解决方案?
  • 你说k是“最多1000”。如果列表非常大(一百万个或更多),并且您最多只返回 1,000 个项目,那么您的问题不是 k 的大小,而是包含 A 的项目数您指定的界限。尽管您可能会说我的方法与 amit 的方法基本相同,但“依赖于堆选择的细节”通常会提供更好的性能。
  • 吉姆,在迷失细节之前,我试图了解您的答案的要点。我会更仔细地调查。
【解决方案4】:

在 A 上设置 segment tree,并为每个段预先计算范围内的前 N ​​个。要查询,请将输入范围分成 O(log m) 段并合并预先计算的结果。查询时间为O(N log log m + log m);空间为 O(m log N)。

【讨论】:

  • 维基百科的段树文章侧重于“给定一个点,找出哪些段包含它”。您是否建议反过来使用分段树?给定范围段,找出哪些点在范围内?到目前为止,我很难理解您提出的算法的确切细节。无论如何,谢谢您的回答。
【解决方案5】:

这并不是一个完全充实的解决方案,只是一个想法。在 A 轴和 B 轴上构建一个quadtree 怎么样?比如说,你会以广度优先的方式走下树;那么:

  • 每当您发现 A 值都超出给定范围 [X, Y] 的子树时,您会丢弃该子树(并且不递归);
  • 每当您找到所有 A 值都在给定范围 [X, Y] 内的子树时,将该子树添加到您正在构建的集合 S 中,并且不递归;
  • 每当您找到一个子树,其中一些 A 值在 [X, Y] 范围内和一些范围外,您就递归到它。

现在你有了所有最大子树的集合 S,其 A 坐标在 X 和 Y 之间;这些子树最多有 O(sqrt(m)),我将在下面展示。

其中一些子树将包含 O(m) 个条目(当然它们将包含 O(m) 个添加在一起的条目),因此我们无法对所有子树的所有条目执行任何操作。我们现在可以在 S 中创建一个子树堆,使得每个子树的 B 最小值小于堆中其子树的 B 最小值。现在从堆的顶部节点提取 B 最小元素,直到你有 N 个;每当您从具有 k 个元素的子树中提取一个元素时,您都需要将该子树分解为 O(log(k)) 个不包含最近提取的元素的子树。

现在让我们考虑复杂性。找到 O(sqrt(m)) 个子树最多需要 O(sqrt(m)) 个步骤(读者练习,使用下面证明中的参数)。我们可能应该在找到它们时将它们插入堆中;这将需要 O(sqrt(m) * log(sqrt(m))) = O(sqrt(m) * log(m)) 步骤。从堆中的 k 元素子树中提取单个元素需要 O(sqrt(k)) 时间来查找元素,然后插入 O(log(sqrt(k))) = O(log(k)) 子树进入大小为 O(sqrt(m)) 的堆需要 O(log(k) * log(sqrt(m))) = O(log(k) * log(m)) 步骤。我们可能会更聪明地使用势能,但我们至少可以将 k 与 m 绑定,这样就剩下 N*(O(sqrt(k) + log(k)*log(m))) = O(N * (sqrt( m) + log(m)^2) = O(N*sqrt(m)) 提取步骤,总共 O(sqrt(m)*(N + log(m))) 步骤......这是在 m 中次线性。


这是 O(sqrt(m)) 子树边界的证明。构建四叉树有多种策略,但为了便于分析,假设我们制作二叉树;在根节点中,我们围绕具有中值A坐标的点根据A坐标拆分数据集,然后下一层我们围绕具有中值B坐标的点根据B坐标拆分数据集(即该半树中包含的一半点的中位数),并继续每级交替方向。

树的高度是 log(m)。现在让我们考虑需要递归多少个子树。如果子树包含 A 坐标 X,或者它包含 A 坐标 Y,或者两者都包含,我们只需要递归。在第 (2*k) 层下,总共有 2^(2*k) 个子树。到那时,每个子树的 A 范围已经被细分了 k 次,每次我们这样做时,只有一半的树包含 A 坐标 X。所以最多 2^k 个子树包含 A 坐标 X。同样,在大多数 2^k 将包含 A 坐标 Y。这意味着我们总共将递归到最多 2*sum(2^k, k = 0 .. log(m)/2) = 2*(2^( log(m)/2 - 1) + 1) = O(sqrt(m)) 个子树。

由于我们在第 (2*k)' 层下最多检查 2^k 个子树,因此我们还可以在 S 中添加最多该层的 2^k 个子树。这给出了最终结果。

【讨论】:

  • Erik,感谢您花这么多时间回答问题。我正在尝试评估您的提案,但我不确定我是否了解某些细节。我们可以从关于构建堆的部分开始吗?考虑 [X, Y] 限制所有元素的简单情况;在那种情况下,S = {entire_quadtree},对吗?我不确定我是否理解下一步是什么。是不是对于 S 中的每个子树,您都使用该子树的值构建了一个堆?在所考虑的情况下,为具有m 节点的子树构建堆需要 O(m) 时间,对吧? (不是亚线性时间)。
  • 1.正确,S = {entire_quadtree} - 一个单例集,一个条目,整个四叉树。 2. 下一步:构建堆,最初是单例 {entire_quadtree}。然后,您将迭代循环以找到 N 个最小元素,每一步都在堆顶部获取子树的 B 最小元素。最初,这意味着取整个四叉树的 B 最小元素(此时堆的唯一元素)。现在你想把整个四叉树,减去它的 B 最小元素,放回堆中;我认为最简单的方法可能是将树分成 (...)
  • (...) 不包含 B-minimal 元素的最大子树并将它们全部放入堆中。 (请注意,为了能够维护堆顺序,四叉树的每个节点都需要有一个指向其 B 最小节点的指针;如果这样做,随着拆分,您可以轻松地维护该属性。)重申这里的关键点:堆包含子树,而不是域元素
  • Erik:我还在研究这个问题,我会尽快发表评论。顺便说一句:您对四叉树(在交替维度的中位数处切割)的描述在技术上不是 k-d 树吗?
  • 我还没有时间就这个问题提供(接近)最终评论。但是,总而言之,这个解决方案似乎足以解决上述问题,所以我接受了答案。稍后我会发送更多的cmets。谢谢!
【解决方案6】:

您描述的结果是大多数搜索引擎旨在实现的目标(排序、过滤、分页)。如果您还没有这样做,请查看 Norch 或 Solr 等搜索引擎。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-08
    • 2015-11-06
    • 1970-01-01
    • 2012-01-08
    • 1970-01-01
    相关资源
    最近更新 更多