算法基础-时间复杂度、对数器、Master定理剖析

算法基础

算法的复杂度

算法复杂度(算法复杂性)是用来衡量算法运行所需要的计算机资源(时间、空间)的量。通常我们利用渐进性态来描述算法的复杂度。

用n表示问题的规模,T(n)表示某个给定算法的复杂度。所谓渐进性态就是令n→∞时,T(n)中增长最快的那部分。

比如:T(n) = 2 * n ^ 2 + n log n + 3,那么显然它的渐进性态是 2 * n ^ 2,因为当n→∞时,后两项的增长速度要慢的多,可以忽略掉。

在算法复杂度分析中,log通常表示以2为底的对数。

引入渐进性态是为了简化算法复杂度的表达式,只考虑其中的主要因素。

当比较两个算法复杂度的时候,如果他们的渐进复杂度的阶不相同,那只需要比较彼此的阶(忽略常数系数)就可以了。

总之,分析算法复杂度的时候,并不用严格演算出一个具体的公式,而是只需要分析当问题规模充分大的时候,复杂度在渐进意义下的阶。

记号O给出了函数f(n)在渐进意义下的上界(但不一定是最小的)。

认识时间复杂度

  1. 常数操作
  2. 时间复杂度
  3. 一个简单的理解时间复杂度的例子

常数时间的操作:

  • 一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

时间复杂度:

  • 为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。
  • 具体来说:在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。

一个简单的理解时间复杂度的例子:

一个有序数组A, 另一个无序数组B, 请打印B中的所有不在A中的数, A数组长度为N, B数组长度为M。

算法流程1:

  • 对于数组B中的每一个数, 都在A中通过遍历的方式找一下;
  • 算法时间复杂度分析:时间复杂度为O(N * M);

算法流程2:

  • 对于数组B中的每一个数, 都在A中通过二分的方式找一下;
  • 算法时间复杂度分析:时间复杂度为O(logN * M);
    • 有M个数,每个数进行二分查找时间复杂度为O(logN);

算法流程3:

  • 先把数组B排序, 然后用类似外排的方式打印所有在A中出现的数;
  • 算法时间复杂度分析:时间复杂度为O((M * logM) + (M + N));
    • 无序数组B排序的时间复杂度假定为O(M * logM)
    • 外排时间复杂度为两个数组元素的长度和O(M + N) ;

对于同一个问题,比较以上3种不同的算法实现的时间复杂度情况:

  • 算法流程1:时间复杂度为 O(N*M) , 性能最差;
  • 算法流程2:时间复杂度为 O(logN * M) ;
  • 算法流程3:时间度杂度为 O(M * logM) + O(M+N) ;
  • 比较算法流程2和算法流程3,因为存在两个变量,M和N,需要根据不同的数据样本量具体分析,采用渐进性分析:
    • 当 M 远大于 N 时, N 可看作常数项忽略:
      • 算法流程2: O(logN * M) ,得到时间复杂度为: O(M)
      • 算法流程3: O(M * logM) + O(M + N) ,得到时间复杂度为: O(M * logM)
      • 得出算法流程2 优于 算法流程3;
    • 当 N 远大于 M 时:
      • 算法流程2: O(logN * M)
      • 算法流程3: O(M * logM) + O(M + N)
      • 需要根据 M 和 N 的具体的样本量才能分析

对数器

对数器的概念和使用:

  1. 有一个你想要测的方法a;
  2. 实现一个绝对正确但是复杂度不好的方法b;
  3. 实现一个随机样本产生器;
  4. 实现比对的方法;
  5. 把方法a和方法b比对很多次(100000+)来验证方法a是否正确。
  6. 如果有一个样本使得比对出错, 打印样本分析是哪个方法出错;
  7. 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。

注意要点:

  • 要测试的算法a是时间复杂度比较低的算法,而算法b唯一要求就是保证正确,而不用管复杂度的高低
  • 随机产生的样本大小要小,这里说的是样本的大小而不是样本的个数。因为出错时,小样本方便分析。
  • 随机产生的样本个数要多,100000+ - 只要大量随机产生的样本才可能覆盖所有的情况。
  • 如果算法b也无法保证完全的正确,在不断出错调试的过程中,也可以不断完善b,最终达到a和b都正确的完美结果。

对数器的使用:

  • 在算法竞赛或代码笔试中,要提前准备好常用算法的对数器,快速编写完算法实现后,使用相应的算法对数器快速验证算法的正确性,减少扣分或扣时;
  • 平常在手写算法实现时,验证算法的正确性非常有效;

AlgorithmComparator:

  • int[] generateRandomArray(int maxSize, int maxValue) 随机样本产生器
  • void comparator(int[] arr) 实现绝对正确的方法b
  • boolean isEqual(int[] arr1, int[] arr2) 实现比对方法a和方法b的处理结果的方法
  • void printArray(int[] arr) 比对失败时打印样本分析

Master定理

有些算法在处理一个较大规模的问题时,往往会把问题拆分成几个子问题,对其中的一个或多个问题递归地处理,并在分治之前或之后进行一些预处理、汇总处理。

这时候我们可以得到关于这个算法复杂度的一个递推方程,通过 Master公式 求解此方程便能得到算法的复杂度。

