数据结构——算法与复杂度

1,326 阅读5分钟

零、前言

📕欢迎访问

个人主页:conqueror712.github.io/

Github主页:github.com/Conqueror71…

笔者注

本文,乃至本系列是我用于辅助自己学习的产物,难免会有些地方写的不如书本上那么精确,更多是对于知识结构的梳理以及自己的理解,毕竟不是要做一个“电子版的书本”,不会面面俱到。不过也请感兴趣的读者们放心,省略掉的重要内容,文中都会有标识,如今无论是搜索引擎还是语言模型,想必都可以很快地解决疑惑,感兴趣的话可以自行查找。

另外,我会将我自己梳理的知识结构图附上,以便大家能更加一目了然地理解知识点之间存在的关系,在这其中可能会有一些理解不到位,或者错误的内容,烦请大家指出,这样一来也能更好地帮助其他有需要的人。关于每章节的课后习题,我打算在有需要的时候做成视频讲解放在 Bilibili 上,供有需要的读者朋友们参考。本文对应视频:✍️相关练习讲解视频

2.png

一、算法

既然我们常说 DSA(Data Structure + Algorithm = Program),那么,什么是算法?

  • 是指基于特定的计算模型,旨在解决某一信息处理问题而设计的一个指令序列。

算法需要具备哪些要素?

  • 输入与输出:不解释
  • 基本操作:字面意思,不解释
  • 确定性和可行性:算法应该可以被描述为由若干语义明确的基本操作组成的指令序列,且每一基本操作在对应的计算模型中均可实现
  • 有穷性和正确性:任意算法都应在执行有限次基本操作之后终止,并给出能够符合给定问题下的正确输出
  • 退化与鲁棒性:实用的算法应能够处理各种极端的输入实例,这里的极端情况就是所谓退化的情况,而算法为了能够尽可能充分地应对此类情况所具有的性质被称为鲁棒性
  • 重用性:算法的总体框架能否便捷地推广至其他场合

算法效率涉及哪几个方面呢?

  • 可计算性:顾名思义,例如无休无止的程序就不具有可计算性
  • 难解性:有些算法虽然满足有穷性,但是实际上花费的时间开销太大,这也属于难解性问题
  • 计算效率:主要从时间和空间这两个层面来衡量计算效率
  • 数据结构:合理的数据结构可以帮助设计出更加优秀的程序

二、复杂度

  • 时间复杂度

    从保守估计的角度出发,在规模为 n 的所有输入中选择执行时间最长者作为 T(n),并以 T(n) 度量该算法的时间复杂度

  • 空间复杂度

    与时间复杂度类似地,算法所需存储空间的多少用空间复杂度来衡量,另外,时间复杂度本身就是空间复杂度的一个天然的上界

  • 渐进复杂度

    渐进复杂度是渐进分析的产物,这是一种着眼长远、更为注重时间复杂度的总体变化趋势和增长速度的策略。为此,我们将引入三种记号:

    • O 记号:作为 T(n) 的渐进上界,具体定义如下:

      若存在常数 cc 及函数 f(n)f(n)

      s.t.s.t. 对于  n>>2∀\ n>>2 都有 T(n)cf(n)T(n) ≤ c·f(n),(此处的 >>>> 为远大于的意思)

      则可认为,在 nn 足够大之后,f(n)f(n) 给出了 T(n)T(n) 增长速度的一个渐进上界

      此时,记为 T(n)=O(f(n))T(n)=O(f(n))

      另外还可得到两个性质:

      1. 忽略常数:对于 常数 c>0c>0,都有 O(f(n))=O(cf(n))O(f(n))=O(c·f(n))
      2. 抓大头:对于 常数 a>b>0a>b>0,都有 O(na+nb)=O(na)O(n^a+n^b)=O(n^a)
    • Ω 记号:作为 T(n) 的渐进下届,具体定义如下:

      若存在常数 cc 及函数 g(n)g(n)

      s.t.s.t. 对于  n>>2∀\ n>>2 都有 T(n)cg(n)T(n) ≥ c·g(n)

      则可认为,在 nn 足够大之后,g(n)g(n) 给出了 T(n)T(n) 增长速度的一个渐进下界

      此时,记为 T(n)=Ω(g(n))T(n)=Ω(g(n))

    • Θ 记号:作为 T(n) 的渐进确界,是对算法复杂度的准确估计——对于规模为 n 的任何输入,算法的运行时间 T(n) 都与 Θ(h(n)) 同阶,具体定义如下:

      若恰好 f(n)=g(n)f(n)=g(n),若存在正常数 c1<c2c_1<c_2 和函数 h(n)h(n)

      s.t.s.t. 对于  n>>2∀\ n>>2 都有 c1h(n)T(n)c2h(n)c_1·h(n) ≤ T(n) ≤ c_2·h(n)

      则可认为,在 nn 足够大之后,h(n)h(n) 给出了 T(n)T(n) 增长速度的一个渐进确界

      此时,记为 T(n)=Θ(h(n))T(n)=Θ(h(n))

    我们可以用一个图来直观的感受这三种记号之间的关联:

