算法的时间复杂度推导方法

算法的时间复杂度推导方法

独立博客地址:chugang.net

语句频度

语句频度是指语句的重复执行次数。

推导大O阶方法
方法
  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶项存在且不是1,则去除与这个项相乘的乘数。
常数阶举例运用

右侧注释中的 num 表示语句执行的次数。

int sum = 0, n = 100;       /* num = 1 */
sum = (n+1) * n / 2;        /* num = 1 */
printf("%d", sum);          /* num = 1 */

这段代码的运行次数函数是 f(n) = 1 + 1 + 1 ,根据“推导大O阶方法”中的第一条规则,把
1 + 1 + 11 替换,运行次数函数变成了 f(n) = 1。该函数只有常数项,只需使用规
则1就可以推导出它即这段代码的时间复杂度是 O(1)

假如 sum = (n+1) * n / 2 执行3次,将上面的代码修改为:

int sum = 0, n = 100;       /* num = 1 */
sum = (n+1) * n / 2;        /* num = 1 */
sum = (n+1) * n / 2;        /* num = 1 */
sum = (n+1) * n / 2;        /* num = 1 */
printf("%d", sum);          /* num = 1 */

这段代码的运行次数函数是 f(n) = 1 + 1 + 1 + 1 + 1 。 按照推导大O阶第一条规则,用1取代
所有的加法常数,这段代码的运行次数函数是 f(n) = 1。这段代码的时间复杂度依然是 f(n) = O(1)

所有这类代码的时间复杂度都是 O(1)O(1)叫做常数阶。不存在 O(2)O(9) 这类写法。

线性阶举例运用

code-3

int i;
for(i = 0; i < n; i++){
    // 时间复杂度为O(1)的代码
}

code-3的运行次数函数是 f(n) = n * 1。加法常数为0个,跳过规则一。变量n的最高阶是 n * 1,无
其他项,跳过规则二。n * 1中的系数本来就是1,也可以直接跳过规则三,得到code-3的时间复杂度是
f(n) = O(n)

code-4

int i;
for(i = 0; i < n; i++){
    // 时间复杂度为O(1)的代码
}

int j;
for(j = 0; j < m; j++){
    // 时间复杂度为O(1)的代码
}

code-4的运行次数函数是f(n) = n * 1 + m * 1。直接跳过规则一。n * 1 + m * 1有两个变量,
但次数都是1,任何一项 n * 1m * 1 都可视为最高价,根据推导规则二“保留最高阶”,得出
运行次数函数是f(n) = n * 1f(n) = m * 1。最后根据规则三,得出code-4的时间复杂度是
f(n) = O(n)

对数阶举例运用

code-5

int count = 1;
while(count < n){
    count = count * 2;
    //其他时间复杂度为O(1)的代码 
}

code-5似乎不能用前面的推导大O阶方法来分析时间复杂度,我从《数据结构与算法分析》P21找到了分析
“运行时间中的对数”的一般法则。这个一般法则是:

