时间复杂度与空间复杂度

数据结构初阶——时间复杂度与空间复杂度

目录

数据结构初阶——时间复杂度与空间复杂度

一、数据结构前言

1.数据结构与算法的概念

二、算法的效率

1.算法的复杂度

三、算法的时间复杂度

1.时间复杂度的概念

2.大O的渐进表示

3.常见的时间复杂度举例

四、算法的空间复杂度

1.空间复杂度的概念

2.常见的空间复杂度举例

五、常见的时间复杂度对比

六、结语


一、数据结构前言

1.数据结构与算法的概念

        在了解时间复杂度与空间复杂度之前。我们有必要前简单了解一下数据结构和算法的概念。那什么是数据结构呢?有关定义是这么说的:数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。通俗的说:数据结构主要指的是在内存当中以某种组织的形式(c语言中是用结构体定义的)对数据进行管理(这里的管理主要是指增删查改)。那么,什么是算法呢?算法(Algorithm)是定义良好的计算过程,它取一个或一组的值作为输入,并产生出一个或一组值作为输出。通俗来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。在简单了解数据结构与算法的概念后,我们就可以展开下面对时间复杂度与空间复杂度的探讨了。

二、算法的效率

        前面我们简单讲诉了算法的概念,既然算法是定义良好的计算过程,那么它就存在好坏之分。我们看下面一个关于斐波那契数列的函数

long long Fib(int N)
{
	if (N < 3)
		return 1;

	return Fib(N - 1) + Fib(N - 2);
}

        可以看出,这样使用递归方式写的求斐波那契数列的函数看上去非常简洁。但简洁真的就一定好吗?怎样才算是一个好的算法,从而引出一个概念,算法的复杂度

1.算法的复杂度

        算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

        时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

三、算法的时间复杂度

1.时间复杂度的概念

        定义如下:在计算机科学中,算法的时间复杂度是一个函数(这里的函数指的是数学中的函数,并不是c语言当中的函数),它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是如果我们每个程序都上机测试的话,会非常的麻烦,于是就有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度

       即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

        看下面的一个例子:

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++j)
		{
			++count;
		}
	}

	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}
	int M = 10;
	while (M--)
	{
		++count;
	}
	printf("%d\n", count);
}

        我们可以以函数的形式将其表示出来,其中函数的未知数是N,简单的计算得知:F(N)=N*N+2*N+10

        现在针对这个函数关于N的取值进行一个简单的分析

F(N)=N*N+2*N+10
N=10F(N)=130
N=100F(N)=10210
N=1000F(N)=1002010
         实际中我们计算时间复杂度时,并不一定要计算精确的执行次数,而只需要大概执行次数那么这里我们使用大O的渐进表示法

2.大O的渐进表示

大O符号(Big O notation):是用于描述函数渐进行为的数学符号

推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数中,只保留最高阶项( 因为其他项对结果的影响不大
  3. 如果最高阶存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法后,则刚刚Func1的时间复杂度可以表示为

N=10F(N)=100
N=100F(N)=10000
N=1000

F(N)=1000000

        通过上面两个表格的对比,我们可以发现当N的值逐渐增大时,两者的F(N)越来越接近。时间复杂度并不是需要精确的计算时间,而是计算时间所在的量级。大O的渐进表示法去掉了那些对结果影响不大的项,更加简洁的表示出了执行次数。

        而对于有些算法的时间复杂度,存在着最好,平均和最坏的情况:

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

        这里以在一个长度为N的数组中搜索一个数据x为例:

  • 最好情况:1次找到
  • 最坏情况:N次找到
  • 平均情况:N/2次找到

        在实际情况中,我们关注的算法的最坏运行情况,所以这个例子的时间复杂度为O(N)

3.常见的时间复杂度举例

 实例1:

// 计算Func2的时间复杂度?
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);
}

        我们来看这串代码,计算时间复杂度首先需要找到基本语句,可以很容易看出是++count,在for循环中,基本语句被执行了2N次,在while循环中,基本语句被执行了10次,根据大O阶的渐进表示规则,只保留最高阶项,并且将系数设置为1。得出时间复杂度为O(N)。(这里再次强调,O(N)计算的是量级,而不是一个具体的式子

实例2:

// 计算Func3的时间复杂度?
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);
}

        这里可以看出来,基本语句是++count,时间复杂度是O(N+M)。(这是需要说明的是,时间复杂度非常喜欢用N这个未知数,但是可以有其他未知数)这两项并没有说谁的阶数更高,所以N和M都不可以去除。

 实例3:

// 计算Func4的时间复杂度?
void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}
	printf("%d\n", count);
}

         这里可以看出来,基本语句跑了100次,但是根据大O阶的第一条渐进表示规则,常数统一写做O(1)。换言之,对于大O阶,O(1)代表的不是1次,而是常数次。(其实,现在的计算机运算速度足够快,循环1次,100次,甚至1万次所花费的时间基本没有区别,这也为大O阶的表示规则提供了技术支持)

实例4:

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	// [begin, end]:begin和end是左闭右闭区间,因此有=号
	while (begin <= end)
	{
		int mid = begin + ((end - begin) >> 1);
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = mid - 1;
		else
			return mid;
	}
	return -1;
}

        这是二分查找的代码。使用二分查找,数组必须有序,最开始查找范围是N,每一次查找都会使查找范围缩小一半,最坏的情况是当这个空间只有一个值才查找到。假设查找了x次,N=1*2*2*2……*2(乘了x次2),那么N=2^x,x就是,那么时间复杂度就是O(logN)。(这里有人会好奇,log下标的2我怎么没有写出来,其实是这样的,在时间复杂度涉及对数时,大部分情况下是以2为底的对数,所以对于O阶表示,如果log没有下标就默认下标为2,仅限于O阶表示,数学考试的时候课千万别忘写呀

        一些关于二分查找的小补充:二分查找是一种效率非常高的算法,比如对于一个长度为100万的数组,倘若使用一般的查找一个一个遍历过去,则最坏需要100万次。但是如果采用二分查找,最坏仅大约需要20次(我们把2的10次方近似为1000),如果长度为10亿,普通查找最坏需要10亿次,而二分查找最坏仅需要约30次。这样对比下来,二分查找的效率之高令人震惊。不过二分查找也有其局限性,它所操作的数组必须是升序或者降序的数组,而给数组排序可不比查找来的简单。所以对于查找这种操作,二分查找用的并不算多,后面我会专门更新排序和查找的博客,请大家点赞收藏关注,敬请期待……

实例5:

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
	if (1 == N)
		return 1;

	return Fac(N - 1) * N;
}

         这里是关于阶乘的代码,对于一个参数N,从N到1,一共是调用了N次(见下面的图片),每次调用是执行了常数次,那么这个阶乘的算法时间复杂度就是O(N)。

         递归时间复杂度的计算方法和技巧:每次递归调用的执行次数累加。

四、算法的空间复杂度

1.空间复杂度的概念

        有关空间复杂度的定义如下:

  • 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。
  • 空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
  • 空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
        注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
        其实,有了前面时间复杂度的铺垫,空间复杂度的计算就会容易很多。下面我们看几个例子,简单梳理一下就可以了。

2.常见的空间复杂度举例

实例1:

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

        我们计算空间复杂度需要找到算法额外消耗的空间,函数原本所带的空间是不算的。那么在这个算法中,有哪些是额外开辟的呢,比如说end,exchange,i,总共是开辟了三个额外空间,也就是常数个,所以空间复杂度就是O(1)。

        这里需要简单的提醒一下,重复创建同一个变量算作同一个,就比如exchange这个变量,在循环中被创建了很多次,但只算做一个额外开辟的空间。

实例2:

// 计算Fibonacci的空间复杂度?
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;
}

        还是和刚才的一样的思路,找寻额外开辟的空间,我们可以看到第7行关于数组的动态开辟,开辟的是n+1个额外空间,既然有了N这个量级,那么后面的常数量级就可以忽略不记了。也就是O(N)。

实例3:

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
	if (N == 1)
		return 1;

	return Fac(N - 1) * N;
}

        这是递归求n的阶乘的函数递归调用了N次,开辟了N个栈帧(关于函数栈帧,后面我也会专门写一期博客,敬请期待),每个栈帧使用了常数个空间。空间复杂度为O(N)。

        对于递归函数求空间复杂度,其实和求时间复杂度的方法类似,是对每次递归调用的变量个数的累加。

五、常见的时间复杂度对比

        一般算法常见的时间复杂度对比如下:

O(1)常数阶
O(n)线性阶
O(n^2)平方阶
O(logn)对数阶
O(nlogn)nlogn阶
O(n^3)立方阶
O(2^n)指数阶

六、结语

        在文章的一开始,先是简单介绍了一下数据结构与算法的概念,然后从定义开始,结合例子,介绍时间复杂度与空间复杂度(这里简单的提醒一下,时间复杂度要比空间复杂度来的重要,要重点理解)。最后是简单的罗列了常见的时间复杂度,让大家加深印象。同时,笔者认为文章的重点之处,均以用加粗或者黄色标记,日后的文章也大概率会采用这种风格。

        这篇文章不足之处,还请读者多多批评指正,日后会继续更新数据结构的相关知识(下一篇是顺序表),敬请期待。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值