1.png

  • 复杂度分析

    典型的复杂度层次:

    • O(1)O(1)
    • O(loglogn)O(\log\log n)
    • O(logn)O(\log n)
    • O(n)O(\sqrt{n})
    • O(n)O(n)
    • O(nloglogn)O(n\log\log n)
    • O(nlogn)O(n\log n)
    • O(n2)O(n^2)
    • O(n3)O(n^3)
    • O(2n)O(2^n)

3.png

  • 输入规模

    严格地说,所谓待计算问题的输入规模,应严格定义为用以描述输入所需的空间规模

    比如,若要统计任意 nN+n∈N_+ 的二进制展开中 11 的个数,显然是通过遍历来统计的,通常我们会默认用 nn 的大小作为输入的规模,从而得到 O(n)O(n) 的复杂度,但这是错误的,我们称之为伪复杂度(本例中就是伪线性复杂度)。

    应当用 nn 的二进制展开的位数 r=1+log2nr=1+\lfloor \log_2n\rfloor 作为输入的规模,从而得到正确的 O(r)O(r) 复杂度。

补充概念:数据集合及其对应的操作可超脱于具体的程序设计语言、具体的实现方式,即构成所谓的抽象数据类型(Abstract Data Type, ADT)。


✍️相关练习讲解视频

Question 1: 试证明,在用对数函数界定渐进复杂度时,常底数的取值无所谓。

证明:

设,某函数的上界可以表示为 f(n)=O(logan), (a>1,a 为常数)f(n)=O(\log_an),\ (a>1, a\ 为常数)

f(n)=O(logan)=O(lnalnn)=O(lnblna×lnnlnb)=O(lnblna×logbn)=O(logbn)f(n)=O(\log_an)=O(\frac{\ln a}{\ln n})=O(\frac{\ln b}{\ln a}\times\frac{\ln n}{\ln b})=O(\frac{\ln b}{\ln a}\times\log_bn)=O(\log_bn)

O(logan)=O(logbn), O(\log_an)=O(\log_bn),\ 证毕


Question 2: 试证明,对于 ε>0∀ε>0,都有 logn=O(nε)\log n=O(n^ε)

证明:

直观地,我们知道 lnx\ln x 的增长速度是慢于 xx

于是有 ε>0, M>0 s.t. n>M 时, 有 lnn<εn ∀ε>0,\ ∃M>0\ s.t.\ n>M\ 时,\ 有\ \ln n<εn\

N=eM,N=e^M,n>N,n>N,lnn>M\ln n > M(利用 lnx\ln x 的单调性)

总有 ln(lnn)<ε(lnn)\ln(\ln n)<ε(\ln n)(利用 ① 式)

同时取 ee 为底数,得 eln(lnn)<eεlnne^{\ln(\ln n)}<e^{ε\ln n},即 lnn<nε\ln n<n^ε,则对于 ε>0∀ε>0logn=O(nε)\log n=O(n^ε),证毕


Question 3: 若 f(n)=O(n2)g(n)=O(n)f(n)=O(n^2)且g(n)=O(n),试证明 f(n)×g(n)=O(n3)f(n)\times g(n)=O(n^3)

证明:

由大 O 记号定义得,

c1>0, N1>0, s.t. n>N1时总有f(n)<c1n2∃c_1>0,\ N_1>0,\ s.t.\ n>N_1时总有f(n)<c_1n^2

c2>0, N2>0, s.t. n>N2时总有g(n)<c2n∃c_2>0,\ N_2>0,\ s.t.\ n>N_2时总有g(n)<c_2n

c=c1c2c=c_1c_2N=max(N1,N2)N=max(N_1, N_2)

则当 n>Nn>N 时,总有 f(n)×g(n)<cn3=O(n3)f(n)\times g(n)<cn^3=O(n^3)