如果一个算法用常数时间(O(1)将问题的大小削减为其一部分(通常是1/2),那么该算法就是 O(log N)
另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是 O(N)

code-5中,假设 n = 8 ,初始化时,while(count < n) 需要运行8次。经过一次循环后,count变为2,
循环需要运行4次,变为原来的一半。根据那条一般法则,判断 code-5 的时间复杂度是 O(log N)

code-5修改为code-6

int count = 1;
while(count < n){
    count = count + 2;
    //其他时间复杂度为O(1)的代码 
}

code-6每次执行循环后,会把问题减少2个常数,时间复杂度应为 O(N)

若将code-6中的count = count + 2改为code = count - 2,时间复杂度仍然是 O(N)。但我有点理解不了。

平方价举例运用

code-7

int i, j;   /*1*/
for(i = 0; i < n; i++){ /*2*/
    for(j = 0; j < n; j++){ /*3*/
        //时间复杂度为O(1)的代码 /*4*/
    }   /*5*/
}   /*6*/

code-7中第二个循环体的时间复杂度是O(N)。第一个循环体将第二个循环体再执行N次,时间复杂度变为O(N2)
如果将第二个循环体中的n改为m,那么code-7的时间复杂度就是O(N*M)。注意,O(N*M)O(N2)都叫做
平方阶,二者实质相同。

多层循环体的时间复杂度就是每层循环体的运行次数相乘。

code-8

int i, j;
for(i = 0; i < n; i++){
    for(j = i; j < n; j++){
        //时间复杂度为O(1)的代码
    }
}

code-8运行次数是(n+1)*n*n/2。只保留最高阶并且去掉它的系数,时间复杂度是O(N2)。有些地方理解不了。

code-9

void function(int count){

    int j;

    for(j = count; j < n; j++){

        printf("%s", "hello,world");

    }
}

n++;        /* num = 1 */

function(n);    /* num = n */

int i,j;    /* num = 1 */

for(i = 0; i < n; i++){     /* num = n*n */

    function(i);

}

for(i = 0; i < n; i++){         /* num = (n+1)*n/2 */

    for(j = i; j < n; j++){

        printf("%s", "hi");

    }
}

code-9的时间复杂度是多少呢?

首先将每行代码的执行次数标出来。

code-9的执行次数(首先忽略掉常数项)是n + n*n + (n+1)*n/2,计算结果为1.5*n*n + 2*n
只保留最高阶1.5*n*n,最后将系数变为1,执行次数为n*n,时间复杂度为O(N2)

这种方法有不确定性的因素存在,或者说,在计算执行次数的时候,使用了互相矛盾的方法。

独立博客地址:chugang.net

重新思考之后,发现并不存在矛盾,而是《大话数据结构》中推导时间复杂度的方法有小缺陷。这个推导
本身就是一种粗略估计,为何在推导过程中有时又在进行精确计算呢?以code-8为例,该书使用了精确
的计算方法。若全部都坚持使用粗略估计计算,那么计算过程是这样的:code-8中第二个循环体的运行
次数是N,或者抽象为“一个变量”,第一个循环体的运行次数也是N或M,也抽象为“一个变量”,整体的运行
次数为两个变量相乘,即时间复杂度为O(N2)。这种粗略估计计算方法,建立的基础是:影响循环执行次数
的变量只有那个与循环终止条件相关的变量,与起始变量无关。

这种方法,又不能适用于对数阶的时间复杂度推导。或许,对数阶中的循环,本来就是一种特殊情况,不能
采用普通循环的推导方法。此处存疑。*

常见的时间复杂度

直接摘抄《大话数据结构》中的有关部分。
>

常见的时间复杂度

>

理解不了这些时间复杂度所耗时间的顺序,应该如何比较它们所耗时间?

独立博客地址:chugang.net

<think>好的,我现在需要详细解释Dijkstra算法时间复杂度推导过程。首先,我得回顾一下Dijkstra算法的基本步骤,这样才能分析每个部分的时间消耗。 Dijkstra算法用于解决单源最短路径问题,主要特点是通过逐步扩展找到从起点到所有其他节点的最短路径。它的基本步骤包括初始化距离数组,然后每次选择当前距离最小的节点,更新其邻居节点的距离。这个过程需要重复直到所有节点都被处理过。 接下来,我需要考虑不同实现方式对时间复杂度的影响。传统实现使用数组来存储距离,每次选择最小距离节点时需要遍历整个数组,这需要O(n)的时间,而每次更新邻居的距离是O(1)。因为对于n个节点,每次选择节点的时间是O(n),总共进行n次,所以这部分的时间复杂度是O(n²)。另外,每条边都会被访问一次,更新操作的时间是O(m),其中m是边的数量。因此,总的时间复杂度是O(n² + m),通常简化为O(n²)。 然后是使用优先队列(比如最小堆)优化的版本。这里,每次取出最小距离节点的时间降为O(log n),因为堆的提取操作是对数时间。同时,每次更新邻居的距离时,可能需要将新的距离插入堆中,这同样需要O(log n)的时间。对于m条边来说,总共有m次更新操作,因此这部分的时间复杂度是O(m log n)。再加上初始化堆和其他操作的时间,总的时间复杂度变为O((n + m) log n)。对于稀疏图(m接近n),这比O(n²)要好,尤其是当使用更高效的数据结构如斐波那契堆时,可以进一步优化到O(m + n log n)。 需要确认不同资料中的说法是否一致。根据引用[3],使用小根堆将找到最小节点的时间从O(n²)降到O(1),这可能是指每次提取的时间是O(1),但实际上堆的提取操作是O(log n),所以可能需要更仔细的分析。引用[2]提到Dijkstra的时间复杂度是O(N²),这可能是指传统实现,而引用[3]讨论了优化后的O(n + e log n),这里e是边数,对应不同的图结构。 另外,需要明确不同步骤的具体操作次数。例如,传统实现中,每次选择节点需要遍历n个元素,共n次,所以O(n²)。而堆优化后,每个节点被插入堆一次,每次插入和提取都是O(log n),因此节点数n乘以log n,加上边数m乘以log n,总的是O((n + m) log n)。对于稠密图(m≈n²),这反而可能更差,但对于稀疏图则更优。 还要考虑是否所有边都被处理一次,以及堆中可能存在重复节点的问题。例如,当某个节点的距离被多次更新时,堆中可能会有多个该节点的条目,但每次提取时会处理最新的距离。这可能导致堆的大小超过n,但总体操作次数仍由边数m决定,所以时间复杂度保持为O(m log n)。 总结起来,传统实现的时间复杂度推导基于遍历数组选择最小节点,而堆优化版本则通过优先队列减少了选择节点的时间,但增加了每次更新的时间。最终的时间复杂度取决于实现方式和图的稀疏程度。</think>### Dijkstra算法时间复杂度推导详解 Dijkstra算法时间复杂度推导需结合其实现方式,主要分为**基础数组实现**和**优先队列优化实现**两种形式。以下是详细推导过程: --- #### **一、基础数组实现(时间复杂度$O(n^2)$)** 1. **初始化操作** - 初始化距离数组$dist$和标记数组$visited$,时间复杂度为$O(n)$。 2. **主循环过程** - 外层循环需要遍历所有$n$个节点,每次选择一个未访问的最近节点$u$。 - 内层循环需要遍历$n$个节点以找到$dist$最小值,时间复杂度为$O(n)$。 - **总选择操作时间**:$n \times O(n) = O(n^2)$。 3. **松弛操作(Relaxation)** - 对每个节点$u$,需遍历其所有邻居节点$v$,并更新$dist[v]$。 - 设图中共有$m$条边,每条边最多被松弛一次,时间复杂度为$O(m)$。 4. **总时间复杂度** $$O(n^2 + m)$$ 由于在稠密图中$m \approx n^2$,因此时间复杂度简化为$O(n^2)$[^2]。 --- #### **二、优先队列优化实现(时间复杂度$O((n+m)\log n)$)** 1. **优先队列的作用** - 使用小根堆(或优先队列)快速获取当前最小距离节点,将选择操作的时间复杂度从$O(n)$降为$O(\log n)$。 2. **操作分解** - **插入操作**:每次更新$dist[v]$时,向堆中插入新值,最多插入$m$次,时间复杂度为$O(m \log n)$。 - **提取最小操作**:共需提取$n$次,时间复杂度为$O(n \log n)$。 3. **总时间复杂度** $$O((n + m)\log n)$$ 在稀疏图($m \approx n$)中,复杂度接近$O(n \log n)$;在稠密图($m \approx n^2$)中,复杂度退化为$O(n^2 \log n)$,因此更适合稀疏图[^3]。 --- #### **三、时间复杂度的直观对比** | 实现方式 | 时间复杂度 | 适用场景 | |----------------|------------------|------------------| | 基础数组 | $O(n^2)$ | 稠密图 | | 优先队列优化 | $O((n+m)\log n)$| 稀疏图 | --- #### **四、推导关键点总结** 1. **基础实现**:核心时间消耗在每次遍历数组寻找最小值。 2. **堆优化**:通过减少最小值查找时间,但增加了堆维护成本。 3. **图的稀疏性**:优化效果与图的边数直接相关,稀疏图收益最大[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值