分治是一种解决复杂问题的思想,它可以将一个问题划分成多个小的问题,通过合并这些问题求得原问题的解。本文对分治法进行复杂性分析,并通过这种方法分析几个具体算法的时间复杂度。
1 分治法的复杂性分析
分治法可以将规模为
n
n
n 的问题分成
k
k
k 个规模为
n
m
\frac {n}{m}
mn 的子问题来求解。设分解阈值
n
0
=
1
n_0=1
n0=1,且用解最小子问题的算法求解规模为1的问题耗费1个单位时间。再设将原问题分解为
k
k
k 个子问题以及将
k
k
k 个子问题的解合并为原问题的解需用
f
(
n
)
f(n)
f(n) 个单位时间。用
T
(
n
)
T(n)
T(n) 表示该分治法解规模为
n
n
n 的问题所需的计算时间,则有:
T
(
n
)
=
{
O
(
1
)
n
=
1
k
T
(
n
m
)
+
f
(
n
)
n
>
1
T(n)=\begin{cases} O(1)&n=1\\ kT(\frac{n}{m})+f(n)&n>1\\ \end{cases}
T(n)={O(1)kT(mn)+f(n)n=1n>1
则
T
(
n
)
=
n
l
o
g
m
k
+
∑
j
=
0
l
o
g
m
(
n
)
−
1
k
j
f
(
n
m
j
)
(1.1)
T(n)=n^{log_m k}+\sum_{j=0}^{log_m(n)-1}k^j f(\frac{n}{m^j})\tag{1.1}
T(n)=nlogmk+j=0∑logm(n)−1kjf(mjn)(1.1)
《算法导论》在推导式(1.1)的过程中使用了递归树,这里尝试用另一种方法推导:
当
n
>
1
n>1
n>1 时,
T
(
n
)
=
k
T
(
n
m
)
+
f
(
n
)
,
T(n)=kT(\frac{n}{m})+f(n),
T(n)=kT(mn)+f(n),
设
n
=
m
t
n=m^t
n=mt, 令
G
(
t
)
=
T
(
m
t
)
=
T
(
n
)
G(t)=T(m^t)=T(n)
G(t)=T(mt)=T(n),则
T
(
m
t
)
=
k
T
(
m
t
−
1
)
+
f
(
m
t
)
G
(
t
)
=
k
G
(
t
−
1
)
+
f
(
m
t
)
=
k
(
k
G
(
t
−
2
)
+
f
(
m
t
−
1
)
)
+
f
(
m
t
)
=
k
2
G
(
t
−
2
)
+
k
f
(
m
t
−
1
)
+
f
(
m
t
)
=
⋯
=
k
t
G
(
0
)
+
k
t
−
1
f
(
m
)
+
k
t
−
2
f
(
m
2
)
+
⋯
+
k
f
(
m
t
−
1
)
+
f
(
m
t
)
=
k
t
G
(
0
)
+
∑
j
=
0
t
−
1
k
j
f
(
m
t
−
j
)
=
k
l
o
g
m
n
+
∑
j
=
0
l
o
g
m
(
n
)
−
1
k
j
f
(
n
m
j
)
=
n
l
o
g
m
k
+
∑
j
=
0
l
o
g
m
(
n
)
−
1
k
j
f
(
n
m
j
)
\begin{split} T(m^t)&=kT(m^{t-1})+f(m^t)\\ G(t)&=kG(t-1)+f(m^t)\\ &=k(kG(t-2)+f(m^{t-1}))+f(m^t)\\ &=k^2G(t-2)+kf(m^{t-1})+f(m^t)\\ &=\cdots\\ &=k^tG(0)+k^{t-1}f(m)+k^{t-2}f(m^2)+\cdots+kf(m^{t-1})+f(m^t)\\ &=k^tG(0)+\sum_{j=0}^{t-1}k^jf(m^{t-j})\\ &=k^{log_mn}+\sum_{j=0}^{log_m(n)-1}k^jf(\frac{n}{m^j})\\ &=n^{log_mk}+\sum_{j=0}^{log_m(n)-1}k^jf(\frac{n}{m^j})\\ \end{split}
T(mt)G(t)=kT(mt−1)+f(mt)=kG(t−1)+f(mt)=k(kG(t−2)+f(mt−1))+f(mt)=k2G(t−2)+kf(mt−1)+f(mt)=⋯=ktG(0)+kt−1f(m)+kt−2f(m2)+⋯+kf(mt−1)+f(mt)=ktG(0)+j=0∑t−1kjf(mt−j)=klogmn+j=0∑logm(n)−1kjf(mjn)=nlogmk+j=0∑logm(n)−1kjf(mjn)
因此得到式(1.1):
T
(
n
)
=
n
l
o
g
m
k
+
∑
j
=
0
l
o
g
m
(
n
)
−
1
k
j
f
(
n
m
j
)
T(n)=n^{log_mk}+\sum_{j=0}^{log_m(n)-1}k^jf(\frac{n}{m^j})
T(n)=nlogmk+j=0∑logm(n)−1kjf(mjn)
计算时间复杂度时,可以直接将
n
,
m
,
k
n,m,k
n,m,k 代入公式求解。
2 经典算法分析
2.1 二分搜索
二分搜索是在一个有序序列 a [ 0 : n ] a[0:n] a[0:n] 中找出某个特定元素 x x x 的方法,如果找到就返回该元素在序列中的位置,若序列中没有该元素就返回查找失败。以升序序列为例,在搜索时,每次将所找元素 x x x 与序列中间元素 m i d mid mid 作比较,如果 x = = m i d x==mid x==mid ,就返回该位置;如果 x < m i d x<mid x<mid ,说明所找元素可能在 m i d mid mid 前面,就在 a [ 0 : m i d ] a[0:mid] a[0:mid] 中以同样方法继续寻找;如果 x > m i d x>mid x>mid ,说明所找元素可能在 m i d mid mid 后面,就在 a [ m i d : n ] a[mid:n] a[mid:n] 中以同样方法继续寻找。代码(C语言)如下:
int binary_search(int a[], int num, int low, int high)
//a:要查找的序列,num:要查找的数,low:查找序列的第一个元素,high:查找序列的最后一个元素
{
int mid = 0;
while (low <= high)
{
mid = (low + high) / 2;
if (num == a[mid]) return mid;
else if (num < a[mid]) high = mid - 1;
else low = mid + 1;
}
return -1;
}
二分搜索问题将1个规模为
n
n
n 的问题分成了1个规模为
n
2
\frac{n}{2}
2n 的子问题,划分问题的代价来源于数的比较,并且最终无需合并,所以分解1个问题和合并为1个问题只需要常数数量级的时间,即
f
(
n
)
=
c
f(n)=c
f(n)=c ,由式(1.1)可知二分搜索所需时间为
T
(
n
)
=
n
l
o
g
2
1
+
∑
j
=
0
l
o
g
2
(
n
)
−
1
c
=
1
+
c
l
o
g
2
n
\begin{split} T(n)&=n^{log_21}+\sum_{j=0}^{log_2(n)-1}c\\ &=1+clog_2n \end{split}
T(n)=nlog21+j=0∑log2(n)−1c=1+clog2n
那么二分搜索的时间复杂度就是
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n).
2.2 两路归并排序
两路归并排序的过程中,一个无序的序列 a [ l o w , h i g h ] a[low,high] a[low,high]将会被拆分成两个相同规模的序列 a [ l o w , m i d ] a[low,mid] a[low,mid], a [ m i d + 1 , h i g h ] a[mid+1,high] a[mid+1,high],再分别对这两个序列排序。排序完成后,这两个序列分别有序,将两序列合并即可完成排序。C语言代码如下:
void merge(int a[], int tmp[], int low, int mid, int high)
//合并算法,a[low,mid]有序,a[mid+1,high]有序,merge函数使得a[low,high]有序
{
int i, j, k;
for (i = low; i <= high; i++) tmp[i] = a[i];
//将a[low,high]复制到tmp[low,high]中
i = low, j = mid+1, k = low;
//i,j用来指示数组tmp中的位置,k用来指示数组a中的位置
while (i <= mid && j <= high)
{
if (tmp[i] <= tmp[j])
//从tmp的两个有序段中挑出最小的元素放入a的下一个位置
{
a[k] = tmp[i];
i++;
}
else
{
a[k] = tmp[j];
j++;
}
k++;
}
while (i <= mid) //tmp[mid+1,high]已经在a中,将tmp[low,mid]中的剩余元素填入a
{
a[k] = tmp[i];
i++, k++;
}
while (j <= high) //tmp[low,mid]已经在a中,将tmp[mid+1,high]中的剩余元素填入a
{
a[k] = tmp[j];
j++, k++;
}
}
void merge_sort(int a[], int tmp[], int low, int high)
//归并排序算法,a:要排序的数组,tmp:相同长度的辅助数组,low:排序序列的第一个元素,high:排序序列的最后一个元素
{
int mid = 0, i;
if (low < high)
{
mid = (low + high) / 2;
merge_sort(a, tmp, low, mid);
merge_sort(a, tmp, mid + 1, high);
//将排序问题分成两个规模减半的子问题
merge(a, tmp, low, mid, high);
//将子问题的结果合并
}
}
两路归并排序的算法将1个规模为
n
n
n 的问题分成了2个规模为
n
2
\frac{n}{2}
2n 的子问题。拆分问题通过函数调用实现,合并问题主要把时间花费在数组
a
a
a 和数组
t
m
p
tmp
tmp 相互转移元素上,因此
f
(
n
)
=
n
f(n)=n
f(n)=n ,由式(1.1)可知两路归并排序所需时间为:
T
(
n
)
=
n
l
o
g
2
2
+
∑
j
=
0
l
o
g
2
(
n
)
−
1
2
j
n
2
j
=
n
+
n
l
o
g
2
n
\begin{split} T(n)&=n^{log_22}+\sum_{j=0}^{log_2(n)-1}2^j\frac{n}{2^j}\\ &=n+nlog_2n \end{split}
T(n)=nlog22+j=0∑log2(n)−12j2jn=n+nlog2n
那么两路归并排序的时间复杂度就是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n).