采访问题-在已排序数组X中搜索使X [i] = i的索引

52

昨天在面试中,我被问到以下问题:

考虑一个Java或C++数组,例如X,其中元素已排序且没有两个元素相同。如何最好地找到一个索引i,使该索引处的元素也等于i。也就是说,X [i] = i

为了澄清,她还给了我一个示例:

Array X : -3 -1 0 3 5 7
index   :  0  1 2 3 4 5

Answer is 3 as X[3] = 3.

我能想到的最好方法是线性搜索。面试后,我对这个问题思考了很多,但没有找到更好的解决方案。我的论点是:具有所需属性的元素可以在数组的任何位置。因此,它也可能在数组的末尾,因此我们需要检查每个元素。

我只是想从社区这里确认一下我是正确的。请告诉我我是正确的 :)


5
类似于二分查找的算法应该能够给出更好的解决方案。 - Reddy
1
我认为亚马逊不会喜欢你公开他们的面试问题... - Peter Alexander
2
4个收藏?!5个赞?!你们(点赞者,收藏者)来自哪里?这是非常简单的问题。我不会雇用一个对二进制和插值搜索一无所知的求职者。 - Alexey Malistov
1
Peter:我已经编辑过了,把公司名称删掉了。Alexey:我知道二分查找,但不知道如何应用它。 - John
10
@Alexey:如果他们只是想知道候选人是否了解二分查找,那么他们会只问在已排序数组中寻找x[i]=3的位置。这个问题很有趣。如果候选人能立即回答,很可能他们以前见过这道题,但他们也可能很聪明,在一开始就发现了额外的技巧。如果候选人在30秒后才回答,说明他们发现了这个技巧。如果他们无法回答,说明他们没有发现。为了区分聪明和有知识的人,请用一个float类型的数组来询问,看看是否得到相同的(现在是错误的)答案;-) - Steve Jessop
显示剩余5条评论
10个回答

105

通过略微修改的二分查找算法,可以在O(logN)时间和O(1)空间内完成。

考虑一个新数组Y,使得 Y[i] = X[i] - i

Array X : -3 -1   0  3  5  7
index   :  0  1   2  3  4  5
Array Y : -3 -2  -2  0  1  2

由于X中的元素是按 递增排序的,所以新数组Y中的元素将是非降序的。因此,在Y中进行0二分查找将得到答案。

但创建Y将需要O(N)的空间和O(N)的时间。因此,您可以修改二分查找算法,使Y[i]的引用替换为X[i] - i

算法:

function (array X) 
       low  = 0
       high = (num of elements in X) - 1

       while(low <= high) 
               mid = (low + high) / 2

               // change X[mid] to X[mid] - mid
               if(X[mid] - mid == 0)
                       return mid

               // change here too
               else if(X[mid] - mid < 0)
                       low = mid + 1;

               else
                       high = mid - 1;
       end while

       return -1 // no such index exists...return an invalid index.

end function

Java 实现

C++ 实现


1
谢谢你的解释。我知道二分查找,但从未想过要这样应用它。 - John
1
我不明白为什么结果数组Y是非递减的。假设X是[3,3,3,3],那么Y应该是[3,2,1,0]。显然是递减的。我觉得我在这里漏掉了什么。有人能给我解释一下吗? - Srikanth
2
@Srikanth:这个问题有一个要求,“没有两个数组元素是相同的”。 - codaddict
1
除了一件事情外,一切都很好:您并没有真正证明数组Y是非递减的(这对于给定条件是正确的),因此您没有突出显示使其非递减的条件。如果您使用浮点数而不是整数会发生什么变化?在您的解释中,除了算法错误的浮点数之外,没有任何变化,真正的算法是O(n)。 - Serge Dundich
+1 很好的回答。值得注意的是,Java有内置的Arrays.binarySearch()和Collections.binarySearch()方法,因此您不必自己实现(至少在Java 7中)。知道这一点可以在面试中节省很多时间。在这种情况下可能没有帮助,但我想知道是否可以定义一个具有equals方法的关键对象,例如:equals(Integer that) { return that - X.indexOf(that) == 0; } 或者一个compareTo方法,例如 compareTo(Integer other) { return other - axList.indexOf(other); } 我刚试了一下,由于类型安全原因,Java不允许我这样做。 - GlenPeterson
显示剩余5条评论

