我一直在关注以下问题:
魔术索引:数组
A[0...n-1]
中的魔术索引被定义为索引i
,例如A[i] = i
。给定一个排序的非不同整数数组,编写一个方法来查找一个魔术索引(如果存在)。
这是我的解决方案:
static int magicNonDistinct(int[] array, int start, int end) {
if (end < start) return -1;
int mid = start + (end - start) / 2;
if (mid < 0 || mid >= array.length) return -1;
int v = array[mid];
if (v == mid) return mid;
int leftEnd = Math.min(v, mid - 1);
int leftRes = magicNonDistinct(array, start, leftEnd);
if (leftRes != -1) return leftRes;
int rightStart = Math.max(v, mid + 1);
int rightRes = magicNonDistinct(array, rightStart, end);
return rightRes;
}
它工作得很好,是Cracking The Code Interview第6版,问题8.3跟进(对不起破坏)一书的推荐解决方案。
然而,当在没有魔术索引的不同数组上运行它时,它会访问所有元素,从而产生最坏的O(n)运行时间。
由于它是递归的,因此最坏情况下需要O(n)内存。
为什么这个解决方案比迭代数组更可取?我认为这个解决方案(我自己的)更好:
static int magicNonDistinctV2(int[] array) {
for (int i = 0; i < array.length; ++i) {
int v = array[i];
if (v == i) return v;
if (v >= array.length) return -1;
else if (v > i) i = v - 1;
}
return -1;
}
O(n)运行时间O(1)空间总是?
有人可以为初始算法获得更好的时间复杂度吗?我一直在考虑是否是O(d),其中d是不同元素的数量,但是这种情况也是错误的,因为最小值/最大值仅在一个方向上起作用(想想是否{{1}并且数组的下半部分都是五个。)
编辑:
好的人认为我看到香蕉并且只要看到二进制搜索的东西就会尖叫O(log(n))。很抱歉不清楚大家。
让我们谈谈我发布的第一篇文章中的代码(CTCI的解决方案):
如果我们有一个如下所示的数组:v = 5, mid = 4
,实际上是一个如下所示的数组:[-1, 0, 1, 2, 3, 4, 5, 6, 7, 8]
,其大小为[-1,...,n-2]
,我们知道没有可匹配的元素。但是 - 算法将访问所有元素,因为元素不是唯一的。我敢说,运行它,它不能像在常规二进制搜索中那样将搜索空间除以2。请告诉我我的推理有什么问题。
答案 0 :(得分:3)
不,在我看来第一个解决方案不是O(log n),因为其他答案状态,它确实是O(n)最坏的情况(在最坏的情况下它仍然需要经过所有元素,考虑等价数组如作者所述,转移了一个。)
它不是O(log n)的原因是因为它需要在中间的两边搜索(二进制搜索只检查中间的一边因此它是O(log n))。
如果你很幸运,它允许跳过项目,但是你的第二个迭代解决方案也会跳过项目,如果不需要查看它们(因为你知道在数组排序的范围内不能有魔法索引)所以在我的意见第二种解决方案更好(相同的复杂性+迭代,即更好的空间复杂性,没有相对昂贵的递归调用)。
编辑:但是当我再次考虑第一个解决方案时,另一方面允许在可能的情况下“向后跳”,迭代解决方案不允许 - 例如考虑像{-10,-9这样的数组,-8,-7,-6,-5} - 迭代解决方案需要检查所有元素,因为它从头开始并且值不允许向前跳过,而当从中间开始时,算法可以完全跳过检查上半场,然后是下半场的上半场等等。
答案 1 :(得分:2)
array[mid] > end
时(因为在这种情况下,神奇指数肯定不会出现在[mid]中,结束]元素)。array[start] > mid
。因此,这种类二进制方法似乎比线性迭代整个数组要好,但在最坏的情况下,你会命中O(n)。
PS:我假设数组按升序排序。
答案 2 :(得分:0)
您似乎误解了所需解决方案的时间复杂性。最糟糕的情况不是O(n)
,而是O(log(n))
。这是因为在每次传递期间,您下次只搜索数组的一半。
这是一个C++ example并检查对于11个元素的整个数组,它只需要3次检查。