虽然我理解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表示法放在一边,第二个解决方案是否比第一个解决方案更有效,因为它只迭代列表一次?
答案 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:
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:
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
两种实现循环次数相同。两者都得到相同的结果。 显然,实现#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)的保证最大。事实证明,它在实践中要低得多,因为每次访问都有两个实际保证的缓存未命中。
以一种方式或另一种方式迭代二维数组(第一行/第一列)在复杂性方面完全相同,甚至在操作次数上也是如此。然而,其中一个常数要大一千倍,并且会慢一千倍。