9
不需要像@codaddict在答案中建议的那样考虑任何数组Y。
使用二分查找并检查给定数组的中间元素,如果它小于它的索引,那么我们不需要检查任何较低的索引,因为数组是排序的,所以如果我们向左移动,减去m个索引和(至少)m个值,所有后续元素也将太小。例如,如果arr[5] = 4,则arr[4] <= (4 - 1)arr[3] <= (4 - 2)等等。如果中间元素大于其索引,则可以应用类似的逻辑。
这是一个简单的Java实现:
int function(int[] arr) {
        int low = 0;
        int high = arr.length - 1;

        while(low <= high) {
            int mid = high - (high - low) / 2;

            if(arr[mid] == mid) {
                 return mid;
            } else if(arr[mid] < mid) {
                 low = mid + 1;
            } else {
                 high = mid - 1;
            }
        }

        return -1; // There is no such index
}

请注意,上述解决方案仅适用于所有元素都不同的情况。

如果 arr[5] = 3,那么解决方案可能仍然在 arr[4] 中或者在 arr[6] 中。我认为我们无法在上述代码中选择一个方向。 - dchhetri
1
@user814628,在这种情况下,由于arr已经排序,因此arr[4]必须小于3,因此我们必须从索引6开始寻找解决方案。 - Vasu

9
有一些更快的解决方案,平均时间复杂度为O(log n)或在某些情况下为O(log log n),而不是O(n)。搜索"二分查找""插值查找",你可能会找到非常好的解释。
如果数组未排序,则元素可以出现在任何位置,您无法将时间复杂度降低到O(n),但对于已排序的数组则不是这种情况。
如所请求的,以下是关于插值查找的一些说明:
虽然二分查找只涉及“大于/不大于”的两个元素的比较,但插值查找也试图利用数值。重点是:您有一个从0到20000的排序值范围。您要查找300-二分查找将从范围的中间开始,在10000处。插值搜索猜测300可能更接近0而不是20000,因此它首先检查元素6000而不是10000。然后再次-如果太高,请递归到较低的子范围,如果太低,请递归到较高的子范围。
对于具有+-均匀值分布的大型数组,插值搜索应该比二分搜索更快-编写代码并自行验证。 而且,最好首先使用一个插值搜索步骤,然后使用一个二分搜索步骤,依此类推。 请注意,这是人在字典中查找内容时直觉上会做的事情。

1
+1 对于“插值搜索”参考。它也在D. Knuth的书中有描述。 - Alexey Malistov
请问您能否解释一下如何在这个问题中应用插值搜索? - John
4
-1,你没有解释如何将插值搜索应用于这个问题。非常关键的一步是,对于整数而言,如果 x[i] 是严格递增的,那么 x[i]-i 就是非递减的。 - Steve Jessop
3
@Kos :问题不是“如何找到一个索引 i 使得 x[i] == 300”,而是“如何找到一个索引 i 使得 x[i] == i”。如果没有考虑到这一点,我不认为这个答案是正确的(尽管它肯定是对插值搜索的良好描述)。 - Steve Jessop
1
Steve,你说得对:)直到现在我都没有注意到这个问题是什么;显然我需要放弃我的编程技能,转而专注于阅读技能。 John,对于插值搜索,只需参考Codaddict的帖子,并将“int mid = ...”这一行替换为适用于插值搜索的适当行。对于混淆,我很抱歉:)。 - Kos
显示剩余2条评论

7
我认为这种方法会更快。
从列表的中间开始。
如果X[i] > i,则转到剩余左侧的中间。
如果X[i] < i,则转到剩余右侧的中间。
一直这样做,每次循环都会将可能的元素数量减少一半。

