数学基础之函数增长与复杂性分类

数学基础之函数增长与复杂性分类

1、渐进符号
我们都知道在过题的时候一定会有资源限制,因此我们在选取算法的时候需要先简单计算一下算法的时间复杂度,如果超了,就得进行优化或者换更快的算法。
但是我们在计算时间复杂度的时候,往往并不需要算出精确的结果,实际上,对于足够大的输入规模,我们只要研究算法的渐进效率(运行时间的增长量级)即可。下面我们先来简单学习一下几种渐进符号。
Θ Θ Θ
(这是由mathtype打出的公式,下面有什么出现歧义的表示可参照图片理解,因为用Latex打出的公式太长了)
在这里插入图片描述
显然,该公式要求 g ( n ) g(n) g(n)是渐进非负的(要保证集合非空)。
Θ Θ Θ记号的主要功能是用来表示一个算法运行时间的确界(根据数据的规模),通常表示为: f ( n ) ∈ Θ ( g ( n ) ) f(n)\inΘ(g(n)) f(n)Θ(g(n))。由定义可知函数 f ( n ) f(n) f(n)的上界为 c 1 g ( n ) c_{1}g(n) c1g(n),下界为 c 2 g ( n ) c_{2}g(n) c2g(n)
举个例子,现在有 f ( n ) = 2 n 2 − 1 f(n)=2n^{2}-1 f(n)=2n21,那么就有 f ( n ) = Θ ( n 2 ) f(n)=Θ(n^{2}) f(n)=Θ(n2) 。因为根据定义,我们可取 n 0 = 1 , c 1 = 1 , c 2 = 2 , n_{0}=1,c_{1}=1,c_{2}=2, n0=1,c1=1,c2=2, 满足 0 ≤ n 2 ≤ 2 n 2 − 1 ≤ 2 n 2 0\leq n^{2}\leq 2n^{2}-1\leq 2n^{2} 0n22n212n2 n > 1 n>1 n>1恒成立。

O Ο O
在这里插入图片描述

O Ο O记号的主要功能是用来表示算法的最坏情况(因为它表示的是一个渐进上界),相信眼尖的同学已经发现这是 Θ Θ Θ记号右边的情况,所以这里就不站在数学角度举例了,我们来看插入排序的核心代码:

void insert_sort(int* a, int n)	// 注意这里是引用传递,否则排序无效
{
	// 利用双层for循环做升序排序
	for (int i = 0; i < n; i++) {
		for (int j = i; j > 0; j--) {
			if (a[j] < a[j-1]) {
				swap(a[j], a[j - 1]);
			}
		}
	}
}

这个算法的最坏情况就是数组a是降序排列的,那么它的运行时间上界就是 O ( n 2 ) Ο(n^{2}) O(n2)

ο ο ο
在这里插入图片描述

ο ο ο记号表示非渐进紧确的上界,不理解这个概念没关系,我们只需要记住上述公式可理解为: l i m n → ∞ f ( n ) g ( n ) = 0 lim_{n\to \infty}\frac{f(n)}{g(n)}=0 limng(n)f(n)=0
这涉及到了高等数学中高阶与低阶的知识,感兴趣的同学可以自行了解一下,这里举一个简单的例子: 2 n + 1 = ο ( n 2 ) 2n+1=ο(n^{2}) 2n+1=ο(n2)

Ω Ω Ω
在这里插入图片描述
Ω Ω Ω记号与 O Ο O记号相对, O Ο O记号给出的是函数的渐进上界,而 Ω Ω Ω记号则给出了函数的渐进下界。研究渐进上界帮助我们判断一个算法能否用来处理一道题目,而研究渐进下界则可以帮助我们改善算法复杂度。

ω ω ω
在这里插入图片描述

