虽然我们将常量放在大O符号中,但它在现实生活中是否重要?

时间:2016-02-04 23:08:00

标签: c++ c algorithm performance big-o

虽然我理解big O符号只是简单地描述算法的增长率,但我不确定以下O(n)算法之间在现实生活中的效率是否有任何差异。

从列表末尾打印链接列表中的节点值。

给定一个节点:

/* Link list node */
struct node
{
  int data;
  struct node* next;
};

解决方案1 ​​O(n)

此解决方案迭代列表两次,一次查找列表的长度,第二次遍历列表的结尾 - N

void printNthFromLast(struct node* head, int n)
{
    int len = 0, i;
    struct node *temp = head;


    // 1) Count the number of nodes in Linked List
    while (temp != NULL)
    {
        temp = temp->next;
        len++;
    }

    // Check if value of n is not more than length of the linked list
    if (len < n)
      return;

    temp = head;

    // 2) Get the (n-len+1)th node from the begining
    for (i = 1; i < len-n+1; i++)
    {
       temp = temp->next;
    }
    printf ("%d", temp->data);

    return;
}

解决方案2 O(n)

此解决方案仅迭代列表一次。 ref_ptr指针指向,并且后面跟着它的第二个指针(main_ptr)。当ref_ptr到达列表的末尾时,main_ptr应指向正确的节点(列表末尾的k个位置)。

void printNthFromLast(struct node *head, int n)
{
  struct node *main_ptr = head;
  struct node *ref_ptr = head;

  int count = 0;
  if(head != NULL)
  {
    while( count < n )
     {
        if(ref_ptr == NULL)
        {
           return;
        }
        ref_ptr = ref_ptr->next;
        count++;
     }

     while(ref_ptr != NULL)
     {
        main_ptr = main_ptr->next;
        ref_ptr  = ref_ptr->next;
     }
  }
}

问题是:尽管两个解决方案都是O(n)而将大O表示法放在一边,第二个解决方案是否比第一个解决方案更有效,因为它只迭代列表一次?

7 个答案:

答案 0 :(得分:20)

是。在发生相同工作的特定示例中,单个循环可能比循环两组数据更有效。但O(2n)O(n)的想法是2 ns vs 1 ns可能并不重要。 Big O可以更好地展示一段代码如何扩展,例如如果您制作了循环O(n^2),则O(n)O(2n)的差异远小于O(n)O(n^2)的差异。

如果您的链表包含数TB的数据,则可能值得减少到单循环迭代。在这种情况下,一个大的O指标可能不足以描述您的最坏情况;你最好不要对代码进行计时并考虑应用程序的需求。

另一个例子是嵌入式软件,其中1 ms vs 2 ms可能是500 Hz和1 kHz控制回路之间的差异。

吸取的教训是,这取决于应用程序。

答案 1 :(得分:6)

如果订单相同,则常量只对有效,并且操作的复杂程度相当。如果它们不是同一个订单,那么一旦您有足够大的n,那么具有更高订单的订单可以保证更长的时间。有时n必须大于典型数据集,选择最有效算法的唯一方法是对它们进行基准测试。

答案 2 :(得分:4)

我认为从我的观点来看,例如O(n)和O(n)这两个例程之间的区别并不是O符号的重点。例如,关键差异在O(n ^ 2)和O(n)之间。 [n ^ 2当然是n平方]

因此,一般来说,O(n ^ p)的幂p对于例程的效率如何随尺寸而变化至关重要。

因此,看看你所拥有的两个例程,它们之间的性能可能会有所不同,但对于第一个近似,它们的行为与数据集大小的增加相似。

缩放是关键的代码示例是Fourier Transform,其中一些方法给出O(n ^ 2)而其他方法给出O(n log n)。

答案 3 :(得分:4)

虽然在您的特定示例中,由于编译器优化,缓存,数据访问速率以及许多其他问题使问题变得复杂,因此过于接近,以回答您的标题问题&#34;虽然我们将常量放在大O表示法中在现实生活中是否重要&#34;很容易:

