我有一个问题,思考了很多次却没有想出答案...所以在这里发布这个问题。也许我能得到别人的观点,尝试让它起作用...
问题是:给定一个排序好的数组,其中包含一个出现奇数次数的值和其他出现偶数次数的值。我们需要在log n时间内找到解决方案。
在O(n)时间内找到解决方案很容易,但是在log n时间内执行看起来相当棘手。
我有一个问题,思考了很多次却没有想出答案...所以在这里发布这个问题。也许我能得到别人的观点,尝试让它起作用...
问题是:给定一个排序好的数组,其中包含一个出现奇数次数的值和其他出现偶数次数的值。我们需要在log n时间内找到解决方案。
在O(n)时间内找到解决方案很容易,但是在log n时间内执行看起来相当棘手。
Group boundaries
| | | | |
0 0 1 1 1 2 2 3 3
| | | |
Segment boundaries
无论何时增加了两个,我们都默认有一个双重边界。
Group boundaries
| || | |
0 0 0 2 2 2 2 3 3
声明: 第j个组的边界位置(1 ≤ j ≤ k)由xj这一线段唯一确定。
证明: 它就在((j - 1)k + bj)th个内存位置之后,并且xj唯一确定了bj。//Group boundaries
| X | | |
0 0 ? 1 2 2 3 3 3
| | | |
Segment boundaries
给定只有 0 0 ?,算法不能确定 ? 是 0 还是 1。但在上下文中,? 必须为 1,否则将会有三个奇数分组,并且可以推断出 X 处的分组边界。这些推断可能对敌手造成问题,但事实证明,只有在相关分组边界变得“无关紧要”时才能进行这些推断。
声明: 在算法执行过程中的任何时间点,考虑它观察到的分组边界集合。恰好有一个连续的偶数距离的分组边界对,并且奇数分组位于它们之间。
证明: 每个其他连续对仅限于偶数分组。//
定义由特殊连续对限定的奇数长度子序列为相关子序列。
声明: 在相关子序列内部的任何分组边界都不能被唯一确定。如果存在至少一个此类边界,则奇数分组的标识不是唯一确定的。
证明: 不失一般性地假设已经探测了不在相关子序列中的每个存储位置,并且包含在相关子序列中的每个分段恰好有一个尚未被探测的位置。假设第 j 个分组边界(称其为 B)位于相关子序列的内部。根据假设,对 xj 的探测将 B 的位置确定为两个相邻的可能性。我们将距离左侧观察到的边界奇数距离的那个称为奇数左侧,另一个称为奇数右侧。对于两种可能性,我们从左到右工作,并固定每个剩余内部分组边界的位置,以便其左侧的分组是偶数的。(我们之所以能够这样做,是因为它们都有两个相邻的可能性。)如果 B 在奇数左侧,则其左侧的分组是唯一的奇数分组。如果 B 在奇数右侧,则相关子序列中的最后一个分组是唯一的奇数分组。两者都是有效输入,因此算法既没有唯一地确定 B 的位置,也没有唯一确定奇数分组。//
示例:
Observed group boundaries; relevant subsequence marked by […]
[ ] |
0 0 Y 1 1 Z 2 3 3
| | | |
Segment boundaries
Possibility #1: Y=0, Z=2
Possibility #2: Y=1, Z=2
Possibility #3: Y=1, Z=1
由于这一声明的影响,算法无论如何运作都必须将相关子序列缩小到一个组。因此根据定义,它必须观察一些组边界。攻击者现在的任务是尽可能保持开放的可能性。在算法执行的任何时刻,攻击者在相关子序列之外的每个内存位置上内部承诺于一个可能性。在开始时,相关子序列是整个输入,因此没有初始承诺。每当算法探测到xj的未承诺位置时,攻击者必须承诺其中的一个值:j-1或j。如果它能够避免让第j个边界被观察到,它会选择留下至少一半的剩余可能性(相对于观察)。否则,它会选择保持相关区间中至少一半的组,并为其他组承诺价值。通过这种方式,攻击者迫使算法观察至少log2(k+1)个组边界,在观察第j个组边界时,算法被迫进行至少log2(k+1)次探测。
扩展:
这个结果可以直接推广到随机算法中,通过随机化输入,在从算法的角度来看,将“至少减半”替换为“期望上至少减半”,并应用标准的集中不等式。它还适用于没有组可以大于s个副本的情况,在这种情况下,下限是Ω(log n log s)。 T(2) = 1 //to make the summation nice
T(N) = log(N) + T(N/2) //log(N) is finding the first/last elements
对于一些N=2^k的情况,
T(2^k) = (log 2^k) + T(2^(k-1))
= (log 2^k) + (log 2^(k-1)) + T(2^(k-2))
= (log 2^k) + (log 2^(k-1)) + (log 2^(k-2)) + ... + (log 2^2) + 1
= k + (k-1) + (k-2) + ... + 1
= k(k+1)/2
= (k² + k)/2
= (log(N)² + log(N))/ 2
= O(log(N)²)
查看数组的中间元素。通过适当的二分搜索,您可以找到该元素在数组中第一次出现和最后一次出现的位置。例如,如果中间元素是'a',则您需要按照下面所示找到i
和j
:
[* * * * a a a a * * *]
^ ^
| |
| |
i j
j - i
是偶数吗?如果是,你就完成了!否则(这是关键),需要问的问题是i是偶数还是奇数? 你明白这个知识点意味着什么吗?然后剩下的就很容易了。
5) 随机选择并不比对手选择更好。数学上更加复杂,因为现在您可以获得部分统计信息,而不是严格的是或否。但这几乎没有任何区别。例如,您可以给每个页面长度k ^ 2,以便高概率下,每个页面中的前log(k)个查询几乎不会告诉您有关该页面奇偶性的信息。对手可以在开始时进行随机选择,它仍然有效。
log n
次二分查找,这不是O((log n)^2)
吗? - IVlad(log n)+(log n/2)+(log n/4)+... = log(n^(log n)*2^(-(log n)((log n)-1)/2)) = (log n)²-(log n)((log n)-1)/2 = (log n)²/2+(log n)/2 = O((log n)²)
可以翻译为:对于给定的数值n,求(log n)+(log n/2)+(log n/4)+...
的和。答案为log(n^(log n)*2^(-(log n)((log n)-1)/2))
。此表达式可以进一步化简为(log n)²-(log n)((log n)-1)/2
。再将其转换为二次方程组(log n)²/2+(log n)/2
,最终得出结果为O((log n)²)
。在WolframAlpha中输入"sum log_2(2^k/2^i) for i=0..k-1"可得相同的结果。 - Nabb// Finds the start of the range of equal numbers containing the index "index",
// which is assumed to be inside the array
//
// Complexity is O(log(K)) with K being the length of range
static int findRangeStart (int[] arr, int index)
{
int candidate = index;
int value = arr[index];
int step = 1;
// find the boundary for binary search:
while(candidate>=0 && arr[candidate] == value)
{
candidate -= step;
step *= 2;
}
// binary search:
int a = Math.Max(0,candidate);
int b = candidate+step/2;
while(a+1!=b)
{
int c = (a+b)/2;
if(arr[c] == value)
b = c;
else
a = c;
}
return b;
}
// Finds the index after the only "odd" range of equal numbers in the array.
// The result should be in the range (start; end]
// The "end" is considered to always be the end of some equal number range.
static int search(int[] arr, int start, int end)
{
if(arr[start] == arr[end-1])
return end;
int middle = (start+end)/2;
int rangeStart = findRangeStart(arr,middle);
if((rangeStart & 1) == 0)
return search(arr, middle, end);
return search(arr, start, rangeStart);
}
// Finds the index after the only "odd" range of equal numbers in the array
static int search(int[] arr)
{
return search(arr, 0, arr.Length);
}
取中间元素e。使用二分查找找到第一个和最后一个出现的位置。O(log(n)) 如果是奇数,则返回e。 否则,递归到具有奇数个元素的一侧[....]eeee[....]
运行时间将为log(n) + log(n/2) + log(n/4).... = O(log(n)^2)。
您可以使用这个算法:
int GetSpecialOne(int[] array, int length)
{
int specialOne = array[0];
for(int i=1; i < length; i++)
{
specialOne ^= array[i];
}
return specialOne;
}
在http://www.technicalinterviewquestions.net上可以找到一个类似的问题这里,得以解决。
啊哈。有答案了。
进行二分查找,对于每个值,向后移动,直到找到具有相同值的第一个条目。如果其索引为偶数,则在奇怪的元素之前,因此向右移动。
如果其数组索引为奇数,则在奇怪的元素之后,因此向左移动。
伪代码(这是一般想法,未经测试...):
private static int FindOddBall(int[] ary)
{
int l = 0,
r = ary.Length - 1;
int n = (l+r)/2;
while (r > l+2)
{
n = (l + r) / 2;
while (ary[n] == ary[n-1])
n = FindBreakIndex(ary, l, n);
if (n % 2 == 0) // even index we are on or to the left of the oddball
l = n;
else // odd index we are to the right of the oddball
r = n-1;
}
return ary[l];
}
private static int FindBreakIndex(int[] ary, int l, int n)
{
var t = ary[n];
var r = n;
while(ary[n] != t || ary[n] == ary[n-1])
if(ary[n] == t)
{
r = n;
n = (l + r)/2;
}
else
{
l = n;
n = (l + r)/2;
}
return n;
}
n-1
个相等数字和另外一个数字组成的数组。您最内层的while循环是O(n)
,因为它将遍历n/2
个元素。 - IVladO(n)
。我没有看到任何编辑,但是如果我们正在考虑相同的事情,它仍然不会是O(log n)
而是O((log n)^2)
。 - IVladO((log n) ^2)
的说法是正确的。通常情况下,嵌套二进制迭代会给您提供O((log n) ^2)
,正如您所说,但我认为这种情况可能有所不同,因为在这里它们不是完全独立的嵌套迭代。内部和外部迭代都不必迭代每个元素,只需迭代不被另一迭代淘汰的那些元素即可...这肯定会减少所需的处理步骤数量,并可能缓解算法的“O-ness”(尽管我必须承认我不确定如何精确计算)。 - Charles Bretana试试这个:
int getOddOccurrence(int ar[], int ar_size)
{
int i;
int xor = 0;
for (i=0; i < ar_size; i++)
xor = xor ^ ar[i];
return res;
}
XOR 操作每次与相同的数字进行 XOR 运算时都会被取消,因此 1^1=0,但是 1^1^1=1,所以每对数字应该被取消,留下奇数。