当前位置:   article > 正文

【数据结构】时间复杂度和空间复杂度_数据结构事件复杂度和空间复杂度

数据结构事件复杂度和空间复杂度

一、数据结构

1.什么是数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的

数据元素的集合。数据结构是带“结构”的数据元素的集合,“结构”就是指数据元素之间的关系。

数据结构包括逻辑结构和存储结构两个层次。
(1)逻辑结构:
1)逻辑结构的定义:数据的逻辑结构是从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的
2)逻辑结构的要素:1.数据元素 2.关系
3)逻辑结构的分类1 1.集合结构:数据元素除了“属于同一集合”的关系外,别无其他关系。

​ 2.线性结构:数据元素之间存在一对一的关系。

​ 3.树结构:数据元素之间存在一对多的关系。

​ 4.图结构或网状结构:数据元素之间存在多对多的关系。

4)逻辑结构的分类2 1.线性结构:线性表、栈、队列、字符串、数组、广义表

​ 2.非线性结构:树、图、集合

(2)存储结构:顺序存储结构和链式存储结构

请添加图片描述

2.什么是算法

算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为

输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

数据结构和算法二者之间是相辅相成的关系,在一个数据结构中可能会用到算法来优化,同样一个算法中也可能包含数据结构。

3.数据结构和算法的重要性

(1)在校园招聘的笔试:

目前校园招聘笔试一般采用Online Judge形式, 一般都是20-30道选择题+2道编程题,或者3-4道编程题。
请添加图片描述

可以看出,现在公司对学生代码能力的要求是越来越高了,大厂笔试中几乎全是算法题而且难度大,中小长的笔试中才会有算法题。算法不仅笔试中考察,面试中面试官基本都会让现场写代码。而算法能力短期内无法快速提高了,至少需要持续半年以上算法训练积累,否则真正校招时笔试会很艰难,因此算法要早早准备。

(2)在校园招聘的面试中:

在面试的环节中,数据结构和算法也是被经常问到,大家在牛客网、LeetCode的面经中也能够发现这一点,比如一下关于数据结构的一些问题:

1)怎么用两个栈实现一个队列。
2)如何判断两个链表是否相交
3)说明Vector和数组的区别
甚至在面试的时候可能会让你当场写算法题目,手撕快速排序、红黑树等等一些问题。

(3)在未来的工作中:

在这里我给大家推荐网上的两篇文章:[数据结构与算法对一个程序员来说的重要性?](学好算法对一个程序员来说是必须的吗?如果是,至少应该学到哪种程度? - 知乎 (zhihu.com))

学好算法对一个程序员来说是必须的吗?如果是,至少应该学到哪种程度?

4.如何学好数据结构和算法

(1)死磕代码,磕成这样就可以了

请添加图片描述

哈哈哈,这只是开个玩笑!!!对于这个建议指的是我们是需要多练习多算题,在这里我给大家推荐一些刷题的内容:《剑指offer》《程序员代码面试指南》LeetCode上面的题目。

(2)注重画图和思考

请添加图片描述

我们不论是在学习还是刷题的时候要注重画图,理清思路之后再开始写代码。

二、算法效率

1.如何衡量一个算法的好坏

