我是自动化专业的应届研究生,最终拿到了tplink、华为、vivo等公司的ssp的offer,分享自己学习过的计算机基础知识(C语言+操作系统+计算机网络+linux)以及数据结构与算法的相关知识,保证看完让你有所成长。
欢迎关注我,学习资料免费分享给你哦!还有其他超多学习资源,都是我自己学习过的,经过过滤之后的资源,免去你还在因为拥有大量资源不知如何入手的纠结,让你体系化学习。
对于一个算法来说,我们如何评价它的好坏呢?一般来说就是通过程序解决问题的时间和空间。现在的计算机空间效率一般已经不是关注的重点了,但是时间效率仍然是算法关注的重点。而考查一个程序的运行时间,就需要将程序每次执行的操作表示成输入规模的函数。这就是时间复杂度分析。时间复杂度的分析也是算法面试中常考的问题,经过最近的学习,总结如下。
举几个例子来说,O(f(n))是一个集合,表示的是所有函数的增长趋势小于等于f(n)的函数集合。比如n属于O(n^2),500n+10属于O(n ^ 2).
对于定义来说就是,对于一个函数g(n)属于O(f(n))中,表示如下的公式:
g ( n ) ∈ O ( f ( n ) ) g(n)\in O(f(n)) g(n)∈O(f(n))
它表示的条件是:对于足够大的n,,g(n)的上界由f(n)的常数倍所确定,也就是说,存在大于0的常数c和非负数n0,使得:对于所有的n大于等于n0时,g(n)都是小于等于cf(n)的。
比如前面所说500n+10小于等于510n^2.它的c=501。
1.如果 t 1 ( n ) ∈ O ( g 1 ( n ) ) t_1(n)\in O(g_1(n)) t1(n)∈O(g1(n)) 并且 t 2 ( n ) ∈ O ( g 2 ( n ) ) t_2(n)\in O(g_2(n)) t2(n)∈O(g2(n)),那么 t 1 ( n ) + t 2 ( n ) ∈ O ( m a x g 1 ( n ) , g 2 ( n ) ) t_1(n)+t_2(n)\in O(max{g_1(n),g_2(n)}) t1(n)+t2(n)∈O(maxg1(n),g2(n))
这也是我们在计算时间复杂度中最常使用到的,比如一个算法的时间复杂度是 n^2+n+2,根据上面的定理,我们可以说它的大O时间复杂度是O(n ^ 2)。
2.对于常数的运行时间,对于大O表示法来说都是O(1)。
3.系数对于大O表示法是没有用的,比如n^2和1000n ^2来说,它们的大O记法是一样的,因为通过定义可以知道,系数其实本质上就是定义中的c,所以它们的大O记法都是O(n ^2)。
int a=10;
int b=5;
上面程序虽然执行了两步,但对于大O表示法来说,都是O(1),因为2相当于是系数,可以去掉。
for(int i=0;i
int count=1;
while(count
由于每次执行count扩大两倍,那么就是2^ x=n,可以解得x=log以2为底的n的对数,所以时间复杂度就是O(logn)
for(int i=0;i
以查找数组中的最大值程序为例,分析计算一个非递归程序时间复杂度是如何计算的。
int MaxElement(int *A,int n)
{
int maxval=A[0];
for(int i=0;imaxval)
{
maxval=A[i];
}
}
return A[i];
}
可以看到,这个问题的输入规模是数组元素的个数决定的。由前面的学习可以知道,一个程序的时间复杂度是由程序之中大O最大部分决定的,所以只需要计算最大值就可以了,这个程序中的最大值是由for循环部分决定的。for循环内部进行了一步的比较操作,那么对于i从0到n-1,一共进行了n-1次比较,那么整个程序执行的次数就是:
C ( n ) = ∑ i = 0 n − 1 1 = n − 1 = O ( n ) C(n)=\sum_{i=0}^{n-1}1=n-1=O(n) C(n)=i=0∑n−11=n−1=O(n)
所以时间复杂度是O(n),这个分析也证明了前面所说的for循环的时间复杂度是O(n).
非递归的时间复杂度分析的步骤如下:
1)首先找到问题的输入规模在哪里,例如本例的n
2)找出算法的基本操作(一般是最内层的循环)
3)建立一个算法基本操作的求和表达式,利用数学知识得到它的和
4)利用大O的一些准则简化表达式,得到最终的大O时间复杂度分析结果。
bool uniqueElements(int *A,int n)
{
for(int i=0;i
我们根据上面的一个算法,找出一个数组中是否包含重复元素,来实践一下刚才所说的分析方法,首先找到输入规模为n,但是这个算法如果在中间出现了相同元素,就结束了,所以说不仅仅取决于n,还取决于数组中数据的情况,这里谈论一下最坏的情况,就是遍历了整个数组。
那么此时的输入规模就是n,找到关键的执行步骤,双重循环中的判断语句,每次执行一次。在内循环之中j在i+1和n-1之间的每一个值都会比较一次。在外循环中,i从0到n-2的每个值,都会重复上面的过程一遍。因此可以得到:
C w o r s t ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 C_{worst}(n)=\sum_{i=0}^{n-2}\sum_{j=i+1}^{n-1}1 Cworst(n)=i=0∑n−2j=i+1∑n−11
此时就考验数学了。首先将内部的j循环展开。
C w o r s t ( n ) = ∑ i = 0 n − 2 ∑ j = i + 1 n − 1 1 = ∑ i = 0 n − 2 [ ( n − 1 ) − ( i + 1 ) + 1 ] = ∑ i = 0 n − 2 ( n − 1 − i ) = ∑ i = 0 n − 2 ( n − 1 ) − ∑ i = 0 n − 2 i C_{worst}(n)=\sum_{i=0}^{n-2}\sum_{j=i+1}^{n-1}1=\sum_{i=0}^{n-2}[(n-1)-(i+1)+1]=\sum_{i=0}^{n-2}(n-1-i)=\sum_{i=0}^{n-2}(n-1)-\sum_{i=0}^{n-2}i Cworst(n)=i=0∑n−2j=i+1∑n−11=i=0∑n−2[(n−1)−(i+1)+1]=i=0∑n−2(n−1−i)=i=0∑n−2(n−1)−i=0∑n−2i
此时两个求和式子分别求解
∑ i = 0 n − 2 ( n − 1 ) = ( n − 1 ) ∑ i = 0 n − 2 1 = ( n − 1 ) ( n − 2 − 0 + 1 ) = ( n − 1 ) 2 \sum_{i=0}^{n-2}(n-1)=(n-1)\sum_{i=0}^{n-2}1=(n-1)(n-2-0+1)=(n-1)^2 i=0∑n−2(n−1)=(n−1)i=0∑n−21=(n−1)(n−2−0+1)=(n−1)2
∑ i = 0 n − 2 i = 0 + 1 + 2 + 3 + . . . + n − 2 = ( n − 2 ) ( n − 1 ) 2 \sum_{i=0}^{n-2}i=0+1+2+3+...+n-2=\frac{(n-2)(n-1)}{2} i=0∑n−2i=0+1+2+3+...+n−2=2(n−2)(n−1)
将两个式子合并,得到
∑ i = 0 n − 2 ( n − 1 ) − ∑ i = 0 n − 2 i = ( n − 1 ) 2 − ( n − 2 ) ( n − 1 ) 2 = ( n − 1 ) n 2 \sum_{i=0}^{n-2}(n-1)-\sum_{i=0}^{n-2}i=(n-1)^2-\frac{(n-2)(n-1)}{2}=\frac{(n-1)n}{2} i=0∑n−2(n−1)−i=0∑n−2i=(n−1)2−2(n−2)(n−1)=2(n−1)n
利用大O的准则可以得到它的最坏时间复杂度就是O(n^2)。当然这种非递归的其实很好记,看到循环就可以判断,这么推导是能够清楚的知道到底是如何计算时间复杂度,可以理解其本质,虽然面试不会考推导,但对自己来说是一种提升。
这里面用到了两个数学里的求和公式:
∑ i = l u 1 = u − l + 1 , 其 中 u , l 分 别 是 上 下 界 \sum_{i=l}^{u}1=u-l+1,其中u,l分别是上下界 i=l∑u1=u−l+1,其中u,l分别是上下界
∑ i = 0 n i = ∑ i = 1 n i = 1 + 2 + 3 + . . . + n = n ( n + 1 ) 2 \sum_{i=0}^{n}i=\sum_{i=1}^{n}i=1+2+3+...+n=\frac{n(n+1)}{2} i=0∑ni=i=1∑ni=1+2+3+...+n=2n(n+1)
其实我从刚开始学习数据结构,就对这个递归程序复杂度的分析很迷惑,不知道如何计算,只是知道去死记硬背一些排序算法,比如归并排序等的时间复杂度。直到最近重新学习,才发现有主定理这种东西。
递归算法的时间复杂度主要有两种方法来计算,要根据算法的实际情况来选择,一种是每次递归只是将数据规模减一或者减二这种的,比如斐波那契数列。另一种是类型归并排序和快速排序这种分治思想的递归算法,将问题的规模分成几份,分别求解。
比如采用递归的算法计算n的阶乘。
int F(int n)
{
if(n==0) return 1;
else return F(n-1)*n;
}
我们假设F(n)所需要的执行的次数是C(n),那么就可以将C(n)用如下的公式表示出来了。
C ( n ) = C ( n − 1 ) + 1 C(n)=C(n-1)+1 C(n)=C(n−1)+1
其中C(n-1)是用来计算F(n-1)的运算次数,而1是表示乘法的运算此时。而当n==0时,就是C(0)=1,因为直接返回了值。那么我们就可以根据上面的式子来进行迭代求解了。
C ( n ) = C ( n − 1 ) + 1 = [ C ( n − 2 ) + 1 ] + 1 = C ( n − 2 ) + 2 C(n)=C(n-1)+1=[C(n-2)+1]+1=C(n-2)+2 C(n)=C(n−1)+1=[C(n−2)+1]+1=C(n−2)+2
C ( n − 2 ) + 2 = [ C ( n − 3 ) + 1 ] + 2 = C ( n − 3 ) + 3 C(n-2)+2=[C(n-3)+1]+2=C(n-3)+3 C(n−2)+2=[C(n−3)+1]+2=C(n−3)+3
逐步迭代可以迭代出以下的式子:
C ( n ) = C ( n − 1 ) + 1 = . . . = C ( n − i ) + i = . . . = C ( n − n ) + n = C ( 0 ) + n C(n)=C(n-1)+1=...=C(n-i)+i=...=C(n-n)+n=C(0)+n C(n)=C(n−1)+1=...=C(n−i)+i=...=C(n−n)+n=C(0)+n
所以这个算法的时间复杂度是O(n)。
下面我们在以著名的汉诺塔问题为例,这里不详细解释这个问题了,直接把递归程序拿过来借用,学习如何使用迭代法分析时间复杂度。
void hanNuo(int n,char A,char B,char C)
{
if(n>0)
{
hanNuo(A,C,B,n-1);
cout<<"A to C"<
还是用C(n)来表示hanNuo(n)的执行次数,那么就可以根据递归的公式来表示出C(n).
C ( n ) = C ( n − 1 ) + 1 + C ( n − 1 ) = 2 C ( n − 1 ) + 1 C(n)=C(n-1)+1+C(n-1)=2C(n-1)+1 C(n)=C(n−1)+1+C(n−1)=2C(n−1)+1
还是按照递归的方法逐渐向下展开
C ( n ) = 2 ∗ C ( n − 1 ) + 1 = 2 ∗ [ 2 ∗ C ( n − 2 ) + 1 ] + 1 = 2 2 ∗ C ( n − 2 ) + 2 + 1 C(n)=2*C(n-1)+1=2*[2*C(n-2)+1]+1=2^2*C(n-2)+2+1 C(n)=2∗C(n−1)+1=2∗[2∗C(n−2)+1]+1=22∗C(n−2)+2+1
C ( n ) = 2 2 ∗ C ( n − 2 ) + 2 + 1 = 2 2 ∗ [ 2 ∗ C ( n − 3 ) + 1 ] + 2 + 1 = 2 3 ∗ C ( n − 3 ) + 2 2 + 2 + 1 C(n)=2^2*C(n-2)+2+1=2^2*[2*C(n-3)+1]+2+1=2^3*C(n-3)+2^2+2+1 C(n)=22∗C(n−2)+2+1=22∗[2∗C(n−3)+1]+2+1=23∗C(n−3)+22+2+1
C ( n ) = 2 i ∗ C ( n − i ) + 2 i − 1 C(n)=2^i*C(n-i)+2^i-1 C(n)=2i∗C(n−i)+2i−1
因为最后递归到n=1,所以上面式子的i=n-1即可。
C ( n ) = 2 n − 1 ∗ C ( 1 ) + 2 n − 1 − 1 = 2 n − 1 + 2 n − 1 − 1 = 2 n − 1 C(n)=2^{n-1}*C(1)+2^{n-1}-1=2^{n-1}+2^{n-1}-1=2^n-1 C(n)=2n−1∗C(1)+2n−1−1=2n−1+2n−1−1=2n−1
所以汉诺塔问题的时间复杂度就是O(2^n)。
这就是迭代法求解递归算法的时间复杂度分析的方法。
当递归函数的时间函数满足如下的关系时,就可以使用主定理了。
T ( n ) = a ∗ T ( n b ) + f ( n ) T(n)=a*T(\frac{n}{b})+f(n) T(n)=a∗T(bn)+f(n)
式子中的a就是递归子问题的个数,b是每个递归子问题是原问题的规模,f(n)表示的是合并递归结果所需要的操作。
使用这个定理,只需要记住三种情况即可
情 况 一 : 当 递 归 部 分 的 执 行 时 间 O ( n l o g b a ) > O ( f ( n ) ) 的 时 候 , T ( n ) = O ( n l o g b a ) 情况一:当递归部分的执行时间O(n^{log_ba})>O(f(n))的时候,T(n)=O(n^{log_ba}) 情况一:当递归部分的执行时间O(nlogba)>O(f(n))的时候,T(n)=O(nlogba)
情 况 二 : 当 递 归 部 分 的 执 行 时 间 O ( n l o g b a ) < O ( f ( n ) ) 的 时 候 , T ( n ) = O ( f ( n ) ) 情况二:当递归部分的执行时间O(n^{log_ba})
情 况 三 : 当 递 归 部 分 的 执 行 时 间 O ( n l o g b a ) = O ( f ( n ) ) 的 时 候 , T ( n ) = O ( n l o g b a ) l o g n 情况三:当递归部分的执行时间O(n^{log_ba})=O(f(n))的时候,T(n)=O(n^{log_ba})logn 情况三:当递归部分的执行时间O(nlogba)=O(f(n))的时候,T(n)=O(nlogba)logn
下面举例分析。
以归并排序为例,归并排序的代码这里就不贴了,首先是将两个问题分成相等的两部分,所以b=2,需要左侧递归和右侧递归,两部分那么a=2,将两部分合并的起来的f(n)的操作的时间复杂度是O(n).所以根据主定理可以得到O(n)=O(f(n)),所以时间复杂度为O(nlogn)。
在看下面的程序
int lianxi(int n)
{
if(n==0)
{
return 0;
}
return lianxi(n/4)+lianxi(n/4);
}
根据主定理,可以看到a=2,b=4,而O(f(n))=O(1),因为只是进行了加法运算。所以比较
O ( n l o g b a ) = O ( n l o g 4 2 ) = O ( n ) > O ( 1 ) O(n^{log_ba})=O(n^{log_42})=O(\sqrt{n})>O(1) O(nlogba)=O(nlog42)=O(n)>O(1)
所以这个时间复杂度就是O(根号n)。
对于快速排序也是一样可以这么分析,这里不在详细分析了。