【数据结构与算法】自学笔记

复杂度分析

O O O记号

在问题规模足够大后,计算成本如何增长?(更侧重整体增长趋势)

T(n):需执行的基本操作次数
T(n)= O O O( f(n) )
if ∃ \exists c > 0 , 当 n >> 2 后 , 有T(n) < c ⋅ \cdot f(n)

例:T(n)= 5 n ⋅ [ 3 n ⋅ ( n + 2 ) + 4 ] + 6 \sqrt{5n\cdot[3n\cdot(n+2)+4]+6} 5n[3n(n+2)+4]+6 对其简化

5 n ⋅ [ 3 n ⋅ ( n + 2 ) + 4 ] + 6 \quad\sqrt{5n\cdot[3n\cdot(n+2)+4]+6} 5n[3n(n+2)+4]+6 \quad // 2–>n

\quad < 5 n ⋅ [ 6 n 2 + 4 ] + 6 \sqrt{5n\cdot[6n^2+4]+6} 5n[6n2+4]+6 \quad \quad \quad \quad // 4–> n 2 n^2 n2

\quad < 35 n 3 + 6 \sqrt{35n^3+6} 35n3+6 \quad \quad \quad \quad \quad \quad \quad \quad // 6–> n 3 n^3 n3

\quad 6 ⋅ \cdot n 1.5 n^{1.5} n1.5 = O O O ( n 1.5 n^{1.5} n1.5)

与T(n)相比,f(n)更为简洁,但依然反应前者的增长趋势

  • 常系数可忽略: O O O( f(n) ) = O O O( c × \times ×f(n) )
  • 低次项可忽略: O O O( n a + n b n^a+n^b na+nb ) = O O O( n a n^a na) , a > b > 0

Ω \Omega Ω记号和大 Θ \Theta Θ记号

  • T(n) = Ω \Omega Ω( f(n) ) :
    ∃ \exists c > 0 , 当 n >> 2 后 , 有T(n) > c ⋅ \cdot f(n)
  • T(n) = Θ \Theta Θ( f(n) ) :
    ∃ \exists c 1 c_1 c1 > c 2 c_2 c2 > 0 , 当 n >> 2 后 , 有 c 1 c_1 c1 ⋅ \cdot f(n) > T(n) > c 2 c_2 c2 ⋅ \cdot f(n)

常见的 O O O()

O O O(1)

常数
      2 = 2013 = 2013 × \times × 2013 = O O O(1) , 甚至 201 3 2013 2013^{2013} 20132013 = O O O(1)

这类算法的效率最高

O O O( l o g c n log^cn logcn)

对数
      lnn  |  lgn  |   l o g 100 n log_{100}n log100n  |   l o g 2013 n log_{2013}n log2013n

常底数无所谓
       ∀ \forall a , b > 0 , l o g a n log_a n logan = l o g a b log_ab logab ⋅ \cdot l o g b n log_bn logbn = Θ \Theta Θ( l o g b n log_bn logbn)

常数次幂无所谓
       ∀ \forall c > 0 , l o g n c logn^c lognc = c ⋅ \cdot logn = Θ \Theta Θ(log n)

对数多项式
      123* l o g 321 n log^{321}n log321n + l o g 105 ( n 2 − n + 1 ) log^{105}(n^2 - n +1) log105(n2n+1) = Θ \Theta Θ( l o g 105 n log^{105}n log105n)

这类算法非常有效,复杂度无限接近于常数
       ∀ \forall c > 0 , logn = O O O( n c n^c nc)

O O O( n c n^c nc)

一般的: a k n k + a k − 1 n k − 1 + . . . + a 1 n + a 0 = O ( n k ) , a k > 0 a_kn^k +a_{k-1}n^{k-1}+...+a_1n+a_0=O(n^k) , a_k>0 aknk+ak1nk1+...+a1n+a0=O(nk),ak>0