ω ω ω记号与 ο ο ο记号相对, ο ο ο记号记号表示非渐进紧确的上界,而 ω ω ω记号则表示非渐进紧确的下界。同样的公式可以理解为:
l i m n → ∞ f ( n ) g ( n ) = ∞ lim_{n\to \infty}\frac{f(n)}{g(n)}=\infty limng(n)f(n)=,这里也举一个简单的小例子, 2 n 2 + 1 = ο ( n ) ) 2n^{2}+1=ο(n)) 2n2+1=ο(n))

最后给两个小性质,可以帮助我们考虑复合函数:

  1. O ( g 1 ( n ) ) + O ( g 2 ( n ) ) = O ( m a x { g 1 ( n ) , g 2 ( n ) } ) Ο(g_{1}(n))+Ο(g_{2}(n))=Ο(max\{g_{1}(n),g_{2}(n)\}) O(g1(n))+O(g2(n))=O(max{g1(n),g2(n)})
  2. O ( g 1 ( n ) ) ⋅ O ( g 2 ( n ) ) = O ( g 1 ( n ) g 2 ( n ) ) Ο(g_{1}(n))\cdotΟ(g_{2}(n))=Ο(g_{1}(n)g_{2}(n)) O(g1(n))O(g2(n))=O(g1(n)g2(n))

这两个性质同样适用于 Θ Θ Θ记号和 Ω Ω Ω记号,但是一般情况下,我们熟练掌握 O Ο O记号就可以了。

2、阶的计算
在我们计算算法时间复杂度时,通常有如下复杂度关系:
c < l o g n < n < n l o g n < n a < a n < n ! c<logn<n<nlogn<n^{a}<a^{n}<n! c<logn<n<nlogn<na<an<n!(其中, c c c为常数, a a a为大于1的常数)

递归算法是我们经常会使用的算法,因此我们必须熟练掌握它的时间复杂度的阶的计算方法——主定理计算:
T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)
T ( n ) T(n) T(n)的阶在计算时,一般需要分为3种情况进行讨论:
( 1 ) ∃ ϵ > 0 , s . t . f ( n ) = O ( n l o g b a − ϵ ) , (1)\exists\epsilon>0,s.t.f(n)=Ο(n^{log_{b}a-\epsilon}), (1)ϵ>0,s.t.f(n)=O(nlogbaϵ), T ( n ) = Θ ( n l o g b a ) T(n)=\Theta(n^{log_{b}a}) T(n)=Θ(nlogba)
( 2 ) f ( n ) = Θ ( n l o g b a ) , (2)f(n)=\Theta(n^{log_{b}a}), (2)f(n)=Θ(nlogba), T ( n ) = Θ ( n l o g b a l o g n ) T(n)=\Theta(n^{log_{b}a}logn) T(n)=Θ(nlogbalogn)
( 3 ) ∃ ϵ > 0 , s . t . f ( n ) = Ω ( n l o g b a + ϵ ) , (3)\exists\epsilon>0,s.t.f(n)=\Omega(n^{log_{b}a+\epsilon}), (3)ϵ>0,s.t.f(n)=Ω(nlogba+ϵ), ∀ c < 1 , ∃ n , s . t . a f ( n b ) ≤ c f ( n ) , \forall c<1,\exists n,s.t.af(\frac{n}{b})\leq cf(n), c<1,n,s.t.af(bn)cf(n), T ( n ) = Θ ( f ( n ) ) T(n)=\Theta(f(n)) T(n)=Θ(f(n))
上述定理实际上是比较复杂的,所幸由于在绝大多数情况下, f ( n ) f(n) f(n)的执行时间是多项式时间,所以主定理可以特殊为:
T ( n ) = a T ( n / b ) + O ( n d ) T(n)=aT(n/b)+Ο(n^{d}) T(n)=aT(n/b)+O(nd),相应的,3种情况也可以改写为:
( 1 ) (1) (1) d > l o g b a , d>log_{b}a, d>logba, T ( n ) = O ( n d ) T(n)=Ο(n^{d}) T(n)=O(nd)
( 2 ) (2) (2) d = l o g b a , d=log_{b}a, d=logba, T ( n ) = O ( n d l o g n ) T(n)=Ο(n^{d}logn) T(n)=O(ndlogn)
( 3 ) (3) (3) d < l o g b a , d<log_{b}a, d<logba, T ( n ) = O ( n l o g b a ) T(n)=Ο(n^{log_{b}a}) T(n)=O(nlogba)
这里我们举个例子来理解一下主定理的用法:

