如何在O(n)时间内找到在SORTED数组中出现奇数次数的数字?

时间:2010-07-05 16:50:01

标签: c algorithm asymptotic-complexity

我有一个问题,我试图一次又一次地考虑它......但是没有得到任何东西,所以在这里发布问题。也许我可以得到其他人的观点,试着让它发挥作用......

问题是:我们得到一个SORTED数组,它由一组偶数发生的值组成,除了一个,它发生了ODD次数。我们需要在log n时间找到解决方案。

在O(n)时间内很容易找到解决方案,但在log n时间执行它看起来相当棘手。

15 个答案:

答案 0 :(得分:29)

答案 1 :(得分:15)

排序数组表示二进制搜索。我们必须重新定义平等和比较。平等简单意味着奇数个元素。我们可以通过观察组的第一个或最后一个元素的索引来进行比较。第一个元素是奇数组之前的偶数索引(从0开始),奇数组之后是奇数索引。我们可以使用二进制搜索找到组的第一个和最后一个元素。总费用为O((log N)²)。

证明O((log N)²)

  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)²)

答案 2 :(得分:10)

查看数组的中间元素。通过几个适当的二进制搜索,您可以在数组中找到第一个和最后一个外观。例如,如果中间元素是'a',则需要找到ij,如下所示:

[* * * * a a a a * * *]
         ^     ^ 
         |     |
         |     |
         i     j

j - i是偶数吗?你完成了!否则(这是关键),问的问题是我是偶数还是奇数?你看到这条知识意味着什么吗?其余的很容易。

答案 3 :(得分:6)

这个答案是为了支持“throwawayacct”发布的答案。他值得赏金。我花了一些时间在这个问题上,我完全相信他的证明是正确的,你需要Ω(log(n)^ 2)个查询来查找出现奇数次的数字。我确信,因为我只是在略过他的解决方案之后重新创建完全相同的论点。

在解决方案中,攻击者会创建一个输入以使算法变得艰难,但对于人类分析器来说也很简单。输入由k个页面组成,每个页面都有k个条目。条目总数是n = k ^ 2,重要的是O(log(k))= O(log(n))和Ω(log(k))=Ω(log(n))。为了进行输入,对手制作一个长度为k的形式为00 ... 011 ... 1的字符串,其中过渡处于任意位置。然后将字符串中的每个符号扩展为aa ... abb ... b形式的长度为k的页面,其中在第i页上,a = i且b = i + 1。每个页面上的转换也处于任意位置,除了奇偶校验与页面扩展的符号一致。

理解分析算法最坏情况的“对手方法”非常重要。攻击者回答关于算法输入的查询,而不承诺将来的答案。答案必须保持一致,当对手被固定下来足以让算法得出结论时,游戏就结束了。

有了这样的背景,这里有一些观察:

1)如果您想通过在该页面中进行查询来学习页面中转换的奇偶校验,则必须了解转换的确切位置,并且需要Ω(log(k))查询。任何查询集合都将转换点限制为一个间隔,任何长度大于1的间隔都具有两个奇偶校验。在该页面中最有效地搜索转换是二进制搜索。

2)最微妙和最重要的一点:有两种方法可以确定特定页面内转换的奇偶校验。您可以在该页面中进行足够的查询以查找转换,或者如果在较早和较晚的页面中找到相同的奇偶校验,则可以推断奇偶校验。无论如何都没有逃脱。任何查询集都会将每个页面中的转换点限制为某个时间间隔。对奇偶校验的唯一限制来自长度为1的区间。否则,转换点可以自由摆动以获得任何一致的奇偶校验。

3)在对手方法中,没有幸运的罢工。例如,假设您在某个页面中的第一个查询是朝向一端而不是中间。由于对手没有答应,他可以自由地将过渡放在长边。

4)最终结果是你被迫直接探测Ω(log(k))页面中的奇偶校验,并且每个子问题的工作也是Ω(log(k))。

5)随机选择的事情并不比对抗选择好得多。数学更复杂,因为现在你可以得到部分统计信息,而不是严格的是你知道一个奇偶校验或不知道它。但它没什么区别。例如,您可以给每个页面长度k ^ 2,因此很有可能,每个页面中的第一个log(k)查询几乎不会告诉您该页面中的奇偶校验。对手可以在开始时做出随机选择,但仍然有效。

答案 4 :(得分:5)

从数组的中间开始向后走,直到得到一个与中心值不同的值。检查该边界上方的数字是否为奇数或偶数索引。如果它是奇数,那么出现奇数次的数字是在左边,所以在你找到的开头和边界之间重复搜索。如果它是偶数,那么出现奇数次的数字必须在数组中稍后,所以在右半部分重复搜索。

如上所述,它具有对数和线性分量。如果你想让整个事物保持对数,而不是只是向后穿过数组到另一个值,你想要使用二进制搜索。除非你期望许多重复相同的数字,否则二进制搜索可能不值得。