很明显,这是一种多项式的形式
例如:
100 n + 200 = O ( n ) 100n+200=O(n) 100n+200=O(n)
( 100 n − 500 ) ( 20 n 2 − 300 n + 2013 ) = O ( n × n 2 ) = O ( n 3 ) (100n-500)(20n^2-300n+2013)=O(n\times n^2)=O(n^3) (100n500)(20n2300n+2013)=O(n×n2)=O(n3)
( 2013 n 2 − 20 ) / ( 1999 n − 1 ) = O ( n 2 / n ) = O ( n ) (2013n^2-20)/(1999n-1)=O(n^2/n)=O(n) (2013n220)/(1999n1)=O(n2/n)=O(n)
[ ( n 2013 − 24 n 2009 ) 1 / 3 + 512 n 567 − 1978 n 123 ] 1 / 11 = O ( n 61 ) [(n^{2013}-24n^{2009})^{1/3}+512n^{567}-1978n^{123}]^{1/11}=O(n^{61}) [(n201324n2009)1/3+512n5671978n123]1/11=O(n61)

线性复杂度:所有 O ( n ) O(n) O(n)类函数

这类算法的效率通常认为已可令人满意了


O O O( 2 n 2^n 2n)

指数: T ( n ) = a n T(n)=a^n T(n)=an

c > 0 , n c = O ( 2 n ) / / e n = 1 + n + n 2 / 2 ! + n 3 / 3 ! + n 4 / 4 ! + . . . c > 0 , n^c=O(2^n) \quad\quad\quad\quad \quad\quad\quad\quad//e^n=1+n+n^2/2!+n^3/3!+n^4/4!+... c>0,nc=O(2n)//en=1+n+n2/2!+n3/3!+n4/4!+...
             n 1000 = O ( 1.000000 1 n ) = O ( 2 n ) n^{1000}=O(1.0000001^n)=O(2^n) n1000=O(1.0000001n)=O(2n)
             1.000000 1 n = Ω ( n 1000 ) 1.0000001^n=\Omega(n^{1000}) 1.0000001n=Ω(n1000)

这类算法的计算成本增长极快,通常被认为不可忍受

O ( n c ) O(n^c) O(nc) O ( 2 n ) O(2^n) O(2n),是从有效算法无效算法的分水岭,有些问题的 O ( 2 n ) O(2^n) O(2n)算法往往显而易见,但设计出 O ( n c ) O(n^c) O(nc)算法却极其不易,甚至有时注定地只能是徒劳无功

例如 2-Subset 问题
问题描述
     S包含n个正整数, ∑ S = 2 m \sum S=2m S=2m
     S是否有子集,满足 ∑ T = m \sum T=m T=m
问题分析:简而言之,S集合内n个整数能否被恰好分成两部分,要求元素的个数不一定相同,但两部分的总和应相同
直觉算法:逐一枚举S的每一子集,并统计其中元素的总和
定理 ∣ 2 s ∣ = 2 ∣ s ∣ = 2 n |2^s|=2^{|s|}=2^n 2s=2s=2n
显而易见 ,直觉算法需要迭代2轮,并(在最坏情况下)至少需要花费很多时间——不堪理想!

