【问题标题】:Finding a number in an array在数组中查找数字
【发布时间】:2023-03-08 18:49:02
【问题描述】:

我有一个包含 20 个数字(64 位整数)的数组,例如 10、25、36,43....、118、121(排序后的数字)。

现在,我必须输入数百万个数字(比如 17、30)。

我必须给出的输出是:

for Input 17:

17 is < 25 and > 10. So, output will be index 0.

for Input 30:

30 is < 36 and > 25. So, output will be index 1.

现在,我可以使用线性搜索、二元搜索来做到这一点。有什么方法可以更快地做到这一点吗?输入数字是随机的(高斯)。

【问题讨论】:

  • 二分搜索可能是最快的。
  • 实际上,这个问题很有趣...有一个 bin 序列,您应该按照该顺序检查。您要做的第一件事是派生一个决策树,它告诉您如何进行搜索以获得最小的预期运行时间。然后使用该树进行搜索。根据20 个号码的分布和传入号码的分布,您可以适当地构建此树。
  • 顺便问一下,你是从哪里得到这个问题的?这是熵压缩算法……信息论?
  • @thang,没有。实际上,我是一名硕士生,我在做一个文本处理工具的项目时发现了它。

标签: c++ algorithm search


【解决方案1】:

如果您了解分布,则可以以更智能的方式指导您的搜索

以下是这种二分搜索变体的大致思路:

假设您的数据预计均匀分布在 0 到 100 上。

如果观察到值 0,则从头开始。如果你的值是 37,你从你拥有的数组的 37% 开始。 这是二分搜索的关键区别:您并不总是从 50% 开始,而是尝试从预期的“最佳”位置开始。

如果您知道参数,这也适用于高斯分布数据(如果您不知道它们,您仍然可以从观察到的数据中轻松估计它们)。您将计算高斯 CDF,这会产生开始搜索的位置。

现在进行下一步,您需要优化搜索。在你看到的位置,有一个不同的值。您可以使用它重新估计位置以继续搜索。

现在即使您不知道分布,它也可以很好地工作。因此,您从二分搜索开始,已经查看了 50% 和 25% 的对象。如果您的查询值是例如非常接近 50% 的入口。除非您的数据集非常“笨拙”(并且您的查询与数据不相关),否则这仍然应该优于总是在中间分裂的“幼稚”二分搜索。

http://en.wikipedia.org/wiki/Interpolation_search