Master公式:T(N) = a * T(N/b) + O(N^d)

  • 说明:
    • T(N):样本量为N的情况下的时间复杂度
    • a*T(N/b) : 子过程调用的时间复杂度,N/b指子过程对数据的拆分情况,如二分为 N/2;
    • O(N^d) :除掉子过程调用外,剩余操作的时间复杂度
  • log(b,a) > d ,时间复杂度为:O(N^log(b,a))
  • log(b,a) = d ,时间复杂度为:O(N^d * logN)
  • log(b,a) < d ,时间复杂度为:O(N^d)

Master公式适用范围:划分为子过程的样本量规模是一致的。

当划分的子过程的样本量不一致时,不能使用Master公式,例:T(N) = T(2 * N / 5) + T(3 * N / 5) + O(N^2)

递归案例:在一个数组中找最大值

  • 使用递归方式:
    • 将数组切分成两部分,分别找到左边部分的最大值和右边部分的最大值,返回两数中的最大值;
    • 递归终止条件:当L == R,当左边界等于右边界时,只有一个数,直接返回;
  • 递归实现时间复杂度分析:T(N) = 2 * T(N/2) + O(1)
    • 样本量等分为两部分,每部分的时间复杂度为T(N/2)
    • 比较大小操作的时间复杂度为:O(1)
    • 代入Master公式得:T(N) = 2 * T(N/2) + O(N^0), a=2,b=2,d=0
    • 符合公式 log(2,2) = 1 > 0 , 得时间复杂度为:O(N^log(2,2)) -> O(N)
public static int findMax(int[] arr,int L, int R){
    if(L == R){
        return arr[L];
    }

    int mid = (L+R)/2;
    int leftMax = findMax(arr, L, mid);
    int rightMax = findMax(arr, mid+1 , R);
    return leftMax > rightMax ? leftMax : rightMax;
}

案例2:如果某个算法时间复杂度公式为:T(N) = 2 * T(N/2) + O(N):

  • 将样本量为N等分为两部分,每部分时间复杂度为:T(N/2)
  • 计算结果操作的时间复杂度为:O(N)
  • 代入Master公式得:T(N) = 2 * T(N/2) + O(N^1), a=2,b=2,d=1
  • 符合公式 log(2,2) = 1 等于 1 , 得时间复杂度为:O(N^1 * logN) -> O(N * logN)

相关链接

gitee地址:https://gitee.com/chentian114/chen_algorithm_study

github地址:https://github.com/chentian114/chen_algorithm_study

优快云地址:https://blog.youkuaiyun.com/chentian114/category_10828595.html

公众号

知行chen

参考

Leetcode

左程云 牛客网 算法初级班课程

回答问题: 计算一个算法时间复杂度,核心在于分析**算法中基本操作的执行次数与输入规模之间的关系**。通常我们使用 **大 O 表示法(Big-O Notation)** 来描述最坏情况下的时间复杂度--- ### 一、什么是时间复杂度时间复杂度是衡量一个算法运行时间随输入规模增长的变化趋势。它不是精确的时间(如秒或毫秒),而是表示程序执行所需的基本操作次数与输入规模 `n` 的函数关系。 --- ### 二、计算步骤 1. **找出算法中的基本操作**(例如赋值、比较、加减乘除等) 2. **统计该操作执行的次数**,作为关于输入规模 `n` 的函数 3. **忽略常数项和低次项**,只保留最高阶项 4. 使用大 O 表示法表达结果 --- ### 三、常见结构的复杂度分析 #### 1. 单层循环: ```cpp for(int i = 0; i < n; ++i) cout << i << endl; ``` - 执行了 `n` 次 - 时间复杂度:**O(n)** #### 2. 双重嵌套循环: ```cpp for(int i = 0; i < n; ++i) for(int j = 0; j < n; ++j) cout << i << " " << j << endl; ``` - 总共执行了 `n * n` 次 - 时间复杂度:**O(n²)** #### 3. 对数型复杂度: ```cpp for(int i = 1; i <= n; i *= 2) cout << i << endl; ``` - `i` 每次乘以 2,总共执行 `log₂n` 次 - 时间复杂度:**O(log n)** #### 4. 嵌套 + 非线性: ```cpp for(int i = 1; i <= n; ++i) for(int j = 1; j <= i; ++j) cout << "*"; ``` - 外层执行 `n` 次,内层分别执行 1, 2, ..., n 次 - 总次数为 `1+2+...+n = n(n+1)/2 ≈ n²/2` - 时间复杂度:**O(n²)** --- ### 四、递归算法时间复杂度分析(举例) #### 1. 简单递归(如阶乘): ```cpp int factorial(int n) { if (n == 0) return 1; return n * factorial(n - 1); } ``` - 递归调用 `n` 次 - 时间复杂度:**O(n)** #### 2. 分治递归(如归并排序): ```cpp void mergeSort(int arr[], int l, int r) { if (l < r) { int m = (l + r) / 2; mergeSort(arr, l, m); mergeSort(arr, m + 1, r); merge(arr, l, m, r); // 合并时间为 O(n) } } ``` 递推式为: ``` T(n) = 2T(n/2) + O(n) ``` 根据主定理Master Theorem)得: - 时间复杂度:**O(n log n)** --- ### 五、总结公式 | 结构 | 时间复杂度 | |------|-------------| | 单层循环 | O(n) | | 双重循环 | O(n²) | | 对数变化(每次×2或÷2) | O(log n) | | 分治递归(如归并排序) | O(n log n) | | 线性递归 | O(n) | | 指数级递归(如斐波那契递归) | O(2ⁿ) | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值