那是否可以进行优化?
答:2-Subset is NP-complete(无法优化
就目前的计算模型而言,不存在可在多项式时间内回答此问题的算法,即上述的直觉算法已属最优

O ( ) O() O()图像

算法分析

两个主要任务 = 正确性(不变性 × \times ×单调性)+ 复杂度

关键在于复杂度的分析!

复杂度分析的主要方法:

  • 迭代:级数求和
  • 递归:递归跟踪 + 递推方程
  • 猜测 + 验证

级数

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

  • 幂方级数:比幂次高出一阶
    T 2 ( n ) = 1 2 + 2 2 + 3 2 + . . . + n 2 = n ( n + 1 ) ( 2 n + 1 ) / 6 = O ( n 3 ) T_2(n)=1^2+2^2+3^2+...+n^2=n(n+1)(2n+1)/6=O(n^3) T2(n)=12+22+32+...+n2=n(n+1)(2n+1)/6=O(n3)
    T 3 ( n ) = 1 3 + 2 3 + 3 3 + . . . + n 3 = n 2 ( n + 1 ) 2 / 4 = O ( n 4 ) T_3(n)=1^3+2^3+3^3+...+n^3=n^2(n+1)^2/4=O(n^4) T3(n)=13+23+33+...+n3=n2(n+1)2/4=O(n4)
    T 4 ( n ) = 1 4 + 2 4 + 3 4 + . . . + n 4 = n ( n + 1 ) ( 2 n + 1 ) ( 3 n 2 + 3 n − 1 ) / 30 = O ( n 5 ) T_4(n)=1^4+2^4+3^4+...+n^4=n(n+1)(2n+1)(3n^2+3n-1)/30=O(n^5) T4(n)=14+24+34+...+n4=n(n+1)(2n+1)(3n2+3n1)/30=O(n5)

  • 几何级数( a>1 ):与末项同阶
    T a ( n ) = a 0 + a 1 + . . . + a n = ( a n + 1 − 1 ) / ( a − 1 ) = O ( a n ) T_a(n)=a^0+a^1+...+a^n=(a^{n+1}-1)/(a-1)=O(a^n) Ta(n)=a0+a1+...+an=(an+11)/(a1)=O(an)

  • 收敛级数
    1 / 1 / 2 + 1 / 2 / 3 + 1 / 3 / 4 + . . . + 1 / ( n − 1 ) / n = 1 − 1 / n = O ( 1 ) 1/1/2+1/2/3+1/3/4+...+1/(n-1)/n=1-1/n=O(1) 1/1/2+1/2/3+1/3/4+...+1/(n1)/n=11/n=O(1)
    1 + 1 / 2 2 + . . . + 1 / n 2 < 1 + 1 / 2 2 + . . . = π 2 / 6 1+1/2^2+...+1/n^2<1+1/2^2+...=\pi^2/6 1+1/22+...+1/n2<1+1/22+...=π2/6=O(1)
    1 / 3 + 1 / 7 + 1 / 8 + 1 / 15 + 1 / 24 + 1 / 31 + 1 / 35 + . . . = 1 = O ( 1 ) 1/3+1/7+1/8+1/15+1/24+1/31+1/35+...=1=O(1) 1/3+1/7+1/8+1/15+1/24+1/31+1/35+...=1=O(1)

  • 调和级数
    h ( n ) = 1 + 1 / 2 + 1 / 3 + . . . + 1 / n = Θ ( l o g n ) h(n)=1+1/2+1/3+...+1/n=\Theta(logn) h(n)=1+1/2+1/3+...+1/n=Θ(logn)

  • 对数级数
    l o g 1 + l o g 2 + l o g 3 + . . . + l o g n = l o g ( n ! ) = Θ ( n l o g n ) log1+log2+log3+...+logn=log(n!)=\Theta(nlogn) log1+log2+log3+...+logn=log(n!)=Θ(nlogn)

循环和级数

————————————————————————

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

算数级数:
Σ i = 0 n − 1 n = n + n + . . . + n = n ∗ n = O ( n 2 ) \Sigma^{n-1}_{i=0}n=n+n+...+n=n*n=O(n^2) Σi=0n1n=n+n+...+n=nn=O(n2)

可以把这个过程想象为点到线再到面的过程,i=0,j=0为第一个点,j++就是点到线的过程,然后i++就是线到面的过程;

————————————————————————

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

算数级数:
Σ i = 0 n − 1 i = 0 + 1 + . . . + ( n − 1 ) = n ( n − 1 ) / 2 = O ( n 2 ) \Sigma^{n-1}_{i=0}i=0+1+...+(n-1)=n(n-1)/2=O(n^2) Σi=0n1i=0+1+...+(n1)=n(n1)/2=O(n2)

————————————————————————

for(int i=0;i<n;i++)
for(int j=0;j<i;j+=2013)

同理,也是算数级数;

————————————————————————

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

几何级数:
1 + 2 + 4 + . . . + 2 ⌊ l o g 2 ( n − 1 ) ⌋ 1+2+4+...+2^{\lfloor {log_2(n-1)} \rfloor} 1+2+4+...+2log2(n1)
= Σ k = 0 ⌊ l o g 2 ( n − 1 ) ⌋ 2 k / / (让 k = l o n g 2 i ) =\Sigma^{\lfloor {log_2(n-1)} \rfloor}_{k=0}2^k // (让k=long_2i) =Σk=0log2(n1)2k//(让k=long2i
= 2 ⌊ l o g 2 n ⌋ − 1 =2^{\lfloor {log_2n} \rfloor}-1 =2log2n1
= O ( n ) =O(n) =O(n)

取非极端元素

问题:给定整数子集S, ∣ S ∣ = n ≥ 3 |S|=n\ge3 S=n3
           找出元素 a ∈ S , a ≠ m a n ( S ) 且 a ≠ m a ∈ ( S ) a\in S, a\ne man(S) 且 a\ne ma\in(S) aS,a=man(S)a=ma(S)

算法

  • 从S中任取三个元素{ x,y,z }

     //若s以2数组形式给出,不妨取前三个
     //由于s是集合,这三个元素必然互异

  • 确定并排除其中的 最小,最大者

      //不妨设 x = m a x ( x , y , z ) , y = m i n ( x , y , z ) x=max(x,y,z),y=min(x,y,z) x=max(x,y,z),y=min(x,y,z)

  • 输出剩下的元素 z

结论:无论输入规模n多大,上述算法需要执行的时间都不变
            T ( n ) = 常数 = O ( 1 ) = Ω ( 1 ) = Θ ( 1 ) T(n)=常数=O(1)=\Omega(1)=\Theta(1) T(n)=常数=O(1)=Ω(1)=Θ(1)

起泡排序

问题:给定n个整数,将它们按(非降)序排序
观察有序/无序序列中,任意/总有一对相邻元素顺序/逆序
算法扫描交换
           依次比较每一对相邻原色,如有必要,交换之,若整趟都没有进行交换,则排序完成;否则,再做一趟扫描交换

void bubblesort(int A[],int n)
{
    for(bool sorted = false;(sorted=!sorted);n--)//逐趟扫描交换,直至完全有序
    {
        for(int i=1;i<n;i++)//自左向右,逐对检查A[0,n]内各相邻元素
        {
            if(A[i-1]>A[i])//若逆序
            {
                swap(A[i-1],A[i]);//交换
                sorted = false;//清除(全局)有序标志
            }
        }
    }
}

分析:该算法必然会结束吗?至少需要迭代多少趟?

  • 不变性:经 k 轮扫描交换后,最大的 k 个元素必然就位

        例如,按照要求给数组 A[ ] = {5,2,7,4,6,3,1}进行非递减的起泡排序,则第1轮扫描交换后,7这个元素一定在数组的最后一个位置,即 A[6] ;第2轮扫描交换后,6这个元素一定在 A[5] ,依次类推

  • 单调性:经 k 轮扫描交换后,问题规模缩减至 n-k

       根据不变性,每一轮扫描交换后,总有一个元素确定好位置,则对于下一轮扫描交换来说,未确定位置的元素数量一定减一,即问题规模缩减至 n-k

  • 正确性:经至多 n 趟扫描后,算法必然终止,且能给出正确解答

迭代和递归

数组求和【迭代】

问题:计算任意n个整数之和
实现:逐一取出每个元素,累加之

int SumI(int A[],int n){
    int sum=0;//o(1)
    for(int i=0;i<n;i++)//o(n)
        sum+=A[i];//o(1)
    return sum;//o(1)
 }

无论A[ ]内容如何,都有:
T ( n ) = 1 + n ∗ 1 + 1 = n + 2 = O ( n ) = Ω ( n ) = Θ ( n ) T(n)=1+n*1+1=n+2=O(n)=\Omega(n)=\Theta(n) T(n)=1+n1+1=n+2=O(n)=Ω(n)=Θ(n)

减而治之

为求解一个大规模的问题,可以将其划分为两个子问题:其一平凡,另一规模缩减;分别求解子问题,由子问题的解,得到原问题的解。

数组求和【线性递归】【减而治之】

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

递归跟踪分析:检查每个递归实例,累计所需时间(调用语句本身,计入对应的子实例),其总和即算法执行时间;
本例中,单个递归实例自身只需 O ( 1 ) O(1) O(1)时间
T ( n ) = O ( 1 ) ∗ ( n + 1 ) = O ( n ) T(n)=O(1)*(n+1)=O(n) T(n)=O(1)(n+1)=O(n)

从递推角度看,为求解sum(A , n),需递归求解规模为n-1的问题sum()A , n-1),再累加上A[n-1];递归基:sum(A , 0);

