【问题标题】:Linear Search Algorithm Optimization线性搜索算法优化
【发布时间】:2010-08-29 17:04:45
【问题描述】:

我刚刚完成了计算机科学 1 的作业问题(是的,这是作业,但请听我说!)。现在,该任务已 100% 完成并且正在运行,所以我不需要帮助。我的问题涉及我正在使用的算法的效率(我们还没有对算法效率进行评分,我只是很好奇)。

我要介绍的函数目前使用线性搜索算法的修改版本(我自己想出了!),以检查给定彩票上有多少号码与中奖号码匹配,假设彩票上的号码和抽出的号码都是按升序排列的。我想知道,有什么方法可以让这个算法更高效?

/*
 * Function: ticketCheck
 *
 * @param struct ticket
 * @param array winningNums[6]
 *
 * Takes in a ticket, counts how many numbers
 * in the ticket match, and returns the number
 * of matches.
 *
 * Uses a modified linear search algorithm,
 * in which the index of the successor to the
 * last matched number is used as the index of
 * the first number tested for the next ticket value.
 *
 * @return int numMatches
 */
int ticketCheck( struct ticket ticket, int winningNums[6] )
{
    int numMatches = 0;
    int offset = 0;
    int i;
    int j;

    for( i = 0; i < 6; i++ )
    {
        for( j = 0 + offset; j < 6; j++ )
        {
            if( ticket.ticketNum[i] == winningNums[j] )
            {
                numMatches++;
                offset = j + 1;
                break;
            }
            if( ticket.ticketNum[i] < winningNums[j] )
            {
                i++;
                j--;
                continue;
            }
        }
    }

    return numMatches;
}

【问题讨论】:

  • 提高效率是指将搜索更改为其他类型的搜索,还是仍要使用线性搜索?
  • 这很令人惊讶。如果两个号码匹配(第一个 if),您将继续寻找相同的号码 i (break;)。您是否希望任何一个列表都有重复项?在我的州,彩票不是这样运作的。第二个if 也很难理解。在我看来,j-- 可以撤消由continue; 引起的j++,但坦率地说,我更喜欢看到干净的goto 而不是这种技巧。
  • 在内部循环中,当您看到大于 ticketnum[i] 的数字时,您可以停止。如您所知,像这样的线性搜索需要 O(nn) 时间,其中 n 是彩票号码的数量。因为 O(Cn*n) 对于任何常数 C 都是相同的,所以当你看到更大的数字时停止不会改变复杂性,但你会减小常数 C,从而使其运行得更快。如果你不介意我这么说,我也不喜欢这种风格。我不会更改循环内的循环索引。它不可读。
  • @Andrew:有一个特别的作业标签,我给你加了。除了在文本中提及作业之外,使用该标签是一种很好的礼貌。
  • @Albin 好的,谢谢!我不是故意忽略它,我只是不知道它存在。我想我试图在帖子正文中明确表明这是一项家庭作业,但如果需要,我将从现在开始使用该标签。

标签: c performance algorithm


【解决方案1】:

它或多或少在那里,但不完全是。在大多数情况下,它是 O(n),但如果每个 ticketNum 大于每个winningNum,它就是 O(n^2)。 (这是因为内部的j 循环在j==6 应该没有break,而是运行下一个i 迭代。)

您希望您的算法在每一步都增加ij,并在i==6j==6 时终止。 [您的算法几乎满足这一点,如上所述。]因此,您只需要一个循环:

for (i=0,j=0; i<6 && j<6; /* no increment step here */) {
    if (ticketNum[i] == winningNum[j]) {
        numMatches++;
        i++;
        j++;
    }
    else if (ticketNum[i] < winningNum[j]) {
        /* ticketNum[i] won't match any winningNum, discard it */
        i++;
    }
    else { /* ticketNum[i] > winningNum[j] */
        /* discard winningNum[j] similarly */
        j++;
    }
}

显然这是 O(n);在每个阶段,它要么递增i 要么增加j,所以它可以做的最多步骤是 2*n-1。这与您的算法具有几乎相同的行为,但更容易理解并且更容易看出它是正确的。

【讨论】:

  • 你的答案有更好的 cmets,所以我删除了我的。
  • 有一个关于两个程序员的轶事——我不记得是哪一个,但它可能是 Pike、Ritchie、Thomson 和 Kernighan 中的两个——独立地将相同的函数写入逗号。我一直认为这不是那么特别。
  • @Pascal:我非常同意。如果您正在考虑相同的算法,您将编写类似的代码。如果你在同一个团队,你会使用相同的风格。相同的算法 + 相同的风格 = 相同的代码。
  • +1 这表明对问题有清晰的理解,而不是专注于给定的解决方案。
  • 您可以删除i++;j++; 的重复,方法是从第一个if() 中删除它们,然后删除elses 并将另一个ifs 更改为@987654337 @ 和 &gt;=.
