使用二进制搜索查找缺失的数字

时间:2012-09-07 08:55:53

标签: c++ algorithm binary-search

我正在读珍珠编程的书。

  

问题:给定一个包含最多40亿的顺序文件   以随机顺序的32位整数,找到一个不在的32位整数   文件(并且必须至少有一个丢失)。这个问题必须要做   如果我们有几百字节的主存和几个字节就可以解决   顺序文件。

     

解决方案:要将其设置为二进制搜索,我们必须定义一个范围,   范围内元素的表示和探测   确定范围的哪一半保存缺失整数的方法。   我们怎么做?

     

我们将使用已知包含至少的整数序列作为范围   一个缺少的元素,我们将用文件表示范围   包含其中的所有整数。我们可以探索一个洞察力   通过计算其中点上方和下方的元素:范围:   上限或下限范围中总共有一半元素   范围。因为总范围缺少元素,所以较小的一半   还必须有一个mising元素。这些是a的大部分成分   针对上述问题的二分搜索算法。

上面的文字是Jon Bently从编程珍珠书中的版权。

以下链接提供了一些信息

"Programming Pearls" binary search help

我们如何使用二分搜索进行搜索,也没有按照上面链接中给出的示例进行搜索?请帮助我理解逻辑只有5个整数而不是百万个整数来理解逻辑。

5 个答案:

答案 0 :(得分:3)

为什么不重新阅读帖子"Programming Pearls" binary search help中的答案。它解释了你问的5个整数的过程 这个想法是你解析每个列表并将它分成2(这是二进制部分来自哪里)基于第一位中的值的单独列表。

即。显示实际数字的二进制表示 原始列表“”:001,010,110,000,100,011,101 => (分成)
(我们删除第一位并将其附加到新列表的“名称”) 为了形成每个波纹管列表,我们从上面的列表中选择以[0或1]开头的值 列出“ 0 ”:01,10,00,11(由列表“010的子集001,010,000,011形成”,删除第一位并将其附加到“名称”新名单)
列出“ 1 ”:10,00,01(由List“”的子集110,100,101形成,通过删除第一位并将其附加到新列表的“名称”)< / p>

现在轮流取一个结果列表并重复该过程:
列表“ 0 ”成为您的原始列表,您将其分解为
列出“0 *** 0 **”和
列出“0 *** 1 **”(粗体数字又是列表中数字的1 [剩余]位被破坏)

继续操作直到你最终得到空列表。