递推方程
T ( n ) = T ( n − 1 ) + o ( 1 ) T(n)=T(n-1)+o(1) T(n)=T(n1)+o(1)
T ( 0 ) = O ( 1 ) T(0)=O(1) T(0)=O(1)

求解
T ( n ) − n = T ( n − 1 ) − ( n − 1 ) = . . . T(n)-n=T(n-1)-(n-1)= ... T(n)n=T(n1)(n1)=...
                   = T ( 2 ) − 2 =T(2)-2 =T(2)2
                   = T ( 1 ) − 1 =T(1)-1 =T(1)1
                   = T ( 0 ) =T(0) =T(0)
T ( n ) = O ( 1 ) + n = O ( n ) T(n)=O(1)+n=O(n) T(n)=O(1)+n=O(n)

数组倒置【减而治之】

任给数组A[0 , n),将其前后颠倒

统一接口:

void reverse(int* A,int low,int high);

递归版

if(low < high){//问题规模的奇偶性不变,需要两个递归基
    swap(A[low],A[high]);
    reverse(A,low+1,high-1);
}

迭代版

while(low < high){
    swap(A[low++],A[high--]);
}

分而治之

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

数组求和【二分递归】【分而治之】

sum(int A[],int low,int high){
    if(low == high) return A[1ow];
    int middle=(low + high) >> 1;
    return sum(A,low,high) + sum(A,middle+1,high);
}//入口形式为sum(A,0,n-1)

