数据结构和算法之——算法复杂度分析

1. 何为数据结构?何为算法?

  • 简单来说,数据结构就是数据的存储方式,比如数组就是把数据存在一段连续的内存上,而链表则是通过指针的关联将数据存在任意可用的内存上;栈是先进后出,队列是先进先出。

  • 而算法则是对这些数据的操作方法,比如数据的插入、查找、删除、排序等。

  • 二者相辅相成,互为一体,数据结构为算法服务,而算法要在指定数据结构上进行操作。

2. 复杂度分析?

  • 学习数据结构和算法的目的是为了在实际应用的时候更加优化地利用内存,提高程序运行效率,而复杂度分析则是给我们提供一个衡量代码质量好坏的标准。

  • 如果我们在不运行程序的情况下就可以定性知道代码的内存占用和时间消耗,这将会给我们提供一个当前程序的总体评估和未来的改进方向。

  • 直接运行程序就可以知道算法的执行时间和占用内存,但这个过程往往会受到运行环境和数据规模的影响,因此,我们需要一个不用进行具体测试就可以粗略估计算法执行效率的方法,这就是复杂度分析。

3. 时间复杂度

3.1 大 O 复杂度表示法

int cal(int n) 
{
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) 
   {
      sum = sum + i;
   }
   return sum;
 }
  • 我们假设每行代码的运行时间为 t,则第一二行代码需要时间为 2 * t,第三四行代码需要时间为 2n * t,总时间为 (2n+2) * t,代码运行总时间与 n 成正比。
  • 用大 O 法可表示为 O(2n+2),这并不代表代码的实际执行时间,只是表征代码执行时间随数据规模的变化趋势
  • 当 n 足够大时,低阶、常量和系数就可以忽略不计,直接表示为 O(n)。

3.2 常用分析方法

  • 循环最多代码,重点关注
  • 串行代码,复杂度相加
  • 嵌套代码,复杂度相乘

3.3 几种常见复杂度

  • 多项式量级
    • 常量阶 (O(1))
    • 对数阶 (O(log_n))
    • 线性阶 (O(n))
    • 线性对数阶 (O(nlog_n))
    • 乘方阶 (O(n^2)、O(n^3)、O(n^k))
  • 非多项式量级(Non-Deterministic Polynomial)
    • 指数阶 (O(2^n))
    • 阶乘阶 (O(n!))
  • 非多项式量级的算法的执行时间会随着数据规模的扩大急剧增加,是非常低效的算法。

时间复杂度

3.4 进阶情况

  • 最好情况时间复杂度(Best Case Time Complexity)
  • 最坏情况时间复杂度(Worst Case Time Complexity)
  • 平均情况时间复杂度(Average Case Time Complexity)

以查找为例,看如下代码

// n 表示数组 array 的长度
int find(int[] array, int n, int x) 
{
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) 
  {
    if (array[i] == x) 
    {
       pos = i;
       break;
    }
  }
  return pos;
}

最好情况时间复杂度就是在程序最理想的状态下,数组第一个元素就是我们要查找的元素,只需要查找一次;而最坏情况时间复杂度就是在程序最糟糕的状态下,数组最后一个元素才是我们要查找的元素,需要查找完整个数组;

事实上,我们要查找的元素可能存在数组中的任何一个位置,甚至可能不存在于数组中,因此,考虑所有情况出现的概率,求出各种情况下时间复杂度的平均值,也就是平均情况时间复杂度。

  • 均摊情况时间复杂度(Amortized Case Time Complexity)
// array 表示一个长度为 n 的数组
// 代码中的 array.length 就等于 n
int[] array = new int[n];
int count = 0;
 