归并排序
熟悉归并排序的人都知道它的时间复杂度是 O ( n l o g n ) Ο(nlogn) O(nlogn),下面我们来看这是如何计算出来的:

void merge(int *a, int start, int middle, int end)
{
	int l1 = middle - start + 1; // 一分为二后数组1的长度
	int l2 = end - middle; // 一分为二后数组2的长度
	int i, j, k;
	int a1[l1+1];
	int a2[l2+1];
	for(i=0; i<l1; i++) // 遍历数组1
		a1[i] = a[start+i];
	for(j=0; j<l2; j++) // 遍历数组2
		a2[j] = a[middle+j+1];
	a1[l1]=0x7fffffff;
	a2[l2]=0x7fffffff; // 两个数组最后一个值都存放int型所能表示的最大值
	for(i=0,j=0,k=start; k<=end; k++)
	{
		if(a1[i] <= a2[j]) a[k] = a1[i++];
		else a[k] = a2[j++];
	} // 合并数组
}
 
void merge_sort(int *a, int start, int end) 
{
	if(start < end)
	{
		int middle = (start + end) / 2; //将数组一分为二
		merge_sort(a, start, middle); //重复操作直至不可分
		merge_sort(a, middle+1, end);
		merge(a, start, middle, end);
	}
}

上述代码是归并排序的核心代码,归并的要义就在于把规模为 n n n的问题分成两个相似的子问题,规模都降为 n / 2 n/2 n/2,再加上分解和合并的复杂度 f ( n ) = Θ ( n ) f(n)=\Theta(n) f(n)=Θ(n),所以套用主定理的模型:
T ( n ) = 2 T ( n / 2 ) + f ( n ) T(n)=2T(n/2)+f(n) T(n)=2T(n/2)+f(n),可见满足简化后的情况 ( 2 ) (2) (2),所以有 T ( n ) = O ( n l o g n ) T(n)=Ο(nlogn) T(n)=O(nlogn)

3、复杂度分类
在学习复杂度分类之前,我们需要先了解一下图灵机的概念,图灵机是一种抽象的计算模型,这里只做简单了解,详细请参考百度百科https://baike.baidu.com/item/图灵机/2112989?fr=aladdin
在这里插入图片描述

图灵机有k个磁带每一个磁带都有一个对磁带某位置进行读操作或者写操作磁头。但是有唯一磁带是输入磁带,它的磁头只能进行读操作,剩下的k-1个磁带是工作磁带,它们的磁带可以进行读操作,也可以进行写操作。
图灵机还有一个用于存储当前状态寄存器,注意图灵机的状态集是有限的。
图灵机的工作原理:
1、判断图灵机当前状态。
2、读取k个磁带上磁头对应位置的内容。
3、修改工作磁带上磁头位置的内容(可以保持不变)。
4、分别将每个磁头移动一个位置或者原地不动。
下面来看复杂度分类:
P复杂度类:
存在相应的确定性图灵机(有一套确定的规则),可以在输入长度的多项式时间内计算出问题的结果,比如两个整数相加。
NP复杂度类:
存在相应的确定性图灵机,可以在输入长度的多项式时间内判断一个结果是否正确,比如子集和问题(判断给定整数集合是否存在一个子集,该子集的元素之和为某定值)。
NPC(NP-Complete)复杂度类:
由NP里最难的问题组成。
由此可见,以上三个复杂度类相互之间是存在包含关系的:
1、P是NP的子集(是否是真子集至今未被证明)
2、NPC是NP的子集

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值