数据结构(c++)学习笔记--绪论

269 阅读4分钟

一、计算

1.算法

1.1 计算 = 信息处理

  • 借助某种工具,遵照一定规则,以明确而机械的形式进行

1.2 计算模型 = 计算机 = 信息处理工具

1.3 所谓算法,即特定计算模型下,旨在解决特定问题的指令序列

  • 输入-待处理的信息(问题)

  • 输出-经处理的信息(答案)

  • 正确性-的确可以解决指定的问题

  • 确定性-可描述为一个由基本操作组成的序列 (如加盐少许,加糖适量,煮至半熟... )

  • 可行性-每一基本操作都可实现,且在常数时间内完成 (如把大象放进冰箱,有三步...)

  • 有穷性-对于任何输入,经有穷次基本操作,都可以得到输出

2.有穷性

2.1 希尔顿序列 (Hailstone Sequence)

  • Hailstonen={{1}n1{n}UHailstone(n/2)n为偶数{n}UHailstone(3n+1)n为奇数 Hailstone(n)=\begin{cases} \{1\} & n≤1 \\ \{n\} U Hailstone(n/2) & n为偶数 \\ \{n\} U Hailstone(3n+1) & n为奇数 \end{cases}

  • 如Hailstone(42)={42,21, 64, 32, ..., 1}

  • 总趋势是下降的

2.2 代码

int hailstone(int n){
	int length = 1; 
	while (1 < n) { n % 2 ? n = 3 * n + 1 : n /= 2; length++; } 
	return length; 
}

2.3 但对于希尔顿序列是否全部有穷,仍然未知,所以希尔顿序列不是算法

3.正确的算法

3.1 符合语法,能够编译、链接

  • 能够正确处理简单的输入

  • 能够正确处理大规模的输入

  • 能够正确处理一般性的输入

  • 能够正确处理退化的输入

  • 能够正确处理任意合法的输入

3.2 健壮

  • 能辨别不合法的输入并做适当处理,而不致非正常退出

3.3 可读

  • 结构化 + 准确命名 + 注释 + ...

3.4 效率

  • 速度尽可能快,存储空间尽可能少

二、计算模型

1.统一尺度

1.1 TA(P) = 算法A求解问题实例P的计算成本

1.2 同一问题通常有多种算法,实验统计时评估各类算法优劣最直接的方式,但不足以准确反映算法的真正效率

  • 不同的算法,可能更适应于不同类型的输入

  • 同一算法,可能由不同程序员、用不同程序语言、经不同编译器生成

  • 同一算法,可能实现并运行于不同的体系结构、操作系统.. 1.3 为了给出客观的评判,需要抽象出一个理想的平台或模型

2.图灵机

2.1 构成部件

  • Tape-依次均匀地划分为单元格,各存有某一字符,初始均为'#

  • Alphabet-字符的种类有限

  • Head

    • 总是对准某一单元格,并可读取或改写其中的字符
    • 每经过一个节拍,可转向左侧或右侧的邻格
  • State

    • TM总是处于有限种状态中的某一种
    • 每经过一个节拍可按照规则转向另一种状态
    • 统一约定,'h' = hal pSKOM38.png

2.2 转换函数

  • Transition Function:(q, c; d, L / R, p)
    • 若当前状态为q,且当前字符为c,则将当前字符改写为d,转向左 / 右侧邻格, 转入'p'状态

    • 特别地,一旦转入约定的状态'h',则停机

    • 从启动至停机,所经历的节拍数目,即可用以度量计算的成本,亦等于Head累计的移动次数(无量纲)

2.3 实例(Increase)

  • 功能:将二进制非负整数加一

  • 原理:全'1'的后缀,翻转为全'0',原最低位'0'或'#'翻转为'1'

  • 代码

    • (<, 1; 0, L, <) -- 左行,1->0

    • (<, 0; 1, R, >) -- 掉头,0->1

    • (<, #; 1, R, >)-- 遇到其他状况,该语句不执行

    • (>, 0; 0, R, >) -- 右行

    • (>, #; #, L, h/<) //遇到其他状况停止

  • 效果 pSKjst1.png

3.RAM

3.1 组成

  • 寄存器顺序编号,总数没有限制

  • 可通过编号直接访问任意寄存器

  • 每一基本操作仅需常数时(循环及子程序本身非基本操作) pSKjTht.png

3.2 语言

  • 赋值操作
    • R\[i]<-c

    • R\[i]<-R\[j]

    • R\[i]<-R\[R\[j]]

    • R\[R\[i]]<-R\[j]

    • R\[i]<-R\[j]+R\[k]

    • R\[i]<-R\[j]-R\[k]

  • 条件语句
    • IF R\[i]=0 GOTO 1 (如果R[i]=0则跳往语句1)

    • IF R\[i]>0 GOTO 1

    • STOP

3.3 效率

  • 与TM模型一样,RAM模型也是一般计算工具的简化与抽象,使我们可以独立于具体的平台,对算法的效率做出可信的比较与评判

  • 算法的运行时间 = 算法需要执行的基本操作次数

  • T(n) = 算法为求解规模为n的问题,所需执行的基本操作次

3.4 实例(Ceiling Division

  • 功能:向下取整的除法,0 <= c,0 < d
    c/d=max{xdx<=c}=max{xdx<1+c}\lceil c/d \rceil=max\{x|d*x<=c\}=max\{x|d*x<1+c\}

  • 算法:反复地从 R[0] = c 中,减去 R[1] = d,统计在下溢之前,所做减法的次数x

  • 代码

    • [0] R[3] <-1

    • [1] GOTO 4

    • [2] R[2] <- R[2] + R[3]

    • [3] R[0] <- R[0] – R[1]

    • [4] IF R[0] > 0 GOTO 2

    • [5] R[0] <-R[2]

    • [6] STOP

  • 效果

    pSKvIrF.png

  • 表的行数即所执行基本指令的总条数

三、渐进复杂度

1.大O记号

1.1 渐进分析

  • 基本操作次数 T(n)

  • 存储单元数 S(n)

1.2 Big-O notation

  • T(n)=O(f(n)) iff ∃c>0 当n>>2时,有T(n)<c*f(n)

  • 实例:5n[3n(n+2)+4]+6<5n[6n2+4]+6<35n3+6<6n3=On3\sqrt{5n·[3n·(n+2)+4]+6}<\sqrt{5n·[6n^2+4]+6}<\sqrt{35n^3+6}<6\sqrt{n^3}=O(\sqrt{n^3})(当常数趋近于n时)

  • 与T(n)相比,f(n)在形式上更为简洁,但依然反映前者的增长趋势

pSQZxT1.png

1.3 T(n)=Ω(f(n)) iff ∃c>0 当n>>2时,有T(n)<c*f(n)

1.4 T(n)=Θ(f(n)) iff ∃c1c_1>c2c_2>0 当n>>2时,有**c1c_1·f(n)>T(n)>c2c_2·f(n)**

pSQmMU1.png

2.多项式

2.1 常数O(1)

  • 从渐近的角度来看,再大的常数,也要小于递增的变数

  • 实例:2 = 2022 = 2022 x 2022 = O(1),即202220222022^{2022} = O(1)

  • 这类算法的效率最高,即不含转向(循环,调用,递归等),必顺序执行

//循环
for(i=0;i<n;i+=n/2013+1)
for(i=1;i<n;i=1<<i)
//分支转向
if((n+m)*(n+m)<48n8m) goto UNREACHABLE
//(递归)调用
if(2==(n*n)%5) O1(n)

2.2 对数logbn\log_{}^{b} n

  • 不注明底数原因
    • ∀a,b>1,logan\log_a n=logab\log_a b·logbn\log_b n=O(logbn\log_b n)

    • ∀c>0,lognc\log n^c=c·logn\log n=O(logn\log n)

    • 123·log321n+log205(7n215n+3)\log_{}^{321} n + log_{}^{205} (7·n^2-15·n+3)=O(log321n\log_{}^{321} n) - 这类算法非常有效,复杂度无限接近于常数:∀c>0,logn\log n=O(ncn^c)

2.3 多项式(O(ncn^c))

  • aknk+ak1nk1+...+a2n2+a1n+a0=O(nK),ak>0a_k·n^k+a_{k-1}·n^{k-1}+...+a_2·n^2+a_1·n+a_0=O(n^K),a_k>0
  • 即只取最大的次方,其他皆可无视

2.4 线

  • 指所有O(n)类函数

3.指数(O(2n2^n))

3.1 T(n)=O(an),a>1,c>1,nc=O(2n)T(n)=O(a^n),a>1,即∀c>1,n^c=O(2^n)

3.2 实例

  • n1000...01=O(1.0000.01n)=O(2n)n^{1000...01}=O(1.0000.01^n)=O(2^n)
  • 1.00...01n=Ω(n1000...01)1.00...01^n=Ω(n^{1000...01})

3.3 从O(nc)O(2n)O(n^c)到O(2^n),是从有效算法无效算法的分水岭

4.层次分级

pSQnuQS.png

四、复杂度分析

1.级数

1.1 级数

  • 算术级数:与末项平方同阶 T(n)=1+2+...+n=n(n+1)/2=O(n2)T(n)=1+2+...+n=n(n+1)/2=O(n^2)

  • 幂方级数:比幂次高出一阶 T(n)=12+22+...+n2=n(n+1)(2n+1)/2=O(n3)T(n)=1^2+2^2+...+n^2=n(n+1)(2n+1)/2=O(n^3)

  • 几何级数:与末项同阶 Ta(n)=a0+a1+a2+...+an=O(an)T_a(n)=a^0+a^1+a^2+...+a^n=O(a^n),1<a$

1.2 收敛级数

k=2n1(k1)k=121+123+134+...+1(n1)n=11n=O(1)\sum_{k=2}^{n}\frac{1}{(k-1)·k}=\frac{1}{2·1}+\frac{1}{2·3}+\frac{1}{3·4}+...+\frac{1}{(n-1)·n}=1-\frac{1}{n}=O(1)

kIsAPerfectPowern1k1=13+17+18+115+...=1=O(1)\sum_{k Is A Perfect Power}^{n}\frac{1}{k-1}=\frac{1}{3}+\frac{1}{7}+\frac{1}{8}+\frac{1}{15}+...=1=O(1)

  • 几何分布:1λ[1+2λ+3λ2+4λ3+...]=1/(1λ)=O(1)(1-λ)[1+2λ+3λ^2+4λ^3+...]=1/(1-λ)=O(1),0<λ<1

1.3 不收敛,但有限

  • 调和级数:h(n)=k=1n1k=1+12+13+...+1n=lnη+γ+O(12n)=Θ(logn)\sum_{k=1}^{n}\frac{1}{k}=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=lnη+γ+O(\frac{1}{2n})=Θ(\log n)
  • 对数级数:k=1nlnk=lnk=1nk=lnn!(n+0.5)lnnn=Θ(nlogn)\sum_{k=1}^{n}\ln k=\ln\prod_{k=1}^nk=\ln n!≈(n+0.5)·\ln n-n=Θ(n·\log n)
  • 对数 + 线性 + 指数:k=1nklogk1xlnxdx=[x2(2lnx1)4]1n=Θ(n2logn)\sum_{k=1}^{n}k·\log k≈\int_1^\infty x·\ln xdx=[\frac{x^2·(2·\ln x-1)}{4}]_1^n=Θ(n^2·\log n)

2.迭代

2.1 迭代 + 算术级数

(该代码执行时间与图形面积相等)

for( int i = 0; i < n; i++ ) 
for( int j = 0; j < n; j++ ) 
O1op(const i, const j)

k=1nn=nn=O(n2)\sum_{k=1}^{n}n=n·n=O(n^2)

pSlKs6f.png

for( int i = 0; i < n; i++ ) 
for( int j = 0; j < i; j++ ) 
O1op(const i, const j)

k=1nn=n(n1)2=O(n2)\sum_{k=1}^{n}n=\frac{n·(n-1)}{2}=O(n^2)

pSlKocV.png

2.2 迭代 vs 级数

for( int i = 1; i < n; i <<= 1 ) (<<为在二进制中向左移动一位,即乘2)
for( int j = 0; j < i; j++ ) 
O1op( const i, const j )

1+2+4+...+2log2(n1)=2log2n1=O(n)1+2+4+...+2^{\log_2 (n-1)}=2^{\log_2 n}-1=O(n)

pSlKHnU.png

for( int i = 0; i < n; i++ ) 
for( int j = 0; j < i; j += 2022 ) 
O1op( const i, const j )

pSlKjhR.png

2.3 迭代 + 复杂级数

for( int i = 0; i <= n; i++ ) 
for( int j = 1; j < i; j += j ) 
O1op( const i, const j )

T(n)=i=0nlog2i=O(nlogn)T(n)=\sum_{i=0}^{n}\lceil\log_2 i \rceil=O(n\log n)

pSlMPBD.png

3.封底估算

  • 1天=24hr x 60min x 60sec≈25 x 4000=10510^5sec
  • 一生=一世纪=100yr x 365=3 x 10410^4day=3 x 10910^9sec

五、迭代与递归

1.减而治之

1.1 为求解一个大规模的问题,可以

  • 将其划分为两个子问题:其一平凡,另一规模缩减

  • 分别求解子问题;再由子问题的解,得到原问题的解 pS3PPWF.png

1.2 递归跟踪:绘出计算过程中出现过的所有递归实例(及其调用关系)

  • 它们各自所需时间之总和,即为整体运行时间

1.3 实例

sum( int A[], int n ) { 
    return n < 1 ? 0 : sum(A, n - 1) + A[n - 1];
}

总体运行时间为T(n)=O(1) x (n+1)=O(n) 1.4 对于大规模的问题、复杂的递归算法,递归跟踪不再适用此时可采用另一抽象的方法

  • 从递推的角度看,为求解规模为n的问题sum(A, n)\underline{\text{sum(A, n)}}(T(n)),需递归求解规模为n-1的问题sum(A, n-1)\underline{\text{sum(A, n-1)}}(T(n-1)),再累加上A[n - 1]\underline{\text{A[n - 1]}}(O(1))
    • 递推方程:T(n)=T(n-1)+O(1),T(0)=O(1)
    • 解:T(n)=T(n+2)+O(2)=T(n-3)+O(3)=...=T(0)+O(n)=O(n)
  • void reverse( int * A, int lo, int hi )(将数组中的区间A[lo,hi]前后颠倒)
 //减治
Rev(lo,hi)=[hi]+Rev(lo+1,hi-1)=[lo]

 //递归:
if (lo < hi) {
         swap( A[lo], A[hi] ); 
         reverse( A, lo + 1, hi – 1 );
 }//线性递归(尾递归),O(n)
 
 //迭代
 while (lo < hi) swap( A[lo++], A[hi--] ); //亦是O(n)

pS3iQA0.png

2.分而治之

2.1 为求解一个大规模的问题,可以将其划分为若干子问题 (通常两个,且规模大体相当),分别求解子问题,由子问题的解,合并得到原问题的解 pS3iaH1.png

T(n)=各层递归实例所需时间之和(递归跟踪)=O(1)x(20+21+22+...+2logb=n)=O(1)x(21+logn1)=O(n)\begin{aligned} T(n)&=各层递归实例所需时间之和(递归跟踪)\\ &=O(1) x (2^0+2^1+2^2+...+2^{\log b=n}) \\ &=O(1) x (2^{1+\log n}-1) \\ &=O(n) \end{aligned}

2.2 实例

sum( int A[], int lo, int hi ) { 
    if ( hi - lo < 2 ) return A[lo]; 
        int mi = (lo + hi) >> 1; return sum( A, lo, mi ) + sum( A, mi, hi ); 
} 

2.3 从递推的角度看,为求解sum(A, lo, hi),需要递归求解sum(A, lo, mi)\underline{\text{sum(A, lo, mi)}}sum(A, mi+1, hi)\underline{\text{sum(A, mi+1, hi)}}(2*T(n/2)),进而将子问题的解累加(O(1))

  • 递推方程:
    • T(n)=2·T(n/2)+O(1)
    • T(1)=O(1)
  • 解:T(n)=4·T(n/4)+O(3)=8·T(n/8)+O(7)=16·T(n/16)+O(15)=...=n·T(1)+O(n-1)=O(n)

2.4 Master Theorem

  • 分治策略对应的递推式,通常(尽管不总是)形如:T(n)=a·T(n/b)+O(f(n))(原问题被分为a个规模均为n/b的子任务;任务的划分、解的合并总共耗时f(n))

  • f(n)=O(nlogbaaε),T(n)=Θ(nlogbaa)f(n)=O(n^{\log_{b}^{a}a-ε}),则T(n)=Θ(n^{\log_{b}^{a}a})

    • 实例:T(n)=2T(n/4)+O(1)=O(n)T(n)=2·T(n/4)+O(1)=O(\sqrt{n})
  • f(n)=O(nlogbaalogkn),T(n)=Θ(nlogbaalogk+1n)f(n)=O(n^{\log_{b}^{a}a}·\log_{}^{k}n),则T(n)=Θ(n^{\log_{b}^{a}a}·\log_{}^{k+1}n)

    • 实例:T(n)=1T(n/2)+O(1)=O(logn)T(n)=1·T(n/2)+O(1)=O(\log n)
    • T(n)=2T(n/2)+O(n)=O(nlogn)T(n)=2·T(n/2)+O(n)=O(n·\log n)
    • T(n)=2T(n/2)+O(nlogn)=O(nlog2n)T(n)=2·T(n/2)+O(n·\log n)=O(n·\log_{}^{2} n)
  • f(n)=Ω(nlogbaa+ε),T(n)=Θ(f(n))f(n)=Ω(n^{\log_{b}^{a}a+ε}),则T(n)=Θ(f(n))

    • 实例:T(n)=1T(n/2)+O(n)=O(n)T(n)=1·T(n/2)+O(n)=O(n)

六、动态规划

1.记忆法-fib()

1.1 fib(n)=fib(n-1)+fib(n-2)

pS3ErdA.png

int fib(n) { 
    return (2 > n) ? n : fib(n - 1) + fib(n - 2); 
}

1.2 复杂度:

T(0)=T(1)=1;T(n)=T(n-1)+T(n-2)+1,∀n>1;

令S(n)=[T(n)+1]/2,则S(0)=1=fib(1),S(1)=1=fib(2)

故S(n)=S(n-1)+S(n-2)=fib(n+1),T(n)=2·S(n)-1=2·fib(n+1)-1=O(fib(n+1))=O(ϕ2ϕ^2)(ϕ=(1+5\sqrt{5})/2≈1.618->黄金分割率)

1.3 封底估算

  • ϕ36225,ϕ43230109flo=1secϕ^{36}≈2^{25},ϕ^{43}≈2^{30}≈10^9flo=1sec
  • ϕ510,ϕ671014flo=105sec1dayϕ^5≈10,ϕ^{67}≈10^{14}flo=10^5sec≈1day
  • ϕ92109flo=1010sec105day3centuryϕ^{92}≈10^9flo=10^{10}sec≈10^5day≈3century

1.4 递归版fib()低效的根源在于,各递归实例均被大量地重复调用。先后出现的递归实例,共计O(ϕnϕ^n)个;而去除重复之后,总共不过O(n)种

O(ϕnϕ^n)O(n)
pS3ExeJ.pngpS3V9F1.png

1.5 动态规划

  • 颠倒计算方向: 由自顶而下递归,改为自底而上迭代
f = 1; g = 0; //fib(-1), fib(0) 
while ( 0 < n-- ) { 
    g = g + f; f = g - f; 
} 
return g;

pS3VkQO.png

此时T(n) = O(n),而且仅需O(1)空间

2.最长公共子序列(LCS)

2.1 定义

  • 子序列(Subsequence):由序列中若干字符,按原相对次序构成 pS3VaYq.png

  • 最长公共子序列(Longest Common Subsequence):两个序列公共子序列中的最长 pS3Vc79.png

2.2 **递归 ** 对于序列A[0,n)和B[0,m),LCS(n,m)无非三种情况

  • 若 n = 0 或 m = 0,则取作空序列(长度为零)

  • 若A[n-1] = 'X' = B[m-1],则取作:LCS(n-1,m-1) + 'X' [pS3V70H.png

  • A[n-1]≠ B[m-1],则在 LCS(n,m-1) 与 LCS(n-1,m) 中取更长者 pS3VLtI.png

unsigned int lcs( char const * A, int n, char const * B, int m ) { 
    if (n < 1 || m < 1) return 0; 
    else if ( A[n-1] == B[m-1] ) return 1 + lcs(A, n-1, B, m-1); 
    else return max( lcs(A, n-1, B, m), lcs(A, n, B, m-1) )
}

2.3 复杂度:每经一次比对,至少一个序列的长度缩短一个单位

  • 最好情况,只需O(n+m)时间

  • 然而最坏情况下,子问题数量不仅会增加,且可能大量雷同子任务LCS(A[a],B[b])重复的次数,可能多达为(n+mabna)=(n+mabmb)\binom{n+m-a-b}{n-a}=\binom{n+m-a-b}{m-b};特别地,LCS(A[0], B[0])的次数可多达(n+mn)=(n+mm)\binom{n+m}{n}=\binom{n+m}{m}

  • 当n = m时,为Ω(2n2^n)

unsigned int lcsMemo(char const* A, int n, char const* B, int m) { 
    unsigned int * lcs = new unsigned int[n*m]; 
    memset(lcs, 0xFF, sizeof(unsigned int)*n*m); 
    unsigned int solu = lcsM(A, n, B, m, lcs, m); 
    delete[] lcs; 
    return solu;
}

unsigned int lcsM( char const * A, int n, char const * B, int m, unsigned int * const lcs, int const M ) { 
    if (n < 1 || m < 1) return 0;
    if (UINT_MAX != lcs[(n-1)*M + m-1]) return lcs[(n-1)*M + m-1]; 
    else return lcs[(n-1)*M + m-1] = (A[n-1] == B[m-1]) ? 1 + lcsM(A, n-1, B, m-1, lcs, M) max( lcsM(A, n-1, B, m, lcs, M), lcsM(A, n, B, m-1, lcs, M) );
}

2.4 动态规划

  • 采用动态规划的策略 只需O(n+m)时间即可计算出所有子问题

  • 将所有子问题(假想地)列成一张表,颠倒计算方向,从LCS(0,0)出发,依次计算出所有项——直至LCS(n,m)
    pS3ZICq.png

unsigned int lcs(char const * A, int n, char const * B, int m) { 
    if (n < m) { swap(A, B); swap(n, m); } 
    unsigned int* lcs1 = new unsigned int[m+1]; 
    unsigned int* lcs2 = new unsigned int[m+1]; 
    memset(lcs1, 0x00, sizeof(unsigned int) * (m+1)); 
    memset(lcs2, 0x00, sizeof(unsigned int) * (m+1)); 
    for (int i = 0; i < n; swap(lcs1, lcs2), i++) 
        for (int j = 0; j < m; j++) 
            lcs2[j+1] = (A[i] == B[j]) ? 1 + lcs1[j] : max(lcs2[j], lcs1[j+1]); 
    unsigned int solu = lcs1[m]; 
    delete[] lcs1; 
    delete[] lcs2; 
    return solu;
}