修改
一步一步处理:
列表“”:001,010,110,000,100,011,101 =&gt;
  列表“0”:01,10,00,11(来自列表“”的子集001,010,000,011)=&gt;
    列表“00”:1,0(来自列表“00”的子集01,00)=&gt;
      列出“000”:0 [最终结果](来自列表“00”的子集0)
      列出“001”:1 [最终结果](来自列表“00”的子集1)
    列表“01”:0,1(来自列表“0”的子集10,11)=&gt;
      列表“010”:0 [最终结果](来自列表“01”的子集0)
      列表“011”:1 [最终结果](来自列表“01”的子集1)
  列表“1”:10,00,01(来自列表“”的子集110,100,101)=&gt;
    列表“10”:0,1(来自列表“1”的子集00,01)=&gt;
      列出“100”:0 [最终结果](来自列表“10”的子集0)
      列表“101”:1 [最终结果](来自列表“10”的子集1)
    列表“11”:0(来自列表“1”的子集10)=&gt;
      列表“110”:0 [最终结果](来自列表“11”的子集0)
      列出“111”:缺席 [最终结果](来自列表“11”的子集 EMPTY

这种方法的积极之处在于,它允许您在集合中找到任意数量的缺失数字 - 即,如果缺少多个数字。

P.S。在完整范围之外的 1个缺失数字的AFAIR,XOR所有数字都有更优雅的解决方案。

答案 1 :(得分:1)

想法是解决更容易的问题:

范围内的缺失值是[minVal,X]还是(X,maxVal)。 如果你知道这一点,你可以移动X并再次检查。

例如,您有3,4,1,5(缺少2个)。 你知道minVal = 1,maxVal = 5.

  1. 范围= [1,5],X = 3,范围[1,3]中应该有3个整数,范围[4,5]中应该有2个整数。范围[1,3]中只有2个,所以你在范围[1,3]
  2. 中寻找
  3. 范围= [1,3],X = 2.范围[1,2]中只有1个值,因此您正在查看范围[1,2]
  4. 范围= [1,2],X = 1.范围[2,2]中没有值,所以这是你的答案。
  5. 编辑:一些伪C ++代码:

    minVal = 1, maxVal = 5; //choose correct values
    while(minVal < maxVal){
        int X = (minVal + maxVal) / 2
        int leftNumber = how much in range [minVal, X]
        int rightNumber = how much in range [X + 1, maxVal]
        if(leftNumber < (X - minVal + 1))maxVal = X
        else minVal = X + 1
    }
    

答案 2 :(得分:1)

这是一个简单的C解决方案,应该说明该技术。为了抽象出任何繁琐的文件I / O细节,我假设存在以下三个函数:

  • unsigned long next_number (void)从文件中读取一个数字并将其返回。再次调用时,将返回文件中的下一个数字,依此类推。遇到文件结尾时的行为未定义。

  • int numbers_left (void)如果有更多数字可供使用next_number()读取,则返回true值;如果已到达文件末尾,则返回false。

  • void return_to_start (void)将阅读位置倒回到文件的开头,以便下一次调用next_number()返回文件中的第一个数字。

我还假设unsigned long至少为32位宽,符合ANSI C实现的要求;现代C程序员可能更喜欢使用来自stdint.huint32_t

鉴于这些假设,这是解决方案:

unsigned long count_numbers_in_range (unsigned long min, unsigned long max) {
    unsigned long count = 0;

    return_to_start();

    while ( numbers_left() ) {
        unsigned long num = next_number();
        if ( num >= min && num <= max ) {
            count++;
        }
    }
    return count;
}

unsigned long find_missing_number (void) {
    unsigned long min = 0, max = 0xFFFFFFFF;

    while ( min < max ) {
        unsigned long midpoint = min + (max - min) / 2;
        unsigned long count = count_numbers_in_range( min, midpoint );

        if ( count < midpoint - min + 1 ) {
            max = midpoint;  // at least one missing number below midpoint
        } else {
            min = midpoint;  // no missing numbers below midpoint, must be above
        }
    }
    return min;
}

需要注意的一个细节是,min + (max - min) / 2是计算minmax平均值的安全方式。它不会因overflowing中间值而产生虚假结果,例如看似简单的(min + max) / 2

另外,尽管使用递归解决这个问题很诱人,但我选择了一个迭代解决方案,原因有两个:第一,因为它(可以说)更清楚地显示了实际做了什么,其次,因为任务是最小化内存使用,也可能包括堆栈。

最后,优化此代码很容易,例如一旦count等于零,通过在一次通过中计算两个范围内的数字并选择具有更多缺失数字的数字,或者甚至通过将二进制搜索扩展到 n - 搜索某些 n &gt; 2减少通过次数。但是,为了使示例代码尽可能简单,我没有留下这样的优化。如果你愿意,你可能想要尝试修改代码,这样它最多需要8次传递而不是当前的32次。(提示:使用16个元素的数组。)

答案 3 :(得分:0)

实际上,如果我们有从a到b的整数范围。样本:[a..b]。 在这个范围内,我们有b-a整数。这意味着,只有一个缺失。 如果只缺少一个,我们只用一个周期计算结果。 首先,我们可以计算范围[a..b]中所有整数的和,等于:

sum = (a + b) * (b - a + 1) / 2

然后我们计算序列中所有整数的总和:

long sum1 = 0;
for (int i = 0; i < b - a; i++)
sum1 += arr[i];

然后我们可以找到缺少的元素作为这两个总和的差异:

long result = sum1 - sum;

答案 4 :(得分:0)

当你在第i个数字位置看到2 ^ 31个零或一个,那么你的答案在第i个位置有一个或零。 (例如:在第5个二进制位置,2 ^ 31个意味着答案在第5个二进制位置为零。

c代码初稿:

uint32_t binaryHistogram[32], *list4BILLION, answer, placesChecked[32];
uint64_t limit = 4294967296;
uint32_t halfLimit = 4294967296/2;
int i, j, done

//General method to point to list since this detail is not important to the question.
list4BILLION = 0000000000h;


//Initialize array to zero. This array represents the number of 1s seen as you parse through the list
for(i=0;i<limit;i++)
{   
    binaryHistogram[i] = 0;
}

//Only sum up for first half of the 4 billion numbers
for(i=0;i<halfLimit;i++)
{
    for(j=0;j<32;j++)
    {
        binaryHistogram[j] += ((*list4BILLION) >> j);
    }
}

//Check each ith digit to see if all halfLimit values have been parsed
for(i=halfLimit;i<limit;i++)
{
    for(j=0;j<32;j++)
    {
        done = 1;   //Dont need to continue to the end if placesChecked are all 
        if(placesChecked[j] != 0) //Dont need to pass through the whole list
        {
            done = 0; //
            binaryHistogram[j] += ((*list4BILLION) >> j);
            if((binaryHistogram[j] > halfLimit)||(i - binaryHistogram[j] == halfLimit))
            {
                answer += (1 << j);
                placesChecked[j] = 1;
            }
        }
    }
}