【问题标题】:What would be the most efficient way to find a[i] = i in a sorted array?在排序数组中找到 a[i] = i 的最有效方法是什么?
【发布时间】:2012-11-17 22:34:53
【问题描述】:

给定一个数组a[],确定至少一个元素i 是否满足条件a[i] == i 的最有效方法是什么?

数组中的所有元素都已排序且不同,但它们不一定是整数类型(即它们可能是浮点类型)。

【问题讨论】:

  • “计算a[i] = i”是什么意思?确定是否所有i都为真?
  • 找出是否存在 a[i] = i 的元素。
  • 给定一个数组,并且您需要知道是否有任何元素的值与其索引匹配,数组已排序这一事实无济于事,我认为该信息是作为一个轻微的红鲱鱼给出的.您可以做的最好的事情是线性算法,检查每个索引处的数组以查看索引是否与存储的值匹配。
  • @Chad:如果数组是整数,那么对其进行排序会有一点好处。如果 a[i]>i 你可以马上知道没有任何 j>i 匹配。您可以分而治之,尝试拒绝尽可能多的元素。但是,OP 说 a[] 可以是非整数,所以它没有帮助。
  • @GuySirton 对于整数值,“轻微的好处”是轻描淡写的。 O(n) 和 O(log n) 之间的差异非常显着。

标签: c++ c algorithm search


【解决方案1】:

一些人声称“排序”、“不同”和“不一定是整数”的相关性。事实上,正确选择有效的算法来解决这个问题取决于这些特征。如果我们可以知道数组中的值既是不同的又是整数的,那么更有效的算法将是可能的,而如果这些值可能是非不同的,无论它们是否是整数,则需要一种效率较低的算法。当然,如果数组还没有排序,你可以先排序(平均复杂度为 O(n log n)),然后使用更有效的预排序算法(即排序数组),但在未排序的在这种情况下,简单地将数组保持未排序并直接比较线性时间(O(n))中的值会更有效。请注意,无论选择哪种算法,最佳情况下的性能都是 O(1)(当检查的第一个元素包含其索引值时);在执行任何算法期间的任何时候,我们都可能遇到a[i] == i 的元素,此时我们返回true;在这个问题中,就算法性能而言,真正重要的是我们可以多快排除所有元素并声明没有这样的元素 a[i] where a[i] == i

问题没有说明a[] 的排序顺序,这是一个非常关键的缺失信息。如果它是递增的,最坏情况的复杂度总是 O(n),我们无法做任何事情来使最坏情况的复杂度更好。但是如果排序顺序是降序的,即使是最坏情况的复杂度也是 O(log n):因为数组中的值是不同的并且是降序的,所以只有一个可能的索引 a[i] 可以等于 i,并且基本上所有你要做的是二分查找找到交叉点(升序索引值与降序元素值交叉的地方,如果甚至有这样的交叉),并确定a[c] == c是否在交叉点索引值c .由于这很简单,我将继续假设排序顺序是升序的。有趣的是,如果元素是整数,即使在升序情况下也会出现类似的“类似交叉”的情况(尽管在升序情况下可能有多个 a[i] == i 匹配),所以如果元素是整数,则二分查找也适用于升序情况,在这种情况下,即使是最坏情况的性能也是 O(log n)(请参阅Interview question - Search in sorted array X for index i such that X[i] = i)。但在这个版本的问题中,我们并没有那么奢侈。

我们可以这样解决这个问题:

从第一个元素 a[0] 开始。如果它的值为== 0,你已经找到了一个满足a[i] == i的元素,所以返回true。如果其值为< 1,则下一个元素(a[1])可能包含值1,因此您继续下一个索引。但是,如果a[0] >= 1,您知道(因为值不同)条件a[1] == 1 不可能为真,因此您可以安全地跳过索引1。但是你甚至可以做得更好:例如,如果a[0] == 12,你知道(因为值是按升序排序的)在元素a[13] 之前不可能有任何元素满足a[i] == i。因为数组中的值可以是非整数的,所以我们现在不能做任何进一步的假设,所以我们可以安全地直接跳到的下一个元素是a[13](例如a[1]a[12]可能都包含介于12.000...13.000... 这样a[13] 仍然可以完全等于 13,所以我们必须检查它)。