【解决方案2】:

您基本上是在寻找两组交集的大小。鉴于大多数乐透使用大约 50 个球(左右),您可以将数字存储为设置为 unsigned long long 的位。找到共同的数字只需将两者相加即可:commonNums = TicketNums &amp; winningNums;

找到交集的大小是计算结果数字中的一位,主题是covered previously(尽管在这种情况下,您将使用 64 位数字,或一对 32-位数,而不是单个 32 位数)。

【讨论】:

  • Jeffry,这很聪明,但我个人不喜欢这种编程风格。在我看来,如果需要数组,代码必须是可读的并对数组进行操作。我认为使用整数作为数组不是一种好的风格。但这只是我。不过,您的解决方案在内存有限且 CPU 速度较慢的嵌入式系统中会很棒。 :) 编辑:但您的回答实际上给出了一般方法。并行迭代这两个序列,并发出 1 表示匹配,发出 0 表示不匹配。我认为这就是 AND 在硬件中的实现方式。
  • @akonsu 这与我提出的算法基本相同,但使用的是位域而不是数组。如果集合很大,这是一种经过验证且广泛使用的技术(例如,Unix 上的select syscall)。
  • @Akonsu:名字没问题(就像老话一样,只是不要叫我吃饭迟到了)。实际上,在大多数硬件中,AND 将按位并行执行(即 CPU 将有 32 或 64 个(或任何字长)单独的 2 输入 AND 门,所有这些门都同时产生输出)。至于“需要数组时的数组”,我倾向于同意——但这是一种情况,不需要(甚至可能也不首选)(IMO)数组。
  • @caf:不完全平行,这是完全正确的。 OTOH,我很确定您可以并行执行其中的一部分,因此总共十个(左右)操作可以在大约四个时钟周期内发生,前三个时钟中的每个时钟并行进行约 3 次操作,然后是一个操作将它们全部组合到最后。
【解决方案3】:

是的,有些东西更快,但可能会使用更多内存。在可能的数字大小中制作一个全为 0 的数组,在每个抽取的数字上放置一个 1。对于每个票号,在该号码的索引处添加值。

 int NumsArray[MAX_NUMBER+1];
 memset(NumsArray, 0, sizeof NumsArray);

 for( i = 0; i < 6; i++ )
   NumsArray[winningNums[i]] = 1;

 for( i = 0; i < 6; i++ )
   numMatches += NumsArray[ticket.ticketNum[i]];

12 次循环,而不是最多 36 次 周围的代码留作练习。

编辑:它还具有不需要对两组值进行排序的优点。

【讨论】:

  • 这与我的解决方案速度相同(~2n 循环迭代),但使用更多内存。但是,在未排序的情况下它会更快,因为我的答案取决于已排序的数据,而您的则不然。
  • 这个方法也很容易扩展,通过使用 NumsArray[winningNums[i]] +=1 来正确处理输入中的重复数字。
  • 是的,如果您想根据中奖号码检查多张彩票,我的还有一个优势,只需要第一个循环一次。
【解决方案4】:

在这样的规模上,这实际上只是一个微小的变化,但如果第二个循环达到一个大于当前票号的数字,它就已经允许刹车了。此外,如果您的秒数遍历的数字低于您的票号,即使在该迭代中找不到匹配项,它也可能会更新偏移量。

PS: 不要忘记,如果我们将球的数量或票的大小视为可变的,那么关于效率的一般结果会更有意义。否则太依赖机器了。

【讨论】:

    【解决方案5】:

    如果不是比较彩票号码数组,而是创建两个标志位数组——如果它的索引在该数组中,则每个标志都被设置——那么你可以对两个位数组执行按位和(彩票和中奖号码集)并产生另一个位数组,其位是仅用于匹配数字的标志。然后数一下设置的位数。

    对于许多彩票来说,64 位就足够了,所以 uint64_t 应该足够大以涵盖这一点。此外,一些体系结构具有计算寄存器中设置的位的指令,一些编译器可能能够识别和优化。

    该算法的效率取决于彩票号码的范围(M)和每张彩票的彩票号码数量(N)。如果标志为 O(N),则设置为 O(N),而两个位数组的与运算和位计数可能为 O(M),具体取决于您的 M(乐透号码范围)是否大于目标 cpu 可以直接执行这些操作。不过,M 很可能很小,其对性能的影响可能小于 N。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-17
      • 2016-12-05
      • 1970-01-01
      • 2015-12-05
      • 2021-03-24
      • 1970-01-01
      • 1970-01-01
      • 2012-05-24
      相关资源
      最近更新 更多