T ( n ) = 各层递归实例所需时间之和 T(n)=各层递归实例所需时间之和 T(n)=各层递归实例所需时间之和
            = O ( 1 ) ∗ ( 2 0 + 2 1 + 2 2 + 2 l o g n ) =O(1)*(2^0+2^1+2^2+2^{logn}) =O(1)(20+21+22+2logn)
            = O ( 1 ) ∗ ( 2 l o g n + 1 − 1 ) = O ( n ) =O(1)*(2^{logn+1}-1)=O(n) =O(1)(2logn+11)=O(n)

从递推角度看,为求解sun(A, low, high),需递归求解sum(A, low, middle)和sum(A, middle+1, high),进而将子问题的解累加;递归基:sum(A, low, low);

递推关系
T ( n ) = 2 ∗ T ( n / 2 ) + o ( 1 ) T(n)=2*T(n/2)+o(1) T(n)=2T(n/2)+o(1)
T ( 1 ) = O ( 1 ) T(1)=O(1) T(1)=O(1)

求解
T ( n ) T(n) T(n)           = 2 ∗ T ( n / 2 ) + c 1 =2*T(n/2)+c_1 =2T(n/2)+c1
T ( n ) + c 1 = 2 ∗ ( T ( n / 2 ) + c 1 ) = 2 2 ∗ ( T ( n / 4 ) + c 1 ) T(n)+c_1=2*(T(n/2)+c_1)=2^2*(T(n/4)+c_1) T(n)+c1=2(T(n/2)+c1)=22(T(n/4)+c1)
                     = . . . = ... =...
                     = 2 l o g n ( T ( 1 ) + c 1 ) = n ∗ ( c 2 + c 1 ) =2^{logn}(T(1)+c_1)=n*(c_2+c_1) =2logn(T(1)+c1)=n(c2+c1)