想象一下,我们有一个非常耗时的函数F,对于给定的输入,它总是产生相同的输出。

我们有一个必须执行N次的循环。在这个循环中,我们多次使用F的返回值来计算某些东西。

F的输入对于此循环的给定迭代始终是相同的。

我们有两个潜在的实现这个循环的实现。

  1. 实施#1:

    loop:
        set inputs to something;
        value = F(inputs);
        do something with value;
        do something else with value;
        do something else else with value;
    done
    
  2. 实施#2:

    loop:
        set inputs to something;
        value = F(inputs);
        do something with value;
        value = F(inputs);
        do something else with value;
        value = F(inputs);
        do something else else with value;
    done
    
  3. 两种实现循环次数相同。两者都得到相同的结果。 显然,实现#2的效率较低,因为它每次迭代会做更多的工作。

    在这个简单的例子中,编译器可能会注意到F总是为同一个输入返回相同的值,并且它可能会注意到我们每次都使用相同的输入调用它,但对于任何编译器,我们都可以构建一个相当于O(C*n) vs O(n)的示例,其中C在实践中非常重要。

答案 4 :(得分:3)

是的,它可能会有所作为。我没有检查你的代码是否正确,但请考虑一下:

第一个解决方案一直到列表循环,另一个循环到n。第二个解决方案在列表上循环一次,但它在第二个指针上使用->next() n次。所以基本上他们应该调用->next()大约相同的时间(也许+ -1左右)。

独立于你的例子,那不是什么大O符号。它是关于如果数据量增加算法如何缩放的近似值。如果你有一个算法O(n)并将其运行时间减少10%(与你的工作方式无关),那么它当然是一个好处。但是如果你将数据加倍,它的运行时间仍会加倍,这就是O(n)符号的含义。 (如果您将数据加倍,则O(n^2)算法将使其运行时间缩放4倍。)

答案 5 :(得分:1)

这是人们在从学术界转向实用性时所提出的问题。

如果您的数据集可能非常大,而且非常大且非常大,那么当然很重要。是你的决定。 有时,数据集大小是主要关注点。 当然不总是。

无论是否,大数据与否,总会有不变因素,它们可以在秒与小时之间产生差异。 你肯定关心他们。

学校通常没有教授的是如何找到大的加速因素。 例如,在完美编写的软件中,大型加速可以潜伏,如this example

获得加速的关键是不要错过任何。 只是找到一些,但不是全部,不够好,大多数工具都有huge blind spots。 该链接指向有经验的程序员学习的方法。

答案 6 :(得分:1)

常数肯定很重要,在很多情况下,人们可能倾向于说“这是唯一重要的事情”

现在很多情况和问题涉及具有非常长的延迟的事情:缓存未命中,页面错误,磁盘读取,GPU停顿,DMA传输。与这些相比,有时候你是否需要额外进行几千次或几万次迭代并不重要。

在过去二十年中,ALU功率一直比内存带宽(更重要的是,延迟)更陡峭,或者访问其他设备(如磁盘)。在GPU上,这比在CPU上更加明显(当DMA和ROP快2-3倍时,ALU变得快15-20倍)

具有O(log N)复杂度(例如,二进制搜索)导致单页错误的算法可能比避免此错误的O(N)算法(例如,线性搜索)慢几千倍

哈希表是O(1),但反复显示比其他具有更高复杂度的算法慢。与向量相比,链接列表通常具有相同(或更好)的算法复杂度。但是,由于列表执行更多分配并且具有更多缓存未命中,因此向量几乎总是明显优于列表。除非对象很大,否则即使必须在向量中移动几千个元素以在中间插入内容,通常也比单个节点分配和插入列表更快。

Cuckoo哈希十年前在短时间内闻名,因为在最坏的情况下(访问2项),O(1)的保证最大。事实证明,它在实践中要低得多,因为每次访问都有两个实际保证的缓存未命中。

以一种方式或另一种方式迭代二维数组(第一行/第一列)在复杂性方面完全相同,甚至在操作次数上也是如此。然而,其中一个常数要大一千倍,并且会慢一千倍。