前言
最近在学习NLP的过程中遇到了一个名词「主定理」,本来以为是一个非常简单的内容,仔细思考了一下发现自己没有听懂,然后就看了各种各样的内容,课程上有些不太能理解,所以决定写(水)一下这篇文章
假装开始
引言
归并排序
首先来看一道题目
有一个数组A = [3,4,1,6,7,2,5,9],要对这个数组使用归并排序(Merge Sort)的方式进行排序,求这次排序的时间复杂度。
所谓归并排序,需要先将数组A分为B = [3,4,1,6]和C = [2,5,7,9]两部分,每个部分按照给定序次进行提前排序。
在排好序的B和C中对应位置进行比大小(即B[0]和C[0],B[1]和C[1],B[2]和C[2],B[3]和C[3]),然后将所有数组排列好放回数组A中。
而对于分得的子数组进行排序时,由于每个子数组中元素个数不止2,因此在排序的时候采取相同的思路,将子数组按照相同的1/2方式分解成子数组,如此往复直到数组中只剩下2个元素,按照给定的顺序排好。
当所有元素个数为2的子数组排好序之后,按照自底向上的方式将结果进行组合和排序,直到组合成原来的数组结束。
对于这种方法,在编程实现中最方便的应该就是使用递归
进行实现了,具体实现方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| def MergeSort(pendingList): """归并排序""" if(len(pendingList) <= 1): return pendingList else: middleIndex = len(pendingList) // 2 frontList = MergeSort(pendingList[:middleIndex]) backList = MergeSort(pendingList[middleIndex:]) return ListMerge(frontList, backList)
def ListMerge(frontList, backList): """数组合并""" sortedList = [] i = 0 j = 0 while (i < len(frontList) and j < len(backList)): if(frontList[i] <= backList[j]): sortedList.append(frontList[i]) i += 1 else: sortedList.append(backList[j]) j += 1 sortedList += frontList[i:] sortedList += backList[j:] return sortedList
pendingList = [3, 4, 1, 6, 7, 2, 5, 9] print ('待排序数组为: ',pendingList) sortedList = MergeSort(pendingList) print ('排序完的数组为: ',sortedList)
|
那么其时间复杂度具体应该为多少呢?
我们首先假设排序数组A的时间复杂度为T(n),那么将其分为两个则每组的复杂度会变成T(n/2)
。两组合并时候由于要循环检测一遍之后插入相应的值,因此还应该再加一个复杂度为n
。最终:T(n) = 2T(n/2) + n
。
那么,怎样从上式得到标准的时间复杂度形如O()
呢?
分治法
这种归并排序的处理方法隶属于一种专门的方法——分治法。
在计算机科学(CS,也就是我的专业)中,分治法是建基于多项分支递归的一种很重要的算法范型。就如同字面上的含义一样,是“分而治之”,即把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
正文部分
主定理(Master Theorem)定义
假设一个递归关系式有如下关系:
T(n)=aT(bn)+f(n),其中a≥1,b>1
其中,n为问题规模,a为递归的子问题数量,bn为每个子问题的规模(假设每个子问题的规模基本一样),f(n)为将规模为n的子问题转换为a个规模为bn的子问题,并且最终将所有子问题合并所需要的时间,其可以表示为f(n)=nd。
因此上式可以表示为
T(n)=aT(bn)+nd
归并排序的时间复杂度计算方法
对于引言中的归并排序,由于其转换为两个子问题,因此规模变为原来的一半,即2n。分解和合并所有子问题的时间复杂度为O(n),因此该归并排序的递归的时间复杂度公式可以表示为:T(n)=2T(2n)+O(n)
主定理证明
设:一个递归关系式有如下表示:
T(n)=aT(bn)+nd
在第0层中,所有子问题合并需要花费时间为:nd
在第1层中,所有子问题合并需要花费时间为:子问题数量*每个子问题的子问题合并时间,即一共有a个子问题*每个子问题花费的时间(bn)d=O(a∗(bn)d))
在第2层中,所有子问题合并需要花费时间为:子问题数量*每个子问题的子问题合并时间,即一共有(a*a个子问题)*(每个子问题花费的时间(b2n)d,其等于a∗a∗(b2n)d=O(a2∗(b2n)d))
以此类推,最后合成第i层需要花费的时间为:ai∗(bin)d
其每一层的复杂度表示如下表
层数 | 复杂度 |
---|
0 | nd |
1 | a∗(bn)d |
2 | a2∗(b2n)d |
… | … |
i | ai∗(bin)d |
其分别对应不同层的内容
在第i层中,,每个问题的规模都是1。那么,从下往上推导,可知,在第i-1层中,所有子问题的规模为b,第i-2层中所有子问题的规模为b2,以此类推,则第0层(即顶层)的规模应该为bi。
而又假设可知,最顶层的问题规模为n。
由此可以推得:bi=n
∴i=logbn
∴T(n)=nd+a∗(bn)d+a2∗(b2n)d+...+ai∗(bin)d=nd∗((bda)0+(bda)1+...+(bda)i)
即:T(n)=i=0∑i=iai∗(bin)d=nd∗i=0∑i=i(bda)i
令q=bda,则T(n)=nd∗i=0∑i=iqi
∴T(n)=nd∗(q0+q1+...+qn)
此时需要分情况讨论q的取值,以1位分界线,因此共需要讨论3中情况:
①q=1,即a=bd
则T(n)=nd(i+1)=nd(logbn+1)=O(nd∗logbn) (此处忽略了常数1)
当q=1时,(q0+q1+...+qn)部分构成等比数列,因此此部分可以用等比数列求和公式Sn=1−qa1(1−qn) 进行计算
在这里Sn=1−qa1(1−qn)=1−qq0(1−qi+1)=1−q1−qi+1
② 当q<1时,即a<bd时,i+1非常大,因此(i+1)→∞limqi+1=0,所以(i+1)→∞lim1−qi+1=1
则T(n)≈1−qnd=C∗nd=O(nd)
③ 当q>1时,即a>bd时,i+1非常大,因此(i+1)→∞limqi+1=∞,所以(i+1)→∞lim1−qi+1=−qi+1,(i+1)→∞lim1−q=−q
则T(n)≈nd∗1−q1−qi+1
=nd∗qi
=nd⋅(bda)logbn
=nd⋅b(logbn)⋅nalogbn
=alogbn
根据对数运算性质alogbn=nlogba,可得:
=nlogba
综上①②③可得:
T(n)=⎩⎨⎧O(nd)O(ndlogbn)O(nlogba)a<bda=bda>bd
应用和例题
1、T(n)=3T(n/2)+n2
nlogba=nlog23≈n1.6
f(n)=n2
因此:nlogba≈n1.6<n2=f(n),即nlogba<f(n)。此时满足第三种情况,即T(n)=f(n)=O(n2)
2、T(n)=4T(2n)+n2
nlogba=nlog24=n2
f(n)=n2
因此:nlogba=n2=f(n),即nlogba=f(n)。此时满足第二种情况,即T(n)=O(ndlogbn)=O(n2log2n)
3、T(n)=2T(2n)+n
此时:a = 2, b = 2
nlogba=nlog22=n=f(n) => Case 2
∴T(n)=O(n1log2n)=O(n⋅log2n)
参考
[bilibili]【学习记录】分治和主定理的推导-计算机算法:https://www.bilibili.com/video/BV1Q7411R7rk
[CSDN]Master—Theorem 主定理的证明和使用:https://blog.csdn.net/jmh1996/article/details/82827579
[wikipedia]Divide-and-conquer_algorithm:https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm
[cnblogs]图解排序算法(四)之归并排序:https://www.cnblogs.com/chengxiao/p/6194356.html
[bilibili]贪心 NLP 自然语言处理:https://www.bilibili.com/video/BV1yK4y1E7n4
[wikipedia]Master theorem (analysis of algorithms):https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms)