数据结构(一):算法分析
一、 数据结构与算法概述
1.1数据结构
数据结构是一门研究程序设计中的对象,以及他们的关系及操作的学科,说白了就是把各种数据元素按照一定的关系组织成集合,用来对其组织,存储和操作
1.2算法
算法即解决程序设计问题的方案,通过一定规范的输入,在一定时间内获得需求的输出结果
1.3程序
程序=数据结构+算法
二、 数据结构分类
2.1逻辑结构
逻辑结构是把数据元素集合的抽象模型,常见的有集合,线性结构,树形结构和图形结构
集合:若干数据元素属于同一个集合,再无其他关系
线性结构:数据元素之间有一一对应的关系
树形结构:数据元素之间有一对多的关系
图形结构:数据元素之间有多对多的关系
2.2物理结构
物理结构是逻辑结构在计算机中的表示方式,一般有顺序存储结构和链式存储结构
顺序存储结构:
把若干数据元素存储到地址连续的数据单元里的结构,常用的数组就是这种结构,顺序存储结构对于指定元素的查找很方便,就像书的目录一般,通过指定位置即可找到对应的值,但也存在一定弊端
由于结构是连续的,对于新插入或剔除的数据,就得需要整个结构重排,因此有了链式存储结构。
链式存储结构:
数据元素存储在不连续的存储单元中,通过元素中引入指针指向其他数据元素的地址,来找到互相关联的数据。
三、 算法的优化考量
算法在计算中的运行是需要时间和内存空间的,对算法优化的考量,要么牺牲时间换空间,要么牺牲空间换时间,没有时间和空间同时能优化到极致的算法。
从广东去北京,坐飞机时间快,但费用高,步行的话费用低但时间长,算法的优化也是如此,我们可以用不同的算法,用不同的成本去解决问题,见如下示例:
计算1到100的和
这个数学问题可以用如下三种方式解决,
第一种是1到100的每个数都相加一次,在for循环中执行了100次相加计算
第二种是使用高斯公式直接求和,只进行了一次公式计算
第三种是使用递归的方式求和,每次递归,都要执行一次方法,这样100的求和需要在内存空间中开辟100个栈内存
综上,第二种的时间比第一种少,一二种算法比第三种算法节省空间,所以选取算法二最佳
四、 算法时间复杂度分析
4.1影响算法时间的因素
三种因素会影响到算法的时间,分别是算法采用的策略,问题输入的规模和计算机硬件的质量,实际计算程序的时间复杂度时,我们并不关心是用哪种语言编写,并且计算机的硬件资源也不可控
那么在算法固定的情况下,我们通过改变问题输入的规模来验证算法时间复杂度的规律。
4.2函数渐进增长
上述问题的前两种解法,计算机进行计算的次数如下,第一种合计做了2n+3次计算,第二种合计做了3次计算,那么不同次数的计算,对于算法的影响程度有多大?
实验4.2.1
假设有四组程序,通过函数在坐标图上的表现(横坐标输入规模,纵坐标算法耗时)如下图
- 算法A1的执行次数为2n
- 算法A2的执行次数为2n+3
- 算法B1的执行次数为3n
- 算法B2的执行次数为3n+3
通过函数的直线可看出,随着n的逐渐增大,算法A1,A2逐渐重叠,算法B1,B2也逐渐重叠,因此我们可得出结论:
随着输入规模的增大,算法的常熟操作可以忽略不计
实验4.2.2
假设有四组程序,通过函数在坐标图上的表现(横坐标输入规模,纵坐标算法耗时)如下图
- 算法A1的执行次数为n
- 算法A2的执行次数为3n
- 算法B1的执行次数为n^2+1
- 算法B2的执行次数为2n^2+1
通过函数的直线可看出,随着n的逐渐增大,算法A1,A2逐渐重叠,算法B1,B2也逐渐重叠,因此我们可得出结论:
随着输入规模的增大,与最高次项相乘的常数可以忽略
实验4.2.3
假设有四组程序,通过函数在坐标图上的表现(横坐标输入规模,纵坐标算法耗时)如下图
- 算法A1的执行次数为n^2
- 算法A2的执行次数为2n^2+2n
- 算法B1的执行次数为n^3
- 算法B2的执行次数为2n^3+2n
通过函数的直线可看出,随着n的逐渐增大,A1的算法要明显优于B1,A2的算法也明显优于B2,因此我们可得出结论:
随着输入规模的增大,最高此项的指数不可忽略,并且随着指数的增长,算法耗时愈发明显增加
实验4.2.4
假设有四组程序,通过函数在坐标图上的表现(横坐标输入规模,纵坐标算法耗时)如下图
- 算法A1的执行次数为1
- 算法A2的执行次数为log(n)
- 算法A3的执行次数为n
- 算法A4的执行次数为n^2
- 算法A5的执行次数为n^3
通过函数的直线可看出,随着n的逐渐增大,最高次项指数高的,耗时增加愈发的高,因此我们可得出结论:
算法函数中n的最高次幂越小,算法效率更高
4.3影响算法的规律总结
- 随着输入规模的增大,算法的常熟操作可以忽略不计
- 随着输入规模的增大,与最高次项相乘的常数可以忽略
- 随着输入规模的增大,最高此项的指数不可忽略,并且随着指数的增长,算法耗时愈发明显增加
- 算法函数中n的最高次幂越小,算法效率更高
五、 算法时间复杂度记法
5.1大O记法
计算算法复杂度时,算法的总执行次数T(n)决定了算法执行时间的增长率O(f(n)),用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大
T(n)增长最慢的算法为最优算法。
- 由于随着输入规模的增大,算法的常熟操作可以忽略不计,所以执行次数只保留最高阶,若为常数,我们用1来代替,
- 由于随着输入规模的增大,与最高次项相乘的常数可以忽略,所以执行次数保留最高阶,并去除与该高阶相乘的常数
综上,如下算法对应的大O计法可表示如下:
算法一执行次数为1——O(1)
算法二执行次数为n+3——O(n)
算法三执行次数为2n^2+3——O(n^2)
5.2常见的大O阶
5.2.2线性阶
线性阶就是随着输入规模的扩大,对应计算次数呈直线增长,如下算法循环体中的代码需要执行n次,故大O计法为O(n)
5.2.3平方阶
如下算法循环体中的外层的i循环需要执行n次,里层的j循环需要执行n次,故大O计法为O(n^2)
5.2.4立方阶
如下算法循环体中的外层的i循环需要执行n次,里层的j循环需要执行n次,最里层的k循环需要执行n次,故大O计法为O(n^3)
5.2.5对数阶
对数函数log(n)也可以表示为log2(n),该函数的大O计法为O(logn)
5.2.6常数阶
一般不涉及循环操作的都是常数阶,因为它不会随着n的增长而增加操作次数, 由于随着输入规模的增大,算法的常熟操作可以忽略不计,所以执行次数只保留最高阶,若为常数,我们用1来代替
故该函数的大O计法为O(1)
5.2.7各大O阶的复杂程度对比如下
综上及结合如下函数图像可得,各种类型的大O阶复杂程度如下
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)
5.2最坏情况
最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非特别指定,我们提到的运行时间都指的是最坏情况下的运行时间
六、 算法空间复杂度分析
现代的计算机设备内存都比较大,基本都是4G起步,优秀一点的32G也遍地皆是,加上JVM有自己的内存优化机制如及时编译,垃圾回收等,所以内存正常情况下使用一般不会有大问题
但是对于空间复杂度的评估,还是要懂的。
6.1内存计算公式
对象占用内存 = 对象头开销 + 实例数据(如果是引用类型则包括 变量 和 实例 两部分开销) + 填充数据。
a.对象头开销
指向类引用、垃圾收集信息、同步信息等。
32位JVM 对象头8字节,数组对象头16字节。
64位JVM 对象头16字节,数组对象头24字节,空对象 / 空数组 都只有对象头。
注:对象头要用来存储对象自身的运行时数据,如hashcode、gc分代年龄
b.实例数据
基本类型在内存只有值所占用空间大小。
引用类型包含 变量 和 值 两部分占用空间大小(引用变量就相当于指针,用一个系统存储单元存储。值则是堆中实例的大小)。
c.填充数据
hotspot JVM里,对象占用空间是8字节对齐的。因为在内存中一个存储单元是8字节的。
意思是一个Java对象使用的内存一定是8字节的整数倍,如果通过计算后发现对象所需内存不是8字节的整数倍,则会将其填充为8字节的倍数。
例如:对象实例可能需要内存为30字节或者28字节等,都会被填充为32字节。
d.继承关系(不计算父类对象头开销)
子类对象占用内存 = 子类对象头开销 + 子类实例数据 + (父类实例数据+填充数据) + 填充数据。
示例1:
上述代码类对象头开销16字节+int类型的k占用4字节,相加为20字节,因为64位机器对象所需内存为8字节的整数倍,故被自动填充为24字节,如下为Java中常见数据类型的内存字节占用一览。
示例2:
若是新实例化了一个数组对象temp,则需要新增占用数组对象头24字节,50*2(char字节占用)=100字节,即该数组占用总空间为124字节,被自动填充到128字节
故该段代码的总字节占用为128+24=152字节
6.2 Java中常见数据类型的内存字节占用
数据类型 |
内存占用字节数 |
byte |
1 |
short |
2 |
int |
4 |
long |
8 |
float |
4 |
double |
8 |
boolean |
1 |
char |
2 |