T ( n ) T(n) T(n)            = ( c 1 + c 2 ) n − c 1 = O ( n ) =(c_1+c_2)n-c_1=O(n) =(c1+c2)nc1=O(n)

动态规划

所谓的动态规划,其实可以理解为,通过递归找出算法的本质,并且给出一个初步的解,再将其等效得转化为迭代的形式。

FIB()

递归: f i b ( n ) = f i b ( n − 1 ) + f i b ( n − 2 ) : 0 , 1 , 1 , 2 , 3 , 5 , 8...... fib(n)=fib(n-1)+fib(n-2):{0,1,1,2,3,5,8......} fib(n)=fib(n1)+fib(n2):0,1,1,2,3,5,8......

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

复杂度分析:
T ( 0 ) = 1 , T ( 1 ) = 1 , T ( n ) = T ( n − 1 ) + T ( n + 1 ) + 1 , ( n > 1 ) T(0)=1,T(1)=1,T(n)=T(n-1)+T(n+1)+1,(n>1) T(0)=1,T(1)=1,T(n)=T(n1)+T(n+1)+1,(n>1)
令 S ( n ) = [ T ( n ) + 1 ] / 2 令 S(n)=[T(n)+1]/2 S(n)=[T(n)+1]/2
则 S ( 0 ) = 1 = f i b ( 1 ) , S ( 1 ) = 1 = f i b ( 2 ) 则S(0)=1=fib(1),S(1)=1=fib(2) S(0)=1=fib(1),S(1)=1=fib(2)
故 S ( n ) = S ( n − 1 ) + S ( n − 2 ) = f i b ( n + 1 ) 故S(n)=S(n-1)+S(n-2)=fib(n+1) S(n)=S(n1)+S(n2)=fib(n+1)
T ( n ) = 2 ∗ S ( n ) − 1 = 2 ∗ f i b ( n + 1 ) − 1 = O ( 2 ∗ f i b ( n + 1 ) ) = O ( n 2 ) T(n)=2*S(n)-1=2*fib(n+1)-1=O(2*fib(n+1))=O(n^2) T(n)=2S(n)1=2fib(n+1)1=O(2fib(n+1))=O(n2)

在这里插入图片描述

递归版 fib() 低效地根源在于,各递归实例均被大量重复地调用

解决方法A( 记忆化 )
将已计算过的实例地结果制表备查

解决方法B( 动态规划
颠倒计算方向:由自顶而下递归,为自底而上迭代

f=0;//fib(0)
g=1;//fib(1)
while(0 < n--)
{
    g=g+f;
    f=g-f;
}
return g;

T ( n ) = O ( n ) ,且仅只需 O ( 1 ) 空间 T(n)=O(n),且仅只需 O(1) 空间 T(n)=O(n),且仅只需O(1)空间
在这里插入图片描述

LCS

子序列:由序列中若干字符,按原想对次序构成
最长公共子序列:两个序列公共子序列中的最长者(可能有多个,也可能有歧义)

递归
对于序列A[0,n]和B[0,m],即LCS(A,B)就有三种情况

  1. 若n=-1或m=-1,则取作空序列(‘’‘’) //递归基
  2. 若A[n]=‘X’=B[m],则取作 L C S ( A [ 0 , n ) , B [ 0 , m ) ) + ′ X ′ LCS(A[0,n),B[0,m))+'X' LCS(A[0,n),B[0,m))+X //减而治之
  3. A[n]!=B[m],则在 L C S ( A [ 0 , n ] , B [ 0 , m ) ) 与 L C S ( A [ 0 , n ) , B [ 0 , m ] ) LCS(A[0,n],B[0,m))与LCS(A[0,n),B[0,m]) LCS(A[0,n],B[0,m))LCS(A[0,n),B[0,m])中取更长者 //分而治之
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值