Martube Home for writing

算法—时间复杂度

算法—时间复杂度

全文大部分引用自 算法—时间复杂度

什么是时间复杂度

关于时间频度: 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的 语句执行次数 称为语句频度或时间频度。记为T(n);

在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。记为O(…),也称为 大O表示法

另外,时间频度不同,但时间复杂度可能相同。如:T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)

注:全文中的 An = A*n,nA = n^A(n的A次方),比如n2 就是n的平方

时间复杂度去估算算法优劣的时候注重的是算法的潜力,也就是在数据规模有压力的情况之下(最坏情况)算法的执行频度,什么意思呢?比如2个算法,在只有100条数据的时候,算法a比算法b快,但是在有10000条数据的时候算法b比算法a快,这时候我们认为算法b的时间复杂对更优;

如何计算一个算法的时间复杂度

算这个时间复杂度实际上只需要遵循如下守则:

常见的时间复杂度

按增长量级递增排列,常见的时间复杂度有:

O(1)—常数阶

O(1)的算法是一些运算次数为常数的算法。例如:

temp = a;
a = b;
b = temp;

根据守则: 用常数1来取代运行时间中所有加法常数; 上面语句共三条操作,单条操作的频度为1,即使他有成千上万条操作,也只是个较大常数,这一类的时间复杂度为O(1);

O(n)—线性阶

O(n)的算法是一些线性算法。例如:

sum = 0;
for(i=0;i<n;i++)
    sum++

上面代码中第一行频度1,第二行频度为n,第三行频度为n,所以f(n)=n+n+1=2n+1。 根据守则: 只要高阶项,不要低阶项目,常数项置为1,去除高阶项的系数: 所以时间复杂度O(n)。这一类算法中操作次数和n正比线性增长。

O(log2N)———对数阶

… 先来复习一波数学

什么是对数?

a^X = N,(a>0 && a!=1),那么X即是以a为底,N的对数,记作 其中a叫做对数的底数,N叫做真数。

对数与指数的关系: 当a>0,a≠1时,a^X=N X=logaN。(N>0) 比如 2^3=8 => 3=log2 8 , 2是底数 3是对数 8是真数…..@_@

例1
    private static void 对数阶() {
        int number = 1;//执行1次
        int n = 100;//执行1次

        while (number < n) {
            number = number * 2; // 执行n/2次
            System.out.println("哈哈");//执行1次
        }
    }

假设n为100,number是1,小于100退出循环。

也就是2^x=n得出x=log₂n。因此它的复杂度为O(log2N)。(X是循环次数)

例2

二分查找; 比如: 1,3,5,6,7,9;找出7 如果全部遍历时间频度为n; 二分查找每次砍断一半,即为n/2; 随着查询次数的提升,频度变化作表:

查询次数 时间频度
1 n/2
2 n/2^2
3 n/2^3
k n/2^k

当最后找到7的时候时间频度则是1; 也就是: n/2^k = 1; n = 2^k; k则是以2为底,n的对数,就是Log2N; 那么二分查找的时间复杂度就是O(Log2N);

O(nlogN)———线性对数阶


这里挖一下 LogN 在这里的写法 参考算法复杂度中的O(logN)底数是什么

关于算法的时间复杂度很多都用包含O(logN)这样的描述,但是却没有明确说logN的底数究竟是多少。

算法中log级别的时间复杂度都是由于使用了分治思想,这个底数直接由分治的复杂度决定。 如果采用二分法,那么就会以2为底数,三分法就会以3为底数,其他亦然。 不过无论底数是什么,log级别的渐进意义是一样的。 也就是说该算法的时间复杂度的增长与处理数据多少的增长的关系是一样的。

我们先考虑O(logx(n))和O(logy(n)),x!=y,我们是在考虑n趋于无穷的情况。 求当n趋于无穷大时logx(n)/logy(n)的极限可以发现,极限等于lny/lnx,也就是一个常数, 也就是说,在n趋于无穷大的时候,这两个东西仅差一个常数(常数系数)。 所以从研究算法的角度log的底数不重要。

最后,结合上面,我也说一下关于大O的定义(算法导论28页的定义), 注意把这个定义和高等数学中的极限部分做比较, 显然可以发现,这里的定义正是体现了一个极限的思想, 假设我们将n0取一个非常大的数字, 显然,当n大于n0的时候,我们可以发现任意底数的一个对数函数其实都相差一个常数倍而已。 所以书上说写的O(logn)已经可以表达所有底数的对数了,就像O(n^2)一样。 没有非常严格的证明,不过我觉得这样说比较好理解,如果有兴趣证明,完全可以参照高数上对极限趋于无穷的证明。


上面看了二分查找,是LogN(Log2N)的; 线性对数阶就是在LogN的基础上多了一个线性阶;

比如这么一个算法流程: 数组a和b,a的规模为n,遍历的同时对b进行二分查找,如下代码:

执行次数为 n * logn

n = a.length;
for(int i =0;i<n;i++){
    binary_search(b);
}

O(n^2)———平方阶

普通嵌套循环
   private static void 普通平方阶(){
        int n = 100;
        for (int i = 0; i < n; i++) {//执行n次
            for (int j = 0; j < n; j++) {//执行n次
                System.out.println("哈哈");
            }
        }
    }

这种就是2层循环嵌套起来,都是执行n次,属于乘方关系,它的时间复杂度为O(n^2)。

等差数列嵌套循环
  private static void 等差数列平方阶() {
        int n = 100;
        for (int i = 0; i < n; i++) {//执行n次
            for (int j = i; j < n; j++) {//执行n - i次
                System.out.println("哈哈");
            }
        }
    }

i = 0,循环执行次数是 n 次。 i = 1,循环执行次数是 n-1 次。 i = 2,循环执行次数是 n-2 次。 … i = n-1,循环执行的次数是 1 次。

result = n + (n - 1) + (n - 2) … + 1 被加数递减,抽象为一个等差数列求n项和的问题,公差为1,带入公式,Sn = n(a1 + an ) ÷2 result = (n(n+1))/2 = (n^2+n)/2 = (n^2)/2 + n/2

1.去掉运行时间中的所有加法常数。 没有加法常数,不考虑。 2.只保留最高阶项。 最高阶参考上面列出的按增长量级递增排列,于是只需要保留result = (n^2)/2 3.如果最高阶项存在且不是1,去掉与这个最高阶相乘的常数得到时间复杂度 除以2相当于是乘以二分之一,去掉它,就得到,result = n^2, 所以这个算法的时间复杂度为O(n^2)。

时间复杂度的优劣对比

常见的数量级大小:越小表示算法的执行时间频度越短,则越优;

    O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n) //2的n方<O(n!)<O(nn)//n的n方