答案 5 :(得分:3)

我有一个在log(N / C)* log(K)中工作的算法,其中K是最大相同值范围的长度,C是要搜索的范围的长度。

此算法与之前发布的大多数差异的主要区别在于它利用了所有相同值范围都很短的情况。它不是通过二进制搜索整个数组来找到边界,而是首先通过跳回1,2,4,8,...(log(K)迭代)步骤快速找到粗略估计,然后二进制搜索结果范围(再次记录(K))。

算法如下(用C#编写):

// 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);
}

答案 6 :(得分:2)

取中间元素e。使用二进制搜索查找第一个和最后一个匹配项。 O(日志(N)) 如果是奇数返回e。 否则,递归到具有奇数个元素[...] eeee [....]

的一侧

运行时将是log(n)+ log(n / 2)+ log(n / 4).... = O(log(n)^ 2)。

答案 7 :(得分:1)

AHHH。有一个答案。

进行二进制搜索,并在搜索时为每个值向后移动,直到找到具有相同值的第一个条目。如果它的指数是偶数,则它在奇数球之前,所以向右移动 如果它的数组索引是奇数,则它在奇数球之后,所以向左移动。

在伪代码中(这是一般的想法,未经测试......):

    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;
    }

答案 8 :(得分:1)

您可以使用此算法:

int GetSpecialOne(int[] array, int length)
{
   int specialOne = array[0];

   for(int i=1; i < length; i++)
   {
      specialOne ^= array[i];
   }
   return specialOne;
}

借助于here

上可以找到的http://www.technicalinterviewquestions.net类似问题解决

答案 9 :(得分:0)

我们没有关于数组内部长度和整个数组的长度分布的信息,对吧?

因此arraylength可能是1,11,101,1001或其他东西,1至少没有上限,并且必须包含至少1种类型的元素('number'),最多为(length-1)/ 2 + 1个元素,总大小为1,11,101:1,1至6,1至51个元素,依此类推。

我们应该假设每个可能的大小相等吗?这将导致中等长度的子阵列大小/ 4,不是吗?

大小为5的数组可以分为1,2或3个子列表。

如果我们详细说明,那么显而易见的并不是那么明显。

大小为5的数组只能以一种方式“划分”为一个子列表,可以称之为“划分”。它只是一个包含5个元素的列表(aaaaa)。为了避免混淆,我们假设列表中的元素是有序字符,而不是数字(a,b,c,...)。

分为两个子列表,它们可能是(1,4),(2,3),(3,2),(4,1)。 (abbbb,aabbb,aaabb,aaaab)。

现在让我们回顾一下之前提出的声明:“划分”(5)是否应该与4个划分成2个子列表的概率相同?或者我们应该将它们混合在一起,并假设每个分区都是可能的,(1/5)?

或者我们可以在不知道子列表长度的概率的情况下计算解决方案吗?

答案 10 :(得分:0)

线索是你正在寻找log(n)。那不到n。

一次一个地走过整个阵列?那是。那不行。

我们知道数组中的前两个索引(0和1)应该是相同的数字。与50和51相同,如果数组中的奇数是之后的

因此找到数组中的中间元素,将其与之后的元素进行比较。如果数字的变化发生在错误的索引上,我们就知道数组中的奇数在它之前;否则,它就在之后。通过一组比较,我们可以确定目标所在阵列的哪一半。

继续前进。

答案 11 :(得分:0)

使用哈希表

For each element E in the input set
    if E is set in the hash table
         increment it's value
    else        
         set E in the hash table and initialize it to 0

For each key K in hash table
    if K % 2 = 1
        return K

由于此算法为2n,因此属于O(n)

答案 12 :(得分:0)

试试这个:

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因此每对都应该取消而将奇数输出。

答案 13 :(得分:-2)

您可以创建一个累积数组并计算每个数字的出现次数,然后在累积数组中找到奇数的元素。 例如:

int a[]=new int[]{2,3,4,2,3,1,4,5,6,5,6,7,1};
int b[]=new int[1000];
for (int i=0;i<b.length;i++) {
    b[i]=0;
}
for (Int i=0;i<a.length;i++) {
    b[a[i]]++;
}
for (int i=0;i<b.length;i++) {
    if ( b[i]!=0) {
        if (b[i] %2==0) {
          system.out.println(i);  break;

    }
}

答案 14 :(得分:-3)

假设索引从0开始。二进制搜索最小的偶数i,使得x [i]!= x [i + 1];你的答案是x [i]。

编辑:由于公众需求,这里是代码

int f(int *x, int min, int max) {
  int size = max;
  min /= 2;
  max /= 2;
  while (min < max) {
    int i = (min + max)/2;
    if (i==0 || x[2*i-1] == x[2*i])
      min = i+1;
    else
      max = i-1;
  }
  if (2*max == size || x[2*max] != x[2*max+1])
    return x[2*max];
  return x[2*min];
}