当前位置:   article > 正文

数据结构C语言版 —— 时间复杂度&空间复杂度概念和计算_c语言时间复杂度

c语言时间复杂度


时间复杂度&空间复杂度

1. 算法效率

算法效率分析一般分为两种,一种是时间效率,另外一种是空间效率。时间效率被称为时间复杂度,空间效率则被称为空间复杂度。时间复杂度是用来衡量一个算法的运行速度,而空间复杂度主要是用来衡量一个算法的所需要的额外空间,早期的计算机存储容量很小,所以比对空间复杂度很是在乎。但是随着计算机的叙述发展,计算机的存储已经到了一个很高的程度,比如现在的一台笔记本至少都是16G内存+512G磁盘,服务甚至是几百个G的内存,几百T的磁盘。所以现在并不那么关心空间复杂度,也经常出现空间换时间的做法。

2. 时间复杂度

1) 时间复杂度的概念

时间复杂度的一个定义:在计算机科学中,算法的时间复杂度是一个函数(一个数学函数),它定量描述了一个算法执行所消耗的时间,从理论上来说,是不能计算出来的,只有当你把代码放到机器上跑才能知道运行时间,但是通过机器测试这种方式明显不现实,因为每台计算机的配置都不一定相同,所以才有了时间复杂度的分析方式。一个算法所花费的时间与其语句的执行次数成正比,算法中的基本操作的执行次数,为算法的时间复杂度

2) 大O的渐近表示法

来看一段代码