void insert(int val) 
{
    if (count == array.length) 
    {
       int sum = 0;
       for (int i = 0; i < array.length; ++i) 
       {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }

    array[count] = val;
    ++count;
 }

这段代码的功能是向数组中插入一个元素,当数组未满时,直接插入,时间复杂度为 (O(1));当数组满时,先计算数组所有元素的和,再插入元素,时间复杂度为 (O(n))

并且,两种复杂度不同的操作具有一定的规律,一系列 (O(1)) 的插入导致数组占满,然后紧跟着一个 (O(n)) 的插入,再继续循环往复。

这时候,我么就可以把 (O(n)) 复杂度的这个操作平均分摊到前面的 (O(1)) 复杂度操作上去,整体的时间复杂度也就变成了 (O(1)),这就是均摊情况时间复杂度。

如果大部分情况时间复杂度都很低,只有少数情况时间复杂度较高,并且这些操作具有前后的时序关系,那么我们就可以应用均摊情况时间复杂度来进行分析。通常来说,均摊情况时间复杂度就等于最好情况时间复杂度

4. 时间复杂度的计算

  • 同阶函数集合 (Theta(f(n))= {g(n)space |space exists space c_1,c_2 >0, n_0, forall space n>n_0, c_1f(n) leqslant g(n) leqslant c_2f(n) }) 称为与 (f(n)) 同阶的函数集合。

  • 低阶函数集合 (O(f(n))= {g(n)space |space exists space c >0, n_0, forall space n>n_0, 0 leqslant g(n) leqslant cf(n) }) 称为比 (f(n)) 低阶的函数集合。

  • 高阶函数集合 (Omega(f(n))= {g(n)space |space exists space c >0, n_0, forall space n>n_0, 0 leqslant cf(n) leqslant g(n) }) 称为比 (f(n)) 高阶的函数集合。

  • 严格低阶函数集合 (o(f(n))= {g(n)space |space forall space c >0, exists space n_0, forall space n>n_0, 0 leqslant g(n) < cf(n) }) 称为 (f(n)) 的严格低阶函数集合。

  • 严格高阶函数集合 (omega(f(n))= {g(n)space |space forall space c >0, exists space n_0, forall space n>n_0, 0 leqslant cf(n) < g(n) }) 称为比 (f(n)) 高阶的函数集合。

  • 迭代法求解递归方程

[egin{aligned} T(n)&=2T(n/2)+cn \ &=2^2T(n/2^2)+cn+cn\ &=2^3T(n/2^3)+cn+cn+cn\ &=cdots\ &=2^kT(n/2^k)+kcn\ &=2^kT(1)+kcn gets 2^k=n\ &=nT(1)+cnlogn\ &=Theta(nlogn)\ end{aligned} ]

  • Master 定理求解递归方程

(ageqslant 1, b>1) 是常数,(f(n)) 是函数,(T(n)) 是定义在非负整数集上的函数且 (T(n)=aT(n/b)+f(n)),那么 (T(n)) 可以如下求解:

  1. (f(n))(n^{log_ba}) 的低阶函数,且低出一个多项式,(f(n)=O(n^{log_b a-epsilon}), epsilon>0),那么 (T(n)=Theta(n^{log_b a}))
  2. (f(n))(n^{log_ba}) 的同阶函数,(f(n)=Theta(n^{log_b a})),那么 (T(n)=Theta(n^{log_b a}logn))
  3. (f(n))(n^{log_ba}) 的高阶函数,且高出一个多项式,(f(n)=Omega(n^{log_b a+epsilon}), epsilon>0),而且存在常数 (c<1) 使得 (af(n/b)<cf(n)) 对充分大的 (n) 成立,那么 (T(n)=Theta(f(n)))

5. 空间复杂度

  • 空间复杂度表征程序占用内存随着数据规模的变化趋势,分析程序中数据的分配空间即可,一般常见的复杂度有 (O(1))(O(n))(O(n^2))

参考资料-极客时间专栏《数据结构与算法之美》

获取更多精彩,请关注「seniusen」!

原文地址:https://www.cnblogs.com/seniusen/p/11979758.html