目录
- 一、数据结构前言
-
- [1.1 数据结构](#1.1 数据结构)
- [1.2 算法](#1.2 算法)
- 二、算法效率
-
- [2.1 复杂度的概念](#2.1 复杂度的概念)
- 三、时间复杂度
-
- [3.1 大O的渐进表示法](#3.1 大O的渐进表示法)
- [3.2 时间复杂度计算示例](#3.2 时间复杂度计算示例)
-
- [3.2.1 示例1](#3.2.1 示例1)
- [3.2.2 示例2](#3.2.2 示例2)
- [3.2.3 示例3](#3.2.3 示例3)
- [3.2.4 示例4](#3.2.4 示例4)
- [3.2.5 示例5](#3.2.5 示例5)
- [3.2.6 示例6](#3.2.6 示例6)
- [3.2.7 示例7](#3.2.7 示例7)
- 四、空间复杂度
-
- [4.1 空间复杂度计算示例](#4.1 空间复杂度计算示例)
-
- [4.1.1 示例1](#4.1.1 示例1)
- [4.1.2 示例2](#4.1.2 示例2)
- 五、常见复杂度对比
- 六、复杂度算法题
-
- [6.1 旋转数组](#6.1 旋转数组)
- 总结
一、数据结构前言
1.1 数据结构
数据结构就是计算机存储、组织数据的方式,是相互之间存在一种或多种特定关系的数据元素的集合。大多数数据结构的用途都有自己的特性。常见的数据结构有:线性表、树、图、哈希等
1.2 算法
我个人认为,算法算法,即是一种解决问题的方法,在计算机中算法就是取一个或一组的值为输入,并产生一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
二、算法效率
这里以一道力扣题来体现算法的效率
轮转数组
超出时间限制便是算法效率不够的原因,代码是正确的,但当给的数组数据很多的时候,就需要机器处理很长时间才能得出结果,这里就牵扯出了一个概念:复杂度
2.1 复杂度的概念
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以如今已经不需要再关注一个算法的空间复杂度了。
我个人认为这也是现在游戏优化普遍做的比较一般的原因
三、时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个函数式T(N) ,这个函数式子就类似于数学中的函数f(x)=ax+b,其图像是一个值的变化趋势,函数式子T(N)也是如此。它定量描述了该算法的运行时间。时间复杂度是衡量程序的时间效率,那为什么不直接去计算程序的运行时间呢?
- 因为程序运行时间和编译环境和运行机器的配置都有关系,比如同一个算法程序,用一个老编译器进行编译和新编译器编译,在同样机器下运行时间不同。
- 同一个算法程序,用一个老低配置机器和新高配置机器,运行时间也不同。
- 并且时间只能程序写好后测试,不能写程序前通过理论思想计算评估。

这个T(N)函数式计算了程序的执行次数。算法程序被编译后生成二进制指令,程序运行,就是cpu执行这些编译好的指令。我们可以通过程序代码或者理论思想计算出程序的执行次数的函数式T(N),假设每句指令执行时间基本一样(实际中有差别,但微乎其微),那么执行次数和运行时间就是等比正相关,这样也脱离了具体的编译运行环境。执行次数就可以代表程序时间效率的优劣。比如解决一个问题的算法a程序T(N)=N,算法b程序T(N)=N2,那么算法a的效率一定优于算法b
案例1:

通过对N取值分析,对结果影响最大的一项是N2,这是为什么呢?这里的O又是什么?
实际中计算时间复杂度时,计算的也不是程序的精确的执行次数,精确执行次数计算起来还是很麻烦的(不同的一句程序代码,编译出的指令条数都是不一样的),计算出精确的执行次数意义也不大,因为我们计算时间复杂度只是想比较算法程序的增长量级,也就是当N不断变大时T(N)的差别,上图可以看出当N不断变大时,常数和低阶项对结果的影响很小,所以只需要计算程序能代表增长量级的大概执行次数,复杂度的表示通常使用大O的渐进表示法
3.1 大O的渐进表示法
大O符号:是用于描述函数渐进行为的数学符号
推导大O阶规则
- 时间复杂度函数T(N)中,只保留最高阶项,去掉那些低阶项,因为当N不断变大时,低阶项对结果影响越来越小,当N无穷大时,就可以忽略不计了。
- 如果最高阶项存在且系数不是1,则去除这个项目的常数系数,因为当N不断变大,这个系数对结果影响越来越小,当N无穷大时,就可以忽略不计了。
- T(N)中如果没有N相关的项目,只有常数项,用常数1取代所有加法常数
3.2 时间复杂度计算示例
3.2.1 示例1

3.2.2 示例2

T(N)里的N不代表任何后面的任何变量,这里的N既可以指代M,也可以指代N
3.2.3 示例3

3.2.4 示例4

这串代码的意思是在字符串中查找指定的字符,查找到直接返回,没有查找到继续查找,遍历到字符串结尾还没有找到,返回NULL
这里若要查找e的话,循环一次,是个常数,所以T(N)=1
若要查找r的话,循环N-2次,所以T(N)=N
若要在中间查找一个字符,则循环N/2次,所以T(N)=N/2
总结:
通过上面可以发现,有些算法的时间复杂度存在最好、平均和最坏的情况
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
大O的渐进表示法在实际中一般情况关注的是算法的上界,也就是最坏运行情况
3.2.5 示例5

外层循环从n到1,循环n次,内存循环从1到n/n-1/... 一直到1,内存循环循环多少次不确定。外层循环n次毋庸置疑
所以这里需要分情况讨论
因为外层循环固定n次,把这n次内层循环的次数累加起来,就得到了总的循环的次数
这里看每一次,第一次end=n,第一次内层循环进来,i<n,i的变化1到n-1,循环n-1次
第二次end- -,i<n-1,i的变化为1到n-2,循环n-2次
当第n次,也就是当最后一次end为1,内层循环i<1,条件不满足,循环不执行
end为2时,也就n-1次,内层循环还会循环一次
剩下如图中所写。
3.2.6 示例6

这里随着n的变化,循环次数也在发生变化,然而并不是n是多少,就循环次数是多少
这里与前面执行次数求解不同的原因是cnt不是+1而是×2
这里时间复杂度的写法看似不对,以2为底怎么能不写呢,实则是当n接近无穷大时,底数的大小对结果影响不大。因此,一般情况下不管底数是多少都可以省略不写,即可以表示为log n
不同书籍的表示方法不同,但写法差别不大
3.2.7 示例7

调用Fac的次数等于递归的次数,这里要把每次递归进行函数调用的时间复杂度累加起来
递归是从N-1开始的,N-1到0,递归了N次
上面就是所有常见时间复杂度的推理了,虽然有些算法的时间复杂度的推理可能很复杂,但以上这些一般在笔试面试的时候已经够用
四、空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中因为算法的需要额外临时开辟的空间。
空间复杂度不是程序占用了多少bytes的空间,因为常规情况每个对象大小差异不会很大,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时显式申请的额外空间来确定
4.1 空间复杂度计算示例
4.1.1 示例1

函数栈帧在编译期间已经确定好了,只需要关注函数在运行时额外申请的空间。BubbleSort额外申请的空间有exchange等有限个局部变量,使用了常数个额外空间
因此空间复杂度为O(1)
4.1.2 示例2

Fac递归调用了N次,额外开辟了N个函数栈帧,每个栈帧使用了常数个空间,因此空间复杂度为O(N)

当再动态内存开辟了n个大小的空间时,单词递归的空间复杂度也为N了,空间复杂度就为O(N2)

单独动态内存开辟n个大小的空间时,空间复杂度也为O(N)
五、常见复杂度对比
随着复杂度增大,程序性能是越差的
六、复杂度算法题
6.1 旋转数组
再看刚刚的题
这道题外层循环k次,内层是numsSize-1变化到1,循环numsSize-1次,模糊认为循环numsSize次,所以认为时间复杂度为O(K*numsSize),K和numsSize均为变量
题目给出二者关系:二者一样大,所以可以认为每个变量时间复杂度为n,这里用n替换了所有的变量,有时不知道两个或多个变量多大,均视为一样大
所以这道题的时间复杂度为O(n2),空间复杂度为O(1),因为代码中没有额外开辟空间。
时间复杂度太高了,超出时间限制
所以改进的思路也就有了,下降时间复杂度就可以,第一次代码使用了循环嵌套,所以时间复杂度为n2,改进代码就不用循环嵌套,用两次循环,时间复杂度就是O(n)
第一次代码改进就是额外开辟一个和原数组大小一样的空间
直接对比原数组和轮转后的数组可以将数组最后k个数据挪到最前面,剩下的数据依次放后面。
这里i来定位原数组的下标,i+k定位新数组的下标,依次把1,2,3,4放到新数组之后,原数组i到了下标为4的位置,此时新数组下标i+k=4+3=7,越界了,这里原数组5要往下标为0的位置放,这里就使用图中的取余,也不会影响前面数据的存放。
该优化算法因为额外开辟的新的空间,所以空间复杂度为O(n),时间复杂度也为O(n),是用空间换时间的一种常见改进思路
再说一种更好的改进思路,既减少了时间复杂度,也不会增加空间复杂度
只需要设计一个函数,来完成逆置的操作即可,想使用的时候随时调用,只要将下标的范围传过去就可以了,范围的下标分别称为left和right
一般来说正确的思路如图,然而却给出了越界的报错
出现越界的用例如图:数组中只有一个数据-1,但其轮转了两次,就是重复轮转
这里调用函数,numsSize-k-1=1-2-1=-2,此时left=0,right=-2,没有进入循环,所以第一次调用没有出现报错,紧接着第二次调用numsSize-k=-1,numsSize-1=0,进入循环int tmp=nums[left],没有-1这个下标,所以溢出了。
所以这里要对k进行特殊处理,首先要理解
当向右轮转 数组元素个数(即 n 次) 时,相当于每个元素都绕了一圈,又回到最初位置 。比如这个列表 [1,2,3,4,5,6,7] ,元素个数 n = 7:
轮转 1 次后:[7,1,2,3,4,5,6]
轮转 2 次后:[6,7,1,2,3,4,5]
......
轮转 7 次后,每个元素都刚好转了一圈,又回到初始的 [1,2,3,4,5,6,7] ,所以看起来 "数据不变" 。
而比如说这里numsSize=7,k=14,7个元素轮转14次还是不变的。
就可以加这样一句代码:
这里没有开辟新的空间,所以空间复杂度为O(1),只使用了一次循环,所以时间复杂度为O(n)
总结
以上就是数据结构复杂度的全部内容了,有一说一暑假自律还是有些难度的,喜欢的兄弟们不要忘记一键三连给予支持~