算法学习:时间复杂度和数组

280 阅读3分钟

这是我参与更文挑战的第 12 天,活动详情查看: 更文挑战

复杂度量级(按数量级递增),大O表示法 T(n)=O(f(n)){T{_{}(n) = O(f(n))}}

  • 常量阶 O(1){O(1)} 指数阶 O(2n){O(2^n)}
  • 对数阶 O(logn){O(log n)} 阶乘阶 O(n!){O(n!)}
  • 线性阶 O(n){O(n)}
  • 线性对数阶 O(nlogn){O(n logn)}
  • 平方阶 O(n2){O(n^2)},立方阶 O(n3){O(n^3)}…K 次方阶 O(nk){O(n^k)}

复杂度量级可以粗略的分为多项式量级非多项式量级。非多项式量级只有两个 O(2n){O(2^n)}O(n!){O(n!)}。当数据规模 n 非多项式量级的算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以非多项式时间复杂度其实是效率非常低的算法

对数时间复杂度最难分析:O(logn){O(logn)}O(nlogn){O(nlogn)},归并排序,快速排序时间复杂度都是O(nlogn){O(nlogn)}

如下代码:

 i=1;
 while (i <= n)  {
   i = i * 2;
 }

2x=n{2^{x} = n} 求解得出 x=log2n{ x = log_2n},所以这段代码的时间复杂度为 O(log2n){O(log_2n)},不管以 2,3 还是 10 为底,我们都可以吧所有对数时间复杂度都记为 O(logn){O(logn)}

因为对数之间是可以互相转换的,log3n{log_3n} 就等于 log32log2n{log_32 * log_2n},所以 O(log3n)=O(Clog2n){O(log_3n) = O(C * log_2n)},其中 C=log32{ C=log_32 } 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n))=O(f(n)){O(Cf(n)) = O(f(n))}。所以,O(log2n){O(log_2n)} 就等于O(log3n){ O(log_3n)}。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn){O(logn)}

image-20201223112248762.png

最好情况,最坏情况,平均情况,均摊时间复杂度

平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度

均摊时间复杂度,以及它对应的分析方法,摊还分析(或者叫平摊分析)

平均复杂度只在某些特殊情况下才会用到,而均摊时间复杂度应用的场景比它更加特殊、更加有限。

对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度

均摊就是一种特殊的平均时间复杂度算法。

数组

链表适合插入、删除,时间复杂度 O(1),数组支持随机访问,根据下标随机访问的时间复杂度为 O(1),而不是说数组适合查找,查找时间复杂度为 O(1)。

删除数组中的某个数据:可以先进行标记,最后再统一删除,也就是标记-清除算法

从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0]就是偏移为 0 的位置,也就是首地址,a[k]就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:

a[k]_address = base_address + k * type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:

a[k]_address = base_address + (k-1)*type_size

对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。更多原因可能是历史原因,C 语言设计者用 0 开始计数数组的下标。

二维数组内存寻址:

对于 m * n 的数组,a [ i ][ j ] (i < m,j < n)的地址为:

address = base_address + ( i * n + j) * type_size