Is this searching algorithm optimal?
我有两个列表,L 和 M,每个列表包含数千个 64 位无符号整数。我需要找出 L 的任意两个成员的总和是否本身就是 M 的成员。
是否可以改进以下算法的性能?
1 2 3 4
| Sort(M)
for i = 0 to Length(L)
for j = i + 1 to Length(L)
BinarySearch(M, L[i] + L[j]) |
- 一旦你找到一个例子,你能停下来吗?如果是这样,那很重要,但是从问题的措辞来看是否允许这样做并不是很清楚。此外,将 M 放入哈希表将比使用二分搜索给出渐近更快的结果。
-
抱歉,不清楚 - 不,一旦找到单个匹配项,搜索就无法停止。
-
如果您对 M 列表进行预处理(对其进行排序或将其放入哈希图中),也许您可??以将最高值存储在 M 中并跳过 L 中大于 max(M) 的元素?
-
列表是否已经排序?
-
@Tom Anderson:几乎可以肯定不是,因为问题中的代码片段显示他对其中一个输入进行了排序
-
@尼克:诅咒!我的计划落空了!
(我假设你的目标是找到 L 中所有与 M 相加的对)
忘记哈希表!
对两个列表进行排序。
然后执行算法的外循环:遍历 L 中的每个元素 i,然后遍历 L 中的每个较大元素 j。在进行过程中,形成总和并检查它是否在 M 中。
但不要使用二分搜索:只需从您最后查看的位置进行线性扫描。假设您正在处理某个值 i,并且您有某个值 j,然后是某个值 j'。搜索 (i j) 时,您将到达 M 中找到该值的点,或第一个最大值。您现在正在寻找 (i j');因为 j' > j,你知道 (i j') > (i j),所以它在 M 中不能比你得到的最后一个位置更早。如果 L 和 M 都平滑分布,则很有可能在 M 中找到 (i j') 的点只有一点距离。
如果数组不是平滑分布的,那么比线性扫描更好的可能是某种跳跃扫描 - 一次向前看 N 个元素,如果跳跃太远,则将 N 减半。
我相信这个算法是 O(n^2),它和任何提出的散列算法一样快(它有一个 O(1) 的原始操作,但仍然需要 O(n**2) 个。这也意味着您不必担心 O(n log n) 进行排序。它具有比散列算法更好的数据局部性 - 它基本上由数组上的成对流式读取组成,重复 n 次。
编辑:我已经编写了 Paul Baker 的原始算法、Nick Larsen 的哈希表算法和我的算法的实现,以及一个简单的基准测试框架。实现很简单(哈希表中的线性探测,线性搜索中没有跳过),我不得不猜测各种大小参数。有关代码,请参见 http://urchin.earth.li/~twic/Code/SumTest/。我欢迎对任何实现、框架和参数提出更正或建议。
对于每个包含 3438 个项目的 L 和 M,值范围从 1 到 34380,并且 Larsen 的哈希表的负载因子为 0.75,运行的中位时间为:
-
贝克(二进制搜索):423 716 646 ns
-
拉森(哈希表):733 479 121 ns
-
Anderson(线性搜索):62 077 597 ns
差异比我预期的要大得多(而且,我承认,不是我预期的方向)。我怀疑我在实施过程中犯了一个或多个重大错误。如果有人发现了,我真的很想听听!
有一件事是我在定时方法中分配了拉森的哈希表。因此,它付出了分配和(一些)垃圾收集的成本。我认为这是公平的,因为它只是算法需要的临时结构。如果您认为它是可以重用的东西,那么将它移动到一个实例字段中并只分配一次(并且 Arrays.fill 它在定时方法中用零填充)就足够简单了,看看它如何影响性能。
-
您在 M 中的查找每个 L^2 至少有 1 次查找,而 M 的散列集正好有 1 次查找,无论它在集合中的哪个位置。此外,您的答案暗示了不同的实现,具体取决于输入的分布,这是算法未知的。
-
查找次数:我认为在内部循环中通过 L 的任何一次都不会超过 |M|在 M 中查找 - 怎么可能?假设来自 L 的所有对都在 M 覆盖的范围内,这意味着查找的总数最多为 |L| * |M|。哈希表将恰好是 |L|^2。我希望由于局部性,我的查找会更快——尽管考虑到数据集的小尺寸("数千"),无论如何它可能都适合缓存。
-
分布:"不同的实现" - 你是什么意思?你是说跳过还是不跳过?我想实现者对问题了解得更多,将能够很好地猜测跳过或其他一些技巧是否有用。但如果不是,则可以自适应地决定。我正在考虑在 Timsort 中执行此操作的有点类似的跳过("galloping"?)行为。
-
@Tom:查找次数:如果我正确理解您的想法,您在 M 中持有一个指针并为每个 L0 L1 遍历它,这意味着您在 M 中至少对 L^2 个查找中的每个查找进行了 1 次查找。对于每个 L1 L2,散列集在 M 中恰好有 1 次查找。
-
@Tom:分发:我同意可以使用有关输入的更多信息进行改进,但是,我们没有从问题中得到任何迹象表明有相关知识。这是一个很好的答案,只是针对一个还没有被问到的问题。
-
@Nick 在查找 i j 时,Tom 可能会发现 M 中的查找(称为 m)大于 i j。查找 i\\' j\\' 时要做的下一件事是检查 m 是否大于 i\\' j\\'。如果是,那么我们在没有任何查找的情况下处理了该对(因为我们可以记住该值)。 Nick\\'s array case 需要在 M 中进行可变数量的查找,但平均不会超过 1,并且可以更低。
-
@Nick:我已经编写了代码并报告了基准测试。如果您能告诉我我是否搞砸了您的算法的实现,我将不胜感激。
-
@Tom 在设置测试方面做得很好。我会检查一下,今晚晚些时候再给你答复。
-
@Tom:你的散列集实现是不必要的,Java 内置了一个。除此之外,我不喜欢你选择少于随机数的决定。这个问题对值没有限制,它实际上是一个更简单的实现,没有额外的东西。此外,在定时器内部进行分配是公平的,因为这是必要的。在我的实现中使用与您在您的实现中使用的截断技术相同的截断技术也是公平的。
-
@Tom:在我的 c# 实现中,我发现 c# 可以比调用 hashset.contains() 更快地访问数组索引。这使您的 impl 更快。这促使我将您的搜索放入它自己的名为 search 的函数中(或者我可以在我的函数中轻松实现我自己的散列集)。在那之后,每次我的构建哈希集与对数组进行排序所需的时间都更快。然后我意识到所有这些都是实现细节,我们可以整天来回执行此操作。问题是 [language-agnostic] ,这意味着只有渐近分析才有用。
-
@Nick:我自己写的,因为我知道它会比 Java 的通用哈希集快得多,而且很容易做到。正如您所看到的,它在 C# 中也更快——而在 Java 中更糟,因为 Java 对基本类型(如泛型集合中的 long)进行了更基本的处理。抱歉有点不确定的数字生成,但正确的算法很难实现,我怀疑它会产生显着差异。
-
@Nick:你能扩展一下"将你的搜索放入它自己的名为搜索的函数中"的意思吗?
-
@Tom:我在 c# 和更高版本的 java 中使用了内置哈希集,即使使用内存分配,这两种情况的构建速度也比对数组进行排序更快。两个函数中的工作都是在求和完成后完成的,到那时它们是相同的实现。工作函数是 M.contains(sum),我通过散列集上的方法检查它。您内联实现它,避免了函数调用的成本,所以我将您的内联解决方案提取到它自己的方法中(或者我可以编写自己的内联哈希集实现)。
-
@Tom 最后一点,您的哈希集 impl 是一个哈希表,搜索时间为 O(n),应将其编辑为常数时间。
-
@Nick:嗯,我不相信这是 O(n)。您是否将线性探测误认为线性扫描?我还是不明白你对哈希集和哈希表的区别。
-
@Tom:同样,散列集对于每个散列正好有 1 个值,散列表有一个包含每个散列的所有值的桶。在散列集中,仅保存特定散列的第一个值,尝试输入给定散列的第二个值失败。然后 cointains impl 简单地获取哈希值,不需要迭代。很容易为您的 impl 生成数千个唯一值,这些值都具有相同的哈希值,这将需要精确的 O(n) 搜索时间。
-
@Nick:那么您对散列集的想法如何处理散列到集合中相同位置的两个不同值?我可以构造哈希集吗?
问题中示例代码的复杂度为 O(m log m l2 log m) 其中 l=|L|和 m=|M|因为它对 L (O(l2)) 中的每一对元素运行二进制搜索 (O(log m)),并且首先对 M 进行排序。
假设哈希表插入和查找是 O(1) 操作,用哈希表替换二进制搜索将复杂度降低到 O(l2)。
这是渐近最优的,只要你假设你需要处理列表 L 上的每一对数字,因为有 O(l2) 个这样的对。如果 L 上有几千个数字,并且它们是随机的 64 位整数,那么您肯定需要处理所有对。
您可以以 n 为代价创建散列集,而不是以 n * log(n) 为代价对 M 进行排序。
您还可以在迭代时将所有总和存储在另一个哈希集中,并添加检查以确保您不会两次执行相同的搜索。
- 注意:我的意思是哈希集,而不是哈希表。
-
我理解\\'散列集\\' 的意思是\\'用散列表实现的集\\'。你说的这两个词是什么意思?
-
散列集通常用作"已被访问过"的概念,每个散列值只能存在 1 个值。在哈希表中,哈希值表示一个桶,里面装满了所有具有该哈希值的项目。
或者,将 L 的所有成员添加到哈希集 lSet,然后迭代 M,对 M 中的每个 m 执行以下步骤:
将 m 添加到 hashset mSet - 如果 m 已经在 mSet 中,则跳过此迭代;如果 m 在 hashset dSet 中,也跳过此迭代。
从m中减去L中小于m的每个成员l得到d,并测试d是否也在lSet中;
如果是,则将 (l, d) 添加到某个集合 rSet;将 d 添加到哈希集 dSet。
这将需要更少的迭代,但会消耗更多的内存。如果这是为了提高速度,您将需要为结构预先分配内存。
您可以通过使用除已排序 M 数组之外的哈希表来避免二进制搜索。