算法复杂度的分析——时间复杂度和空间复杂度

算法的复杂度

如何分析一个算法的复杂度?

算法的时间复杂度和空间复杂度统称为算法的复杂度

时间复杂度:

下面代码的循环语句总共会执行多少次?

void Test(int n) 
{ 
	int iConut = 0; 
	for(int i = 0; i < n; ++i) 
	{ 
		for(int j = 0; j < n; ++j) 
		{ 
			iCount++; 
		} 
	} 
	for(int k = 0; k < 2*n; ++k)
	{ 
		iCount++; 
	} 
	int count = 10;
	while(count--) 
	{ 
		iCount++; 
	} 
}
语句总执行次数:f(n) =   n^2 + 2*n + 10
时间复杂度实际就是一个函数,该函数计算的是执行基本操作的次数

注意:此时使用执行次数来衡量算法的好坏,是因为在不同的环境下面,同一段代码的执行效率也会有所不同,比如:在普通笔记本电脑运行10s的程序,在超级计算机上可能0.1s都不到就可以 运行出结果。

算法存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数,通常最好情况不会出现
(下界)
例如:在一个长度为N的线性表中搜索一个数据x
最好情况:1次比较
最坏情况:N次比较
平均情况:N/2次比较
在实际中通常关注的是算法的最坏运行情况,即:任意输入规模N,算法
的最长运行时间。理由如下:
①一个算法的最坏情况的运行时间是在任意输入下的运行时间上界
②对于某些算法,最坏的情况出现的较为频繁
③大体上看,平均情况与最坏情况一样差
因此:一般情况下使用O渐进表示法来计算算法的时间复杂度

此处有一段关于时间复杂度的解释,我觉得很贴切,大家可以参考一下:

(转载)一个算法语句总的执行次数是关于问题规模N的某个函数,记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化时,T(N)也在变化,算法执行次数的增长速率和f(N)的增长速率相同。则有T(N) =O(f(N)),称O(f(n))为时间复杂度的O渐进表示法。


常见的时间复杂度:

void Test0(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
} 
此时循环执行10次,为常数次,则时间复杂度为:O(1);

void Test1(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int iIdx = 0; iIdx < 2*n; ++iIdx) 
	{ 
		iCount++; 
	}
}
此时:循环执行10+2*n次,常数次和常数系数不计算在时间复杂度之内,则时间复杂度为:O(n);

void Test2(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int iIdx = 0; iIdx < 2*n; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int i = 0; i < n; ++i) 
	{ 
		for (int j = 0; j < n; ++j) 
		{ 
			iCount++; 
		} 
	} 
} 
程序循环执行次数为:10+2*n+n^2

出去常数和常数系数,选择增长最快的一部分,作为时间复杂度:O(n^2)

void Test3(int m, int n) 
{ 
 int iCount = 0; 
 for (int i = 0; i < m ; ++i) 
 { 
 iCount++; 
 } 
 for (int k = 0; k < n ; ++k) 
 { 
 iCount++; 
 } 
} 
此时有m 和n两个不确定的循环上界,则此时的时间复杂度为:O(m+n);

void Test4(int m, int n)// f(n,m) = 2*m*n == O(m*n)
{ 
 int iCount = 0; 
 for (int i = 0; i < 2*m ; ++i) 
 { 
 	for (int j = 0; j < n ; ++j) 
 	{ 
 		iCount++; 
 	} 
 } 
} 
此时m和n是两个嵌套的位置循环次数,则时间复杂度为:O(m*n);


一般算法O(n)计算方法:
①用常数1取代运行时间中的所有加法常数
②在修改后的运行次数函数中,只保留最高阶项
③如果最高阶项系数存在且不是1,则去除与这个项相乘的常数


分治算法的时间复杂度:

我们用简单的二分查找为例子,(下面为二分查找的代码):

#include 
#include 
int BinarySearch(int *a ,size_t size,int x){
	size_t mid = 0;
	size_t left = 0;
	size_t right = size -1;
	assert(a);
	while(left<=right){
		mid = left+((right - left )>>1);
		if(a[mid] < x){
			left = mid+1;
		}
		else if(a[mid] > x){
			right = mid -1;
		}
		else return mid;
	}
	return -1;
}
int main(){
	int a[10]= {1,2,3,4,5,6,7,8,9,10};
	printf("%d",BinarySearch(a,10,10));
	return 0;
}
此时:二分查找函数的参数为:要查找的数组,数组的大小,要查找的数;

我们可以把整个有序数组比作一个二叉树,根节点的左子树都小于根,右子树都大于根,二叉树有N个结点,则二叉树的高度就是:h≈log以2为底的N次方。