继续该过程会产生如下算法:

// Algorithm 1
bool algorithm1(double* a, size_t len)
{
    for (size_t i=0; i<len; ++i) // worst case is O(n)
    {
        if (a[i] == i)
            return true; // of course we could also return i here (as an int)...
        if (a[i] > i)
            i = static_cast<size_t>(std::floor(a[i]));
    }
    return false; // ......in which case we’d want to return -1 here (an int)
}

如果a[] 中的许多值都大于它们的索引值,这有很好的性能,如果a[] 中的所有值都大于n,则性能很好(仅在一次迭代后返回false!),但是如果所有值都小于它们的索引值(它会在 n 次迭代后返回 false),那么它的性能就会很差。所以我们回到绘图板......但我们需要的只是轻微的调整。考虑到该算法可以被编写为从 n 向下扫描到 0,就像它可以从 0 向前扫描到 n 一样容易。如果我们结合从两端向中间迭代的逻辑,我们得到一个算法如下:

// Algorithm 2
bool algorithm2(double* a, size_t len)
{
    for (size_t i=0, j=len-1; i<j; ++i,--j) // worst case is still O(n)
    {
        if (a[i]==i || a[j]==j)
            return true;
        if (a[i] > i)
            i = static_cast<size_t>(std::floor(a[i]));
        if (a[j] < j)
            j = static_cast<size_t>(std::ceil(a[j]));
    }
    return false;
}

这在两种极端情况下都具有出色的性能(所有值都小于 0 或大于 n),并且在几乎任何其他值分布情况下都具有非常好的性能。最坏的情况是如果数组下半部分的所有值都小于它们的索引,并且上半部分的所有值都大于它们的索引,在这种情况下性能会下降到 O( n)。最佳情况(极端情况)是 O(1),而平均情况可能是 O(log n),但我要请数学专业的人来确定。

一些人提出了一种“分而治之”的方法来解决问题,但没有具体说明如何划分问题以及如何处理递归划分的子问题。当然,这样一个不完整的答案可能不会让面试官满意。上面算法 2 的朴素线性算法和最坏情况下的性能都是 O(n),而算法 2 通过尽可能跳过(不检查)元素将平均情况下的性能提高到(可能)O(log n)。分而治之的方法只能胜过算法 2,如果在平均情况下,它能够以某种方式跳过比算法 2 可以跳过的更多的元素。假设我们通过递归地将数组分成两个(几乎)相等的连续两半来划分问题,并确定对于产生的子问题,我们是否能够跳过比算法 2 可以跳过的元素更多的元素,尤其是在算法 2 的最坏情况。对于本讨论的其余部分,让我们假设算法 2 的输入是最坏情况。在第一次拆分之后,我们可以检查两半的顶部和底部元素是否存在导致 O(1) 性能的相同极端情况算法2,但结果是两半相结合的 O(n) 性能。如果下半部分的所有元素都小于 0 并且上半部分的所有元素都大于 n-1,就会出现这种情况。在这些情况下,对于我们可以排除的任何一半,我们可以立即以 O(1) 的性能排除下半部分和/或上半部分。当然,该测试不能排除的任何一半的性能仍有待进一步递归后确定,再次将该一半除以一半,直到我们找到顶部或底部元素包含其索引值的任何段。与算法 2 相比,这是一个相当不错的性能改进,但它仅发生在算法 2 最坏情况的某些特殊情况下。我们对分而治之所做的只是减少(略微)引起最坏情况行为的问题空间的比例。分而治之仍然存在最坏情况,它们与引发算法 2 最坏情况行为的大部分问题空间完全匹配。

那么,鉴于分而治之算法的最坏情况较少,继续使用分而治之的方法是否有意义?

总之,没有。也许会。如果您事先知道大约一半的数据小于 0,一半大于 n,那么这种特殊情况通常会采用分而治之的方法更好。或者,如果您的系统是多核的并且您的“n”很大,那么在所有核心之间平均分配问题可能会有所帮助,但是一旦在它们之间分配,我认为每个核心上的子问题可能是最好的用上面的算法 2 解决,避免了问题的进一步划分,当然也避免了递归,正如我在下面所说的......

