分治法的计算时间、时间复杂度推导以及经典算法分析

分治是一种解决复杂问题的思想,它可以将一个问题划分成多个小的问题,通过合并这些问题求得原问题的解。本文对分治法进行复杂性分析,并通过这种方法分析几个具体算法的时间复杂度。

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=0logm(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(mt1)+f(mt)=kG(t1)+f(mt)=k(kG(t2)+f(mt1))+f(mt)=k2G(t2)+kf(mt1)+f(mt)==ktG(0)+kt1f(m)+kt2f(m2)++kf(mt1)+f(mt)=ktG(0)+j=0t1kjf(mtj)=klogmn+j=0logm(n)1kjf(mjn)=nlogmk+j=0logm(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=0logm(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=0log2(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=0log2(n)12j2jn=n+nlog2n
那么两路归并排序的时间复杂度就是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n).

猜你喜欢

转载自blog.csdn.net/diqiudq/article/details/128589092