显然有N个结点的M叉树的高度就是log以M为底的N次方。

此时最坏的情况就是把所有的结点都了以便,即二分查找的时间复杂度就是:O(log以2为底的N次方);

我们下面给出二分查找的递归算法,以及所有的测试用例:

int BinarySearch(int* a,size_t left,size_t right, int x)//二分查找的递归算法
{
	size_t mid;
	assert(a);
	mid = left+((right - left)>>1);
	if(left > right )return -1;
	if(a[mid] > x){
		BinarySearch(a,left,mid-1,x);
	}
	else if(a[mid]		BinarySearch(a,mid+1,right,x);
	}
	else if (a[mid] == x)
		return mid;
}
int main(){
	size_t mid,left,right;
	int a[10]= {1,2,3,4,5,6,7,8,9,10};
	left = 0;
	right = 9;
	printf("%d",BinarySearch(a,left,right,10));
	printf("%d",BinarySearch(a,left,right,9));
	printf("%d",BinarySearch(a,left,right,8));
	printf("%d",BinarySearch(a,left,right,7));
	printf("%d",BinarySearch(a,left,right,6));
	printf("%d",BinarySearch(a,left,right,5));
	printf("%d",BinarySearch(a,left,right,4));
	printf("%d",BinarySearch(a,left,right,3));
	printf("%d",BinarySearch(a,left,right,2));
	printf("%d",BinarySearch(a,left,right,1));
	printf("%d",BinarySearch(a,left,right,33));
	return 0;
}
其中递归算法时间复杂度:递归总次数*每次递归次数




空间复杂度
空间复杂度:函数中创建对象的个数关于问题规模函数表达式

int Sum(int N) 
{ 
 int count = 0; 
 for(int i = 1; i <= N; ++i) 
 count += i; 
 return count; 
} 
此时程序只创建了常数个变量,则空间复杂度就是:O(1);


下面这段代码功能是将两个数组按照一定得顺序进行合并:

int* Merge(int* array1, int size1, int* array2, int size
2)
{ 
 int index1 = 0, index2 = 0, index = 0; 
 int* temp= (int*)malloc(sizeof(int)*(size1+size2)); 
 if(NULL == temp) 
 return NULL; 
 while(index1 < size1 && index2 < size2) 
 { 
 if(array1[index1] <= array2[index2]) 
 temp[index++] = array1[index1]; 
 else
 temp[index++] = array2[index2]; 
 } 
 while(index1
因为要合并,所以要创建两个数组总共大小的空间,去存放两个数组的里面的变量,所以空间复杂度为:O(size1+size2);


下面我们分析一下斐波那契数列的时间和空间复杂度:

long long Fib(int n) 
{ 
 if(n < 3) 
 return 1; 
 return Fib(n-1)+Fib(n-2); 
} 
之前有说过:其中递归算法时间复杂度:递归总次数*每次递归次数。

此时,当作一颗二叉树,第一次n=3,即根节点为n=3,左子树调用传参:n=2,右子树调用传参n=1;

则其二叉树高度为:h≈log以2为底3次方,结点个数为2^h+1,也就是也就是调用了2^n+1遍,其中1为常数,所以得:时间复杂度为O(2^N);
在调用的过程中,没有创建临时变量,则空间复杂度为:O(N);


斐波那契尾递归实现:

时间复杂度:O(n);

long Fib(long first, long second, long N) 
{ 
 if(N < 3) 
 return 1; 
 if(3 == N) 
 return first+second; 
 return Fib(second, first+second, N-1); 
} 

对于尾递归的空间复杂度,是和编译器有一定关系的,在VS环境运行代码,编译器是会对尾递归进行优化

假设我们在main函数中调用Fib函数:Fib(1,1,10);则编译器会在栈中为Fib(1,1,10)创建一个栈区,

但是在调用Fib(1,2,9)的时候,此时函数已经不会再对之前的数值在做任何的操作了,所以在函数一层一层递归之后,返回的时候,不再需要空间对之前的值进行更改,所以编译器不会在给Fib(1,2,9)这个函数开辟新的栈,直接就在Fib(1,1,10)的栈区进行操作。

总结:在编译器对尾递归进行优化的时候,空间复杂度为:O(1);如果不做优化的话,那么空间复杂度就是:O(n)。

在给出非递归实现:

long Fibonacci(int n) {
      if (n <= 2)
          return 1;
      else {
          long num1 = 0;
          long num2 = 1;
         for (int i = 2;i < n - 1;i++) {
             num2 = num1 + num2;
             num1 = num2 - num1;
         }
         return num2;
     }
 }

限于编者水平,有很多的不正确的地方,欢迎各位前来指正!



















你可能感兴趣的:(c语言)