在递归分治法的每个递归级别,算法需要某种方法来记住问题的尚未解决的第二半部分,同时它会递归到第一半部分。这通常是通过让算法首先递归地调用自己的一半然后再调用另一半来完成的,这种设计在运行时堆栈上隐式地维护此信息。另一种实现可能通过在显式堆栈上维护基本相同的信息来避免递归函数调用。在空间增长方面,算法 2 是 O(1),但任何递归实现都不可避免地是 O(log n),因为必须在某种堆栈上维护此信息。但除了空间问题之外,递归实现还有额外的运行时开销,需要记住尚未递归进入的子问题一半的状态,直到它们可以递归进入。这种运行时开销不是免费的,鉴于上述算法 2 的简单实现,我认为这种开销是成比例的显着。因此,我建议上面的算法 2 将全面覆盖绝大多数情况下的任何递归实现。

【讨论】:

    【解决方案2】:

    在最坏的情况下,你不能比检查每个元素做得更好。 (想象一下 a[i] = i + uniform_random(-.25, .25) 之类的东西。)您需要一些关于您的输入内容的信息。

    【讨论】:

    • 不过,在某些情况下您可以改进这一点。如果你有一个 100 元素的数组,索引从 0 到 99,中间元素包含 100,那么由于数组已排序,你知道可以跳过数组的后半部分。但是,是的,最坏的情况是线性的。
    • @k53sc 是的,O(n) 因为它是数组的完整传递。
    【解决方案3】:

    其实我会从最后一个元素开始,做一个基本的检查(例如,如果你有 1000 个元素,但最高是 100,你知道你只需要检查 0..100)。在最坏的情况下,您仍然需要检查每个元素,但找到可能的区域应该更快。如果是上面所说的(a[i] = i + [-0.25..0.25]),你是 f($!ed 并且需要搜索每一个元素。

    【讨论】:

      【解决方案4】:

      对于已排序的数组,您可以执行插值搜索。类似于binary search,但假设值分布均匀,可能会更快。

      【讨论】:

      • 我认为这不适用于所述非整数值的情况。选择中点后(无论是对半、插值还是掷骰子),您仍然不知道仅根据中点值丢弃哪个段。
      【解决方案5】:

      我认为这里的主要问题是您的陈述相互矛盾:

      a[i] == i

      数组中的所有元素都是排序的和不同的,它们不一定是整数

      如果数组的值等于它的访问下标,这意味着它是一个整数。如果它不是一个整数,并且他们说..char,什么被认为是“排序的”? ASCII 值(A &lt; B &lt; C)?

      如果它是一个字符数组,我们会考虑:

      a[i] == i
      

      如果是真的

      i == 6510 && a[i] == 'A'

      如果我参加这次面试,我会在回答之前向面试官询问后续问题。那就是……

      如果我们只知道你所说的,我们可以肯定地说我们可以在 O(n) 中找到值,因为这是对数组进行一次完整传递的时间。有了更多细节,我们可以通过对数组的二进制搜索将其限制为 O(log(n))。

      【讨论】:

        【解决方案6】:

        注意到数组中的所有元素都是有序且不同的,所以如果我们用b[i]=a[i]-i构造一个新数组b,数组中的元素b 也是排序的,我们需要找到的是在数组 b 中找到零。我认为二分查找可以解决问题!这是一个link,用于计算排序数组中出现的次数。您也可以在原始数组上执行类似的分治技术,而无需构造辅助数组!时间复杂度是O(Logn)!

        Take this as an example:
        a=[0,1,2,4,8]
        b=[0,0,0,1,4]
        What we need to find is exactly index 0,1,2
        

        希望对你有帮助!

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-07-24
          • 1970-01-01
          • 1970-01-01
          • 2012-10-20
          • 2016-01-08
          • 1970-01-01
          • 2012-04-16
          • 2020-08-16
          相关资源
          最近更新 更多