算法分析基础

数学基础

定义

如果存在正常数c与n0,使得当N >= n0时T(N) <= cf(N),则记为T(N=O(f(N))

如果存在正常数c与n0, 使得当N >= n0时T(N) >= cg(N),则记为T(N)=Ω(g(N))

当且仅当T(N) = O(h(N))且T(N) = Ω(h(N))时,则T(N)=Θ(h(N))

如果T(N) = O(p(N))且T(N) != Θ(p(N)),则T(N)=o(p(N))

 

法则

如果T1(N) = O(f(N))且T2(N) = O(g(N)),那么:

(a)T1(N) + T2(N) = max(O(f(N)),O(g(N)))

(b)T1(N) * T2(N) = O(f(N)g(N))

 

如果T(N)是一个k次多项式,则T(N)=Θ(Nk)

 

对于任意常数k,logkN=O(N)

 

函数增长率

典型增长率

 

函数 名称
c 常数
logN 对数级
log2N 对数平方根
N 线性级
NlogN  
N2 平方级
N3 立方级
2N 指数级

 

 

判断两个函数的增长率

可以通过比较两个函数的极限:limnf(N)g(N),来确定两个函数的增长率。必要的时候,还可以使用洛必达法则。

该极限有4种可能:

1 极限是0,这意味着f(N)=o(g(N));

2 极限是常数c != 0,这意味着f(N)=Θ(g(N));

3 极限是,这意味着g(N)=o(f(N));

4 极限摆动,二者无关

 

运行时间计算的一般法则

for循环

一次for循环的运行时间至多是for循环内语句(包括测试)的运行时间乘以迭代得次数

 

复制代码
for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
        k++;
    }
}

复制代码

 上面的例子当中,时间复杂度是N2

 

 

顺序语句

将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得得运行时间)

1
2
3
4
5
6
7
8
9
for (i = 0; i < N; i++) {
    A[i] = 0;
}  
 
for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
        A[i] += A[j] + i + j;
    }
}

 上面例子当中,第一个循环时间复杂度是O(N),第二个循环时间复杂度是O(N2),两个循环的总体时间复杂度为O(N)+O(N2)=max(O(N),O(N2)),也就是还是O(N2)

 

 if/else语句

对于程序片段:

if (condition) {
    S1;
} else {
    S2;
}

一个if/else语句得运行时间从不超过判断再加上S1和S2中运行时间长的运行时间。

 

运行时间中的对数

如果一个算法用常数时间O(1)将问题的大小削减为其一部分(通常是1/2),那么该算法得时间复杂度就是O(logN)。举个例子,下面是用迭代的方法写的二分查找:

复制代码
int BinarySearch(const ElementType A[], ElementType X, int N) {
    int Low, Mid, High;

    Low = 0;
    High = N - 1;

    while (Low <= High) {
        Mid = (Low + High) / 2;
        
        if (A[Mid] < X) {
            Low = Mid + 1;
        } else {
            if (A[Mid] > X) {
                High = Mid - 1;
            } else {
                return Mid;
            }
        }

     }
      return NotFound;
}
复制代码

在上面的二分法中,每次迭代的时间复杂度是O(1),并且每次迭代之后,问题复杂度就减为原来的一半,因此,这个程序的时间复杂度为O(logN)

另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是O(N)。举个例子,下面使用递归的方式求解N!:

复制代码
long int Factorial(int N) {
    if (N <= 1) {
        return 1;
    } else {
        return N * Factorial(N - 1);
    }
}
复制代码

在上面的递归中,每次递归的时间复杂度是O(1),并且递归之后,问题复杂度只比原来少1,因此,这个程序的时间复杂度就是O(N)

 

递归时间复杂度总结

对于一个问题复杂度为T(N)的问题,可以分解为如下的表达式:

T(N)=aT(Nb)+f(N),其中a是常数,且a >= 1, b也是常数,b > 1。

1 如果存在正常数ε>0f(N)=O(Nlogbaε),那么T(N)=Θ(Nlogba);

2 如果f(N)=Θ(Nlogba),那么T(N)=Θ(NlogbalogN);

3 如果存在正常数ε>0使得f(n)=Ω(Nlogba+ϵ)且存在常数c < 1使得对于足够大的N满足af(Nb)cf(N),那么T(N)=Θ(f(N)).

 

这三种情形如何记忆呢?我们可以这样记忆,三种情况的结果取决于函数f(N)与Nlogba的大小。如果Nlogba大,那么结果就是情形1;如果是f(N)大,那么结果就是情形3;如果两个同级别,那么就是情形2。

还需要注意的事情是,这三种情形没有覆盖所有的情形。情形1与情形2之间有Gap,如果f(N)比Nlogba小,但是如果不是同时小一个Nϵ因子,就落到情形1与情形2之间;同时,情形2与情形3之间也有Gap,如果f(N)比Nlogba大,但是如果不同时大一个Nϵ因子,就落到情形2与情形3之间。如果发生这两种情形,就无法应用上面的三个公式。

 

下面我们看一下实际的例子。

1 T(N)=9T(N3)+N

其中a = 9, b = 3, f(N) = N,Nlogba=Nlog39=Θ(N2),并且f(N)=O(Nlog39ε),其中ε=1,那么这个满足情形1,即T(N)=Θ(Nlog39)=Θ(N2)

 

2 T(N)=T(23N)+1

其中a = 1,b = 32,f(N) = 1,同时Nlog321=N0=1,那么这就满足情形2,即T(N)=Θ(Nlog321logN)=Θ(logN)。这其实就是上面运行时间中的对数当中得情形1。

 

3 T(N)=3T(N4)+NlogN

其中a = 3,b = 4,f(N) = NlogNNlogba=Nlog43=Ω(N0.793)。因为f(N)=Ω(Nlog43+ε)  ε0.2,同时对于足够大的N,满足af(Nb)=3(N4)logN434NlogN=cf(N),其中c=34。这就满足了情形3,即T(N)=Θ(NlogN)

 

4 T(N)=2T(N2)+NlogN

其中a = 2,b = 2,f(N)=NlogN, Nlogba=Nlog22=N,表面上看,这属于情形3,但是,却不存在一个正常数ε,使得f(N)Nlogba+ε=NlogNNlog22+ε=NlogNNNε=logNNε1。所以无法运用上面3中情形的公式。

 

posted @   chaoguo1234  阅读(221)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示