【问题标题】:Same time complexty but highly varying runtimes时间复杂度相同但运行时间差异很大
【发布时间】:2018-01-04 13:48:14
【问题描述】:

对于问题:给定一个偶数(大于 2),返回两个素数,其和等于给定数。

以下算法的时间复杂度分别为 O(A2.5) 和 O(Alog(log(A))。仍然对于大至 73939138 的 A(整数)值,第二个是真的很慢。我尝试了很多输入,第一个更快。你能解释一下原因吗?

int ul=A/2;
vector <int> answer;
for (int i=1; i<=ul; i++)
{
    if (check(i)==1 && check(A-i)==1 ) //check(i) checks primality of i in O(i^1.5)
   {
       int myint[2] ={ i,A-i };
       answer.assign( myint, myint+2);
       return answer;
   }
}

vector<bool> primes(A+1,true);
int i,j;
//Sieve of Eratosthenes O(Alog(log(A)))
for(i=2;i*i<A+1;i++)
{
    if(primes[i])
    {
        for(j=2;i*j<A+1;j++)
            primes[i*j]=0;
    }
}
vector<int> arr,answer;
//arr is vector containing all primes from 2 to A; O(A)
for(i=2;i<A+1;i++)
{
    if(primes[i])
        arr.push_back(i);
}
i=0;j=arr.size()-1;
//Algorithm to find 2 numbers summing up to a value
while(i<=j)
{
    if(arr[i]+arr[j]>A)
        j--;
    else if(arr[i]+arr[j]<A)
        i++;
    else
    {
        answer.push_back(arr[i]);
        answer.push_back(arr[j]);
        return answer;
    }
}

编辑:check(n)定义如下:

int check(int n)
{
    int ul=sqrt(n);
    if(n<2)
        return 0;
    for(int i=2; i<=ul; i++)
    {
        if(n%i==0)
            return 0;
    }
    return 1;    
}

【问题讨论】:

    标签: c++ algorithm primes


    【解决方案1】:

    时间复杂度不是关于算法运行的快速,而是关于它的速度如何缩放随着问题的变大。在每个元素上花费 1 秒的算法与在每个元素上花费 1 微秒的算法具有相同的时间复杂度:O(n)。在这两种情况下,如果您的元素数量是原来的 10 倍,那么算法的运行时间将是原来的 10 倍。

    【讨论】:

    • 那么,如果我遇到这样一个选择算法的问题,那么我该如何判断哪一个更适合我的目的呢?我是否必须运行并检查所有可能的输入,看看哪个更好?
    • @Tanay 你检查它的相关输入,看看哪个更好。您还可以找到一种算法优于另一种算法的点,并根据输入大小决定使用哪种算法。
    • @Tanay -- 选择算法需要了解算法及其数据敏感性。时间复杂度不能给你答案;它可以帮助您了解正在发生的事情。如果 O(n^2) 算法进行了大量的车轮旋转并且数据足够小,则 O(n^2) 算法可能是比 O(n) 算法更好的选择。
    【解决方案2】:

    您考虑的复杂性并不能立即提供有关算法性能的信息,而是提供有关渐近行为的信息,通常用于最坏情况场景。 p>


    最坏情况与平均情况

    看看A = 73939138的答案就知道了:

    73939138 = 5 + 73939133
    

    所以基本上,您的第一个算法对 check 进行了大约 10 次调用,而第二个算法正在通过巨大的循环来填充数组 primes..

    第一种算法的平均情况复杂度可能远低于O(A^2.5),而第二种算法的平均情况复杂度接近或等于O(A log(log(A))

    注意:以下关于平均情况复杂性的内容只是猜测,不要将它们视为可靠的结果。

    第二种算法:

    在该算法中,无论A 是什么,您都必须使用埃拉托色尼筛 填充数组primes,即O(A log(log(A)))。由于这是算法中最耗时的部分,因此该算法的平均情况复杂度可能接近其最坏情况复杂度,所以O(A log(log(A)))

    第一个算法:

    这里比较复杂……基本上看算法的结果。根据Wikipedia's page on Goldbach's conjecture,将A写成两个素数之和的平均数是A / (2 * log(A) ^ 2)

    由于素数不能对两种不同的方式做出贡献,这意味着平均有2 * A / (2 * log(A) ^ 2) = A / (log(A) ^ 2) 个素数对其中一种方式做出贡献。

    如果我们**假设*1这些素数是均匀分布的,那么较小的应该接近A / (A / log(A) ^ 2) = log(A)^2

    因此您只需要检查最多大约为log(A)^2 的数字。

    1完全不知道如果这是真的,我只是在猜测......


    渐近行为

    检查@PeterBecker's answer and comments

    当你说O(A log(log(A))) 复杂性时,你隐藏了很多东西——f(A) = C * (A log(log(A))) + g(A) 的任何函数g(A)O(A log(log(A))) 也是O(A log(log(A)))

    例如:

    f(A) = c1 * A * log(log(A)) + c2 * A + c3 * log(A)
    

    ...是O(A log(log(A)))

    系数c1c2c3 决定了算法实现的真实行为,不幸的是,这些系数通常很难找到(或很复杂)。

    例如,快速浏览一下您的实现会显示以下内容:

    • 第一种算法不使用任何类型的容器,因此对内存的要求很少(只有一些局部变量)。
    • 第二种算法使用两个比较大的数组,primesarr——如果A = 73939138
      • primes 包含 73939139 “实体” — 这可能已通过 std::vector&lt;bool&gt; 的专门化进行了优化,但您仍然需要 ~9MB,它不适合 L1 缓存,可能是 L2,并且您需要一些位-每次访问的明智操作。
      • arr 应该包含 ~4000000 int(请参阅 here),并且您需要多次分配,因为您使用的是 push_back

    【讨论】:

    • 我明白最坏情况下的复杂性不一定总是发生。但是我如何通过查看平均时间复杂度更好的算法来判断。 ?
    • @Tanay 这在我的回答中进行了解释,无论n 是什么,第二个算法仍然需要计算直到n 的所有素数,这是算法中最耗时的部分,所以它的平均复杂度接近(如果不等于)它的最坏情况。对于第一个算法,它更复杂,我会尝试在我的答案中添加一些信息。
    • @Tanay 我已经编辑了答案。请注意,我在这里做了 HUGE 假设,但你应该明白它背后的想法。
    • 现在,这是我一直在寻找的解决方案 @Holt。非常感谢!!
    • +1,这是一个很好的答案。我相信第一个算法比您的分析要好一些,因为它不是使用两个“随机”素数,而是从 p=3 开始,然后接下来尝试 5、7、11 等。这些第一个整数比素数更有可能是素数1/log(A)。我相信它,但我也无法证明它;)。我的猜测是复杂度是 O(log(A) * log(log(A)) 步骤。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-13
    • 2020-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多