3
您可以执行二分搜索: 搜索中间值,如果该值小于索引,则没有较低的索引将包含相同的值。 然后搜索较高的一半,并继续搜索,直到找到元素或达到一个元素跨度。

1
您可以执行二进制搜索 - 您的关键是什么? - codaddict
单元格的值为“value”,索引为“key”。 - Mor Shemesh
(假设数组长度已知)将以下与编程相关的内容从英语翻译成中文。仅返回翻译后的文本: - Mor Shemesh
2
-1,该单元格的值不是二分搜索中要使用的值。 - Steve Jessop
1
+1:我认为你没有像其他人(例如JOTN)那样清楚地解释,但似乎你是第一个回答的。 - Tony Delroy
显示剩余2条评论

1
这是我想出的一个解决方案,如果有重复项它也能工作(我错误地忽略了无重复项的限制)。
//invariant: startIndex <= i <= endIndex

int modifiedBsearch(int startIndex, int endIndex)
{
   int sameValueIndex = -1;
   int middleIndex = (startIndex + endIndex) /2;
   int middleValue = array[middleIndex];
   int endValue = array[endIndex];
   int startValue = array[startIndex];

   if(middleIndex == middleValue)
      return middleValue;
   else {
      if(middleValue <= endIndex)
         sameValueIndex = modifiedBsearch(middleIndex + 1, endIndex)

      if(sameValueIndex == -1 && startValue <= middleIndex)
         sameValueIndex = modifiedBsearch(startIndex, middleIndex -1);
   }

   return sameValueIndex;

}

我猜测这需要 O(log n) 的时间,但一开始不太清楚????
如果你运气不好,它将花费 O(n log n) 的时间(栈树的高度将为 log n ,并且它将是一棵完整的树,在最后一层有n个节点,在倒数第二层有n/2 个节点,以此类推)。
因此,平均而言,它会在 O(log n) 和 O(n log n) 之间。

它不可能是O(n log n),因为你最多只访问每个元素一次。 - Konstantin Milyutin

0

我想修改二分查找的版本就可以了。

假设这个序列是

Array : -1 1 4 5 6
Index :  0 1 2 3 4

Result : 1

或者

Array : -2 0 1 2 4 6 10
Index :  0 1 2 3 4 5 6 

Result: 4

从这两个例子中我们可以看出,如果mid < a[mid],那么所需的结果永远不会在右侧... 伪代码大致如下

mid <- (first + last )/2

if a[mid] == mid then
       return mid

else if a[mid] < mid then
        recursive call (a,mid+1,last)

else 
         recursive call (a,first,mid-1)

0
阅读问题后,似乎有一种情况可以用来加速查找。当将位置与值进行比较时,如果值大于位置,则该值可以用作下一个要评估的位置。这将有助于更快地跳过数组。这可以做到是因为数组已经排序。我们跳过的值在概念上向左移动了,并且位于错误的位置。
例如:
int ABC[] = { -2, -5, 4, 7, 11, 22, 55 };

如果我的当前位置是2,它的值为4,则它们不相等,概念上值4向左移动。我可以使用值4作为我的下一个位置,因为如果值4不在位置上,则小于4的所有值也不在位置上。
以下是一些示例代码,仅供讨论:
void main()
{
    int X[] = { -3, -1, 0, 3, 5, 7};
    int length = sizeof(X)/sizeof(X[0]);

    for (int i = 0; i < length;) {
        if (X[i] > i && X[i] < length)
            i = X[i];                 // Jump forward!
        else if (X[i] == i) {
            printf("found it %i", i);
            break;
        } else
            ++i;
    }
}

0

就我个人而言,使用二分法可能会更快。

查看中间值,如果它比所需值高,则在较低的一半重新搜索。

经过一次比较,您已经将数据集一分为二。


0

Java:

public static boolean check (int [] array, int i)
{
    if (i < 0 || i >= array.length)
        return false;

    return (array[i] == i);
}

C++:

bool check (int array[], int array_size, int i)
{
    if (i < 0 || i >= array_size)
        return false;

    return (array[i] == i);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接