如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:

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

	return Fib(N - 1) + Fib(N - 2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

这就需要了解下面的算法的复杂度。

2.算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般
是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算
机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计
算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

3.复杂度在校招中的考察

请添加图片描述

三、时间复杂度

1.时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数(数学的带未知数的函数表达式,而不是我们C语言中的函数),它定量描述了该算法的运行时间。
一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

2.大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法: 量级的估算
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。(它对结果产生了决定性的影响)
3、如果最高阶项的系数存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

3.时间复杂度的三种情况

有些算法的时间复杂度存在最好、平均和最坏情况:

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

例如:在一个长度为N数组中搜索一个数据x

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

在实际中一般情况关注的是算法的最坏运行情况。

4.常见时间复杂度计算举例

实例1:

// 请计算一下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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

计算次数时间复杂度函数表达式:F(N)=N^2+2N+10

N=10 F(N)=130
N=100 F(N)=10210
N=1000 F(n)=1002010

N越大,后两项对表达式的影响就运小,表达式中对结果影响不大的项去掉(对应大O表示法的第二点)
时间复杂度:(N^2)

实例2:


// 计算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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

计算次数时间复杂度函数表达式:F(N)=2*N+10
常数10去掉时候,N前面的系数该如何处理呢?
我们试想一下,两个穷人,一个身上有1块钱,一个身上有5角钱,是不是对他们来说都是一贫如洗呀,相反,有两个富豪,一个有10亿,一个有5亿,对于他们来说,是不是都不愁生活的问题。所以N前面的系数也需要省去。(对应大O表示法的第三点)
时间复杂度:O(N)

实例3:

// 计算Func4的时间复杂度?
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(100)
但其实它的时间复杂度是O(1)
这时候就有人提问了,100和1能一样吗,如果是10000?10000000呢?
最终的结果都是O(1),我们试想一下,珠穆朗玛峰已经很高了吧,但对于宇宙中其他的物体来说呢,可能就是很小的一个物体了。所以执行次数无论的100,还1000 只要是一个常数,那么它的时间复杂度就是O(1),成为常数阶。(对于大O表示法的第一点)
O(1)不是代表一次,而是常数次。

实例4:冒泡排序的时间复杂度

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
    assert(a);
    int i = 0;
    for(i = 0; i < n-1; i++)
    {
        int j = 0;
        int exchange = 0;
        for(j = 0; j < n - 1 - j; j++)
        {
            if(a[j] > a[j+1])
            {
                int tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = a[j];
            }
            exchange = 1;
        }
        if(exchange==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

首先我们先了解一下什么是冒泡排序
冒泡排序:它是通过两两比较相邻记录的关键字,如果为逆序,则进行交换
具体执行次数:时间复杂度计算时以最坏的情况为准,则假设数组是逆序的,那么第一次执行n-1次,第二次执行n-2次,第三次执行n-3次…直到最后全部有序,执行次数是一个等差数列,执行次数=((n-1)+1)/2。
所以冒泡排序的时间复杂度为:O(N^2)

实例5:二分查找的时间复杂度

// 计算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);
		int mid = (begin + end) / 2;
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = 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

我们先介绍一些二分查找:
二分查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
查找过程:从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间的关键字,则在表中大于或小于中间记录的那一半中查找,这样重复操作,直到查找成功,或者在某一步中查找区间为空,则代表查找失败。
具体执行次数:这里同样考虑最坏的情况,即在数组中没有查找到元素,数组会从中间查找,每次排除一半的元素,一次一次地执行下去,直到查找到或者区间为空。
N/2/2/2…/2=1
N=2^X
X=logN
所以二分查找的时间复杂度为O(logN)

实例6:斐波那契数列的时间复杂度

long long Fib(n)
{
    if(n==1||n==2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

请添加图片描述
具体次数:以上图为例,我们看到在N>2的时候,每一层的调用次数以2的指数形式增长,尽管右边缺省了一部分,但对结果没有太大的影响,所以我们认为是一个等比数列。
所以斐波那契数列的时间复杂度为O(2^N)

实例7:阶乘递归的时间复杂度

long long Fac(size_t N) {
	if (N == 0)
		return 1;

	return Fac(N - 1) * N;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

具体次数:这里N调用N-1,N-1调用N-2…直到N=0;所以一共调用了N+1次
所以阶乘递归的时间复杂度为O(N)

四、空间复杂度

1.空间复杂度的概念

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度(额外开辟的空间)
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

2.空间复杂度的计算方法

空间复杂度的计算的方法和时间复杂度的计算方法相似,且都是用大O的渐进表示法表示。
具体方法如下:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。(它对结果产生了决定性的影响)
3、如果最高阶项的系数存在且不是1,则去除与这个项目相乘的常数。

3.常见空间复杂度计算举例

实例1:阶乘递归的空间复杂度

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

	return Fac(N - 1) * N;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们知道,每次函数调用开始时都会在栈区上形成自己的函数栈帧,函数调用结束后销毁栈帧。
对于上面的递归来说,只有在N=0的时候函数才返回,而在这之前所形成的Fac(N) Fac(N-1)…这些函数的函数栈帧在返回之前都不会释放,而是一直存在,直到调用完毕直到才一步一步释放,所以在计算递归类空间复杂度的时候,我们关注的是递归的深度。
这里调用的递归深度为N+1(递归N+1次),所以空间复杂度为O(N)

实例 2:斐波那契数列的空间复杂度

long long Fib(n)
{
    if(n==1||n==2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

请添加图片描述
如上图所示,有2^N次递归,递归的顺序为6-5-4-3-2-1,6-5-4-3-2以此类推,直到把最后一个分支递归完毕。但是空间是不会累积的,所以尽管我们同一个函数栈帧被开辟很多次,但他仍然只计入一次开辟的空间复杂度。
所以斐波那契数列的空间复杂度为O(N)

实例3:冒泡排序的空间复杂度

void BubbleSort(int* a, int n)
{
    assert(a);
    int i = 0;
    for(i = 0; i < n-1; i++)
    {
        int j = 0;
        int exchange = 0;
        for(j = 0; j < n - 1 - j; j++)
        {
            if(a[j] > a[j+1])
            {
                int tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = a[j];
            }
            exchange = 1;
        }
        if(exchange==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

这里我们在循环外部定义了一个变量,然后在循环内部又定义了三个变量;可能有的同学会认为tmp变量因为在循环内部,每次进入循环都会被重新定义,所以空间复杂度为N^2,其实不是这样的。
我们知道虽然时间是累积的,一去不复返,但是空间是不累积的,我们可以重复使用;对于i,j和exchange变量来说,只定义了一次,尽管循环过程中值发生改变,但是它还是只占用了定义时所开辟的空间。而对于tmp变量来说,每次进入if这个局部范围时开辟空间,离开这个局部范围时空间销毁,下一次在进入时又重新开辟空间,出去又再次销毁;所以其实从始至终temp都只占用了一个空间,并没有额外的占用其他的空间。
所以尽管上面一共定义了四个变量,但它的空间复杂度为O(1)

实例4:二分查找的空间复杂度

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);
		int mid = (begin + end) / 2;
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = 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

和冒泡排序一样,尽管这里定义了几个变量,但冒泡排序的空间复杂度仍然为O(1)
在对数组元素排序的过程中,只有开辟一个新的数组等一些情况空间复杂度为O(N),在原数组进行排序的空间复杂度为O(1)

五、常见复杂度的对比

请添加图片描述
请添加图片描述

六、总结

1.算法分析的两个主要方面是算法的时间复杂度和空间复杂度,以考察算法的时间和空间效率。一半情况下,鉴于运算空间较为充足,故将算法的时间复杂度作为分析的重点。
2.有些算法的时间复杂度存在最好、平均和最坏情况,但我们在实际中关注的是最坏情况。(悲观保守的预估)
3.时间复杂度和空间复杂度都是用大O的渐进表示法来表示。
4.时间复杂度看运算执行的次数,空间复杂度看变量定义的个数。但注意有些局部变量即使定义了N次,但可能用的是同一块空间。
5.在递归中,时间复杂度看调用的次数,空间复杂度看调用的深度。
6.时间是累积的,一去不复返;空间是不累积的,可以重复利用。申请空间和租房一样,释放空间和退房一样。
7.冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1)。
8.二分查找的时间复杂度为O(logN),空间复杂度为O(1)。
9.阶乘递归的时间复杂度为O(N),空间复杂度为O(N)。
10.斐波那契递归的时间复杂度为O(2^N),空间复杂度为O(N)。
11.数据结构是在内存中管理数据(增删查改),数据库是在磁盘中管理数据(增删查改)

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

闽ICP备14008679号