void func1(int N)
{
    int count = 0;
    int i = 0;
    for (i = 0; i < N ; i++) //执行N次
    {
        for (int j = 0; j < N ; j++) //执行N次
        {
            count++;
        }
    }
    int k = 0;
    for (k = 0; k < 2 * N ; k++) //执行2*N次
    {
        count++;
    }
    int M = 10;
    while (M--) //执行10次
    {
        count++;
    }
    printf("%d\n",count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

那么我们来计算一下这个代码的运行次数就是

f ( N ) = N 2 + 2 ∗ N + 10 f(N) = N^{2}+2*N+10 f(N)=N2+2N+10

但实际我们计算时间复杂度时,并不一定要计算的那么精确,而是只计算大概的执行次数.

我看到func1的执行次数,如果当我们的N非常大时,假设N = 1000,那么这里的+10是可以忽略了,因为 100 0 2 = 1000000 1000^{2}=1000000 10002=1000000,在一百万面前+10可以说是微乎其微了,所以+1和+10没什么区别。同理 2 ∗ N 2*N 2N也是一样的,当N足够大趋近于无穷时, 2 ∗ N 2*N 2N也时微乎其微了。

那么就可以使用大O的渐近表示法

O O O符号:是用于描述函数渐进行为的数学符号

推到大 O O O阶方法

  1. 用常数1取代运行时间汇总的所有加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶存在且不是1,则去除与这个项数相乘的常数,得到的结果就是大 O O O

通过上面的方法来推导一下

用常数1取代运行时间汇总的所有加法常数

f ( N ) = N 2 + 2 ∗ N + 1 f(N) = N^{2}+2*N+1 f(N)=N2+2N+1

在修改后的运行次数函数中,只保留最高阶项

f ( N ) = N 2 f(N) = N^{2} f(N)=N2

这里的最高阶项不是1,所以func1函数的时间复杂度就是 O ( N 2 ) O(N^{2}) O(N2)

O O O渐进表示法去掉了那些对结果影响不大的项数,只保留了最影响结果的那一项

另外有些算法存在着,最好、平均和最坏情况

  • 最坏情况:任意输入规模的最大运行次数(上界)
  • 平均情况:任意输入规模的期望运行次数
  • 最好情况:任意输入规模的最小运行次数(下界)

举个例子:

假设在一个数组中查找一个数字。

  • 最好情况:1次找到,数组第一个数子就是我们要找的, O ( 1 ) O(1) O(1)
  • 最坏情况:最后一个是我们要找的, O ( n ) O(n) O(n)
  • 平均情况:注意:这里的平均情况并不是最好和最坏情况相加的平均值,而是我们期望运行的次数,有时候平均情况可能和最好或者是最坏情况一样。

我们平常嘴上所说的时间复杂度就是最坏情况的时间复杂度

3) 时间复杂度案例举例

实例1

void Func2(int N)
{
    int count = 0;
    for (int k = 0; k < 2 * N ; ++ k)
    {
    	++count;
	}
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

粗略计算就是 f ( N ) = 2 ∗ N + 10 f(N) = 2*N+10 f(N)=2N+10

在修改后的运行次数函数中,只保留最高阶项,如果最高阶存在且不是1,则去除与这个项数相乘的常数

那么这个代码的时间复杂度就是 O ( N ) O(N) O(N)

实例2

void Func3(int N, int M)
{
    int count = 0;
    for (int k = 0; k < M; ++ k)
    {
        ++count;
    }
    for (int k = 0; k < N ; ++ k)
    {
        ++count;
    }
	printf("%d\n", count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这种情况有一点特殊,因为不知到N和M谁大,所以对于这种不确定谁对结果的影响大,就都需要保留下来。所以这个代码的时间复杂度就是 O ( N + M ) O(N+M) O(N+M)

如果可以确定N远远大于M那么时间复杂度就是 O ( N ) O(N) O(N)

实例3

void Func4(int N)
{
    int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
        ++count;
    }
    printf("%d\n", count);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

对于这种场数次数的时间复杂度就是 O ( 1 ) O(1) O(1)

实例4

int find(int* arr, int N int key)
{
	assert(arr);
	int i = 0;
	for (i = 0; i < N; i++)
	{
		if (arr[i] == key)
		{
			return i;
		}
	}
	return -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这个代码是在一个数组中查找一个数字,对于这种情况就是直接取它的最坏情况的时间复杂度,就是 O ( N ) O(N) O(N)

实例5

void BubbleSort(int* arr, int n)
{
    assert(arr);
	int i = 0;
	for (i = 0; i < n-1; i++)//排序趟数
	{
		int flag = 0;
		int j = 0;
		for (j = 0; j < n - 1 - i; j++)//比较次数
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
    
}
  • 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

这是冒泡排序的代码,冒泡排序的时间复杂度也是比较特殊的。

冒泡排序的外层循环是排序的趟数,每一趟冒泡排序都会确定一个数字的位置。所以每一趟排序后比较的次数都要减去1。

那么计算的次数就是 ( N − 1 ) + ( N − 2 ) + ( N − 3 ) . . . + 2 + 1 (N-1) + (N-2) + (N-3)... + 2 + 1 (N1)+(N2)+(N3)...+2+1

这是要给等比数列,通过等比数列的前N项和公式得出 N ∗ ( N − 1 ) 2 \frac{N*(N-1)}{2} 2N(N1)

通过大 O O O渐进法得出冒泡排序的时间负责度就是 O ( N 2 ) O(N^{2}) O(N2)

实例6

void BinarySearch(int* arr, int size, int key)
{
    assert(arr);
	int left = 0;
	int right = size - 1;
	int mid = 0;
	while (left < right)
	{
		mid = (right - left) / 2 + left;
		if (arr[mid] < key)
		{
			left = mid + 1;
		}
		else if (arr[mid] > key)
		{
			right = mid - 1;
		}
		else
		{
			return mid;
		}

	}

	return -1;
    
}
  • 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

这是一个二分查找的代码,二分查找一次砍掉数组的一半元素。那么查找的次数就是 N / 2 / 2 / 2 / 2... / 2 = 1 N/2/2/2/2.../2 = 1 N/2/2/2/2.../2=1,找了 x x x次,则 / 2 /2 /2 x x x次,那么长度为 N N N的数组最坏则要查找次数就是, N = 2 x N = 2^{x} N=2x,转为对数的形式就是 x = log ⁡ 2 N x = \log_{2}{N} x=log2N

所以二分查找的时间复杂度就是 O ( log ⁡ 2 N ) O(\log_{2}{N}) O(log2N)

实例7

long long Factorial(size_t N)
{
	return N < 2 ? N : Factorial(N-1)*N;
}
  • 1
  • 2
  • 3
  • 4

这是一个递归计算N的阶层的代码,这个代码的时间负责度又是多少呢?

递归算法的时间复杂度 = = = 递归次数 * 每次递归函数中的执行的次数

这里的递归次数是 N − 2 N-2 N2,每次递归函数中的执行次数就是1,那么这个代码的时间复杂度就是 O ( N ) O(N) O(N)

实例8

long long Fibonacci(size_t N)
{
	return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
  • 1
  • 2
  • 3
  • 4

这是递归计算斐波那契数列的函数

在这里插入图片描述

递归计算斐波那契数列,类似于一颗二叉树。假设这一棵二叉树是满二叉树。

那么第一层计算次数就是 2 0 2^{0} 20,第二层就是 2 1 2^{1} 21,第三层就是 2 2 2^{2} 22,以此类推…,那么函数每一次的执行次数就是 f ( N ) = 2 N − 1 f(N) = 2^{N-1} f(N)=2N1,根据等比数列前N项和公式$S_{n}\frac{a_{1}(1-q^{n} ) }{1-q} $,它的准确的时间复杂度就是 2 N − 1 − 空缺 2^{N}-1-空缺 2N1空缺

通过大 O O O渐进法推导后,这个代码的时间复杂度就是 O ( 2 N ) O(2^{N}) O(2N)

3. 空间复杂度

1) 空间复杂度概念

空间复杂度是衡量一个算法在运行过程中临时占用存储空间大小。空间复杂度不是很细致的计算一个代码所占用多少个字节的空间,而是计算变量的个数。空间复杂度基本和时间复杂度的计算方法类似,也是使用 O O O渐进表示法

2) 计算实例

实例1

void BubbleSort(int* arr, int n)
{
    assert(arr);//1
	int i = 0;//1
	for (i = 0; i < n-1; i++)//排序趟数
	{
		int flag = 0;//1
		int j = 0;//1
		for (j = 0; j < n - 1 - i; j++)//比较次数
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];//1
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
    
}
  • 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

这个冒泡排序的空间复杂度为 1 + 1 + 1 + 1 + 1 1+1+1+1+1 1+1+1+1+1

加起来都是常数,所以空间复杂度就是 O ( 1 ) O(1) O(1),因为arr数组是从外面传递过来的,不是我们创建的,所以不算入时间复杂度。那么每次循环创建的变量不需要记录进去吗?因为变量用完就会自动回收的,所以也是不算进去的。

时间复杂度考虑的是算法运行中需要额外创建的空间

实例2

long long* Fibonacci(size_t n)
{
    if(n==0) return NULL;
    long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
    fibArray[0] = 0;
    fibArray[1] = 1;
    for (int i = 2; i <= n ; ++i)
    {
    	fibArray[i ] = fibArray[ i - 1] + fibArray [i - 2];
    }
    return fibArray ;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里是一个计算斐波那契数列的函数,这里很明显创建了额外的空间。创建了一个大小为n的数组。所以这个代码的空间复杂度为O(N)

实例3

long long Factorial(size_t N)
{
	return N < 2 ? N : Factorial(N-1)*N;
}
  • 1
  • 2
  • 3
  • 4

这是一个递归求阶乘的代码,每次递归都会在栈上开辟空间,也就是开辟栈帧。一共要开辟 N − 1 N-1 N1层,所以通过大 O O O渐进法推导出这个代码空间复杂度就是 O ( N ) O(N) O(N)


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/552304
推荐阅读
相关标签
  

闽ICP备14008679号