预期的平均运行时间显然是 O(log(log(n)),来自 Wikipedia。

更新:因为有人抱怨只有 20 个数字,情况就不一样了。是的,他们是。 使用 20 个数字线性搜索可能是最好的。因为 CPU 缓存。通过少量内存(适合 CPU 缓存)进行线性扫描可以非常快。尤其是展开的循环。但是,恕我直言,这种情况非常可悲且无趣。

【讨论】:

  • +1 根据我的经验,插值搜索胜过二分搜索。
  • -1,这是对插值搜索的错误使用。如果您的搜索空间很大,Interp 搜索效果很好。这个搜索空间只有 20。给你的随机性(高斯)是输入数字,而不是搜索空间。您概述的选择起点的方法不正确。起点应该是根据您知道的传入数据的任何统计属性最有可能的 bin。例如,如果你的 20 个数字是 0 80 ... 99,那么假设传入数据是均匀分布的,你应该总是从检查第一个 bin 开始!
  • 事实上,即使在 wikipedia 文章中:这种方法只有在计算键值之间差异的大小是合理的情况下才有效。
  • @thang 有 20 个数字,由于 CPU 缓存和流水线,即使是线性搜索也可能更快。所以这不是一个论点。他可能想在某个时候扩大规模。维基百科的声明是指这样一个事实,例如在(文本)字符串列表上,插值搜索不会很好地工作;尽管您可以将字符串解释为可变长度数字并尝试进行这种搜索,因为这种差异对于文本来说是不明智的。另外,请注意,对于选择起始 bin 的简单示例,我说“假设在 0...100 上统一”!
  • @ErichSchubert,我不认为是这样。这就是问题的重点——你的搜索空间很小,你需要做很多搜索。事实上,即使有 20 个数字,线性搜索也并不更快。使用 20 个数字,整个搜索空间都在缓存中,因此无论线性还是二进制,您都在缓存上进行操作。此外,线性搜索空间也有可能搞砸分支预测(如果不是更糟的话)。使用二分搜索,您可以进行更少的比较...
【解决方案2】:

我相信对您来说最好的选择是使用upper_bound - 它会发现数组中的第一个值大于您正在搜索的值。

仍然取决于您尝试解决的问题,lower_boundbinary_search 可能是您需要的。

所有这些算法都具有对数复杂度。

【讨论】:

  • 鉴于问题描述,std::binary_search不适合。
【解决方案3】:

没有什么比binary search 更好的了,因为您的数组已排序。

线性搜索是O(n),而二分搜索是O(log n)

编辑:

插值搜索做了一个额外的假设(元素必须是均匀分布的)并且每次迭代进行更多的比较。

您可以同时尝试并根据经验衡量哪个更适合您的情况

【讨论】:

  • 不正确。看我的回复。如果您对分布有一些的想法,可以在log(log(n)) 中进行插值搜索
  • 还是二分查找的变种
  • 是的,但它不是 二分搜索 算法,它在各种类库中以binarySearchstd::binary_search 的形式提供,总是从 50% 开始。就像 A* 不是 Dijkstra,尽管它是它的一个变体。
  • 我明白你的意思,但是如果我们搜索的数组是纯随机生成的,即不是高斯数组,那么知道要搜索的数字的分布可能没有那么有用
  • @iTech,如果您按照我的描述调整比较指数以优化预期的比较次数,则可以提高性能。我想,从技术上讲,这仍然是二进制搜索,因为在每一步它都会将空间分成 2 个部分,尽管部分不相等。我认为最坏和平均情况仍然是 O(log n),但平均情况的常数可能会更小,具体取决于 20 数字的分布与传入数字的分布有多大不同。如果您进行 100 万次搜索,这可能很重要......
【解决方案4】:

事实上,这个问题很有趣,因为它是一个信息论框架的重铸。

给定 20 个数字,您最终将得到 21 个 bin(包括 最后一个)。

对于每个传入的号码,您将映射到这 21 个垃圾箱之一。这种映射是通过比较完成的。每次比较都会为您提供 1 位信息(= -- 两种状态)。

所以假设传入的数字需要5次比较才能确定它属于哪个bin,那么就相当于用5个比特来表示那个数字。

我们的目标是尽量减少比较次数!我们有 100 万个数字,每个数字属于 21 个有序的代码字。我们如何做到这一点?

这正是一个熵压缩问题。

让 a[1],.. a[20] 成为你的 20 个数字。

令 p(n) = pr { 传入号码为

如下构建决策树。

Step 1.

   let i = argmin |p(a[i]) - 0.5|

   define p0(n) = p(n) / (sum(p(j), j=0...a[i-1])), and p0(n)=0 for n >= a[i].
   define p1(n) = p(n) / (sum(p(j), j=a[i]...a[20])), and p1(n)=0 for n < a[i].

Step 2.

   let i0 = argmin |p0(a[i0]) - 0.5|
   let i1 = argmin |p1(a[i1]) - 0.5|

等等……

当我们完成时,我们最终得到:

i, i0, i1, i00, i01, i10, i11, etc.

其中的每一个 i 都为我们提供了比较位置。

所以现在我们的算法如下:

让 u = 输入数字。

if (u < a[i]) {
   if (u < a[i0]) {
      if (u < a[i00]) {
      } else {
      }
   } else {
      if (u < a[i01]) {
      } else {
      }
   }
} else {
   similarly...
}

所以 i 定义了一棵树,而 if 语句正在遍历树。我们也可以把它放到一个循环中,但是用一堆 if 来说明会更容易。

例如,如果您知道您的数据均匀分布在 0 到 2^63 之间,而您的 20 个数字是

0,1,2,3,...19

然后

i      = 20  (notice that there is no i1)
i0     = 10
i00    = 5
i01    = 15
i000   = 3
i001   = 7
i010   = 13
i011   = 17
i0000  = 2     
i0001  = 4     
i0010  = 6     
i0011  = 9
i00110 = 8
i0100  = 12
i01000 = 11
i0110  = 16
i0111  = 19
i01110 = 18

好吧,基本上比较如下:

if (u < a[20]) {
  if (u < a[10]) {
     if (u < a[5]) {
     } else {
         ...
     }
  } else {
     ...
  }
} else {
  return 21
}

请注意,我不是在进行二分搜索!我首先检查终点。为什么?

有 100*((2^63)-20)/(2^63)% 的几率会大于 a[20]。这基本上是 99.999999999999999783159565502899% 的机会!

因此,对于具有上述属性的数据集,该算法的预期比较次数为 1! (这比 log log 更好:p)

请注意,我在这里所做的是,我基本上使用较少的比较来查找更可能的数字,并使用更多的比较来查找不太可能的数字。例如,数字 18 需要 6 次比较(比二分查找多 1 次);但是,数字 20 到 2^63 只需要 1 次比较。同样的原则也用于无损(熵)数据压缩——使用更少的比特来编码经常出现的代码字。

构建树是一次性过程,您可以在 100 万次之后使用该树。

问题是……这棵决策树什么时候变成二分搜索?作业练习! :p 答案很简单。类似于无法再压缩文件的情况。

好的,所以我没有把这个从我的背后拉出来......基础在这里:

http://en.wikipedia.org/wiki/Arithmetic_coding

【讨论】:

    【解决方案5】:

    您可以使用std::lower_boundstd::upper_bound 执行二进制搜索。这些给你返回迭代器,所以你可以使用std::distance 来获取索引。

    【讨论】:

    • 还有..std::binary_search
    • @Rapptz 如果搜索到的元素不存在于数组中则不起作用。
    • @Rapptz:std::binary_search 几乎没用。当您进行二分搜索(或任何搜索)时,您几乎总是想知道元素的位置。 std::binary_search 所做的只是告诉你truefalse
    • @BenjaminLindley 是的,我忘记了,我以为它会像其他查找算法一样将迭代器返回到元素。
    • @Rapptz 即使这样做了,也只有 iff 元素在数组中才有用。 OP 正在寻找界限。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-12-14
    • 2020-10-25
    • 1970-01-01
    • 2017-05-08
    • 1970-01-01
    • 2020-06-19
    相关资源
    最近更新 更多