数据结构与算法基础
思维导图
数据结构和算法.png一、数据结构
1、数据结构基础
1.1、什么是数据结构?
数据结构:是相互之间存在一种或多种特定关系的数据元素集合。
数据结构中最基本的5个概念:数据、数据元素、数据项、数据对象、数据结构
学习数据结构,首先我们要先知道什么是数据。
1.2、数据
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。
1.3、数据元素
数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。
例如:在人类中,什么事数据元素?当然是人。
1.4、数据项
数据项:一个数据元素可以由若干个数据项组成。
例如:人这个元素可以由鼻子、眼睛、嘴、手和脚这些数据项组成,也可以由姓名、年龄、家庭住址等组成。
数据项是数据不可分割的最小单位
1.5、数据对象
数据对象:是性质相同的数据元素的集合,是数据的子集。
由以上基础概念,我们可得出这样一幅图:
WX20200412-211341@2x.png
2、逻辑结构与物理结构
2.1、逻辑结构
逻辑结构:是指数据对象中数据元素之间的相互关系。
2.1.1、集合结构
集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系。
他们的共性是同属于一个集合。
集合结构.png
2.1.2、线性结构
线性结构:线性结构中的数据元素之间是一对一的关系。
线性结构.png
2.1.3 、树形结构
树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。
树形结构.png
2.1.4、图形结构
图形结构:图形结构的数据元素是多对多的关系。
图形结构.png
2.2、物理结构
物理结构:是指数据的逻辑结构在计算机中的存储形式。(数据的物理结构在很多书中也叫做存储结构)
数据元素的存储结构形式有两种:顺序存储和链式存储
2.2.1、顺序存储结构
顺序存储结构:是把数据元素存放在地址联系的存储单元里,其数据间的逻辑关系和物理关系是一致的。
这样的存储结构其实说白了就是排队占位,大家都按照顺序排好,每个人占一小段空间,谁也别插队。如图:
2.2.2、链式存储结构
链式存储结构:是把数据元素存放子啊任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
链式存储是因为总有一些人不愿意排队,而且有人会放弃排队,这样就要有一个其他的存储结构,也就是链式存储结构。
例如银行或医院的等地方,设置了排队系统,每个人去了先领一个号,等着叫号,在这个等待的期间,你可以随意支配,想去哪去哪,只要及时回来就行,你只关注你前面那个号就行。如图:
3、抽象数据类型
3.1、数据类型
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
在C语言中,按照取值的不同,数据类型可以分为两类:
- 原子类型:是不可以再分解的基本类型,包括整型、实型、字符型等。
- 结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干个整型数据组成的。
3.2、抽象数据类型
抽象是指抽取出事物具有的普遍性的本质。它是抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括. 抽象是一种思考问题的方式,它隐藏繁杂的细节,只保留实现目标所必需的信息.
抽象数据类型: 是指一个数学模型以及定义在该模型上的一组操作。
例如,我们在编写计算机绘图软件系统时,经常会使用到坐标. 也就是说,会经常使用x,y来描述横纵坐标. 而在3D系统中,Z深度就会出现. 既然这3个整型数字是始终出现在一起. 那就可以定义成一个Point的抽象数据类型. 它有x,y,z三个整型变量. 这样开发者就非常方便操作Point 数据变量.
抽象数据类型可以理解成实际开发里经常使用的结构体和类; 根据业务需求定义合适的数据类型以及动作.
二、算法
启示:算法是解决特定问题求解步骤的描述,在计算机中的表现为指令的有限序列,并且每条指令表示一个或多个操作。
2.1、数据结构与算法的关系
程序=数据结构+算法
2.2、算法的定义
算法:是解决特定问题求解步骤的描述,在计算机中的表现为指令的有限序列,并且每条指令表示一个或多个操作
有人会问,有没有通用算法? 换过头来,有没有包治百病的药?
现实开发中会有成千上万的问题,算法也是千变万化,没有通用的算法可以解决所有问题. 甚至解决一个小问题,很优秀的算法却不一定非常适合它;
每个人对问题的解决方案是不一样的,但是我们可以根据一些参考来评估算法的优劣;
2.3、算法的特性
算法具有五个基本特性: 输入、输出、有穷性、确定性、可行性。
2.3.1 输入输出
输入和输出特性比较容易理解,算法具有零个或多个输入
对于绝大多数算法来说,输入参数都是必要的,除了个别情况(比如打印),因此算法的输入可以是零个。
算法至少有一个或多个输出,算法是一定需要输出的,不需要输出,你用这个算法干吗?输出的形式可以是打印或者返回值。
2.3.2、有穷性
有穷性: 指算法在执行有限的步骤之后,自动结束而不会出现无限循环,且每一个步骤在可接受的时间内完成。
2.3.3、确定性
确定性: 算法的每一个步骤都具有确定的含义,不能出现二义性。 算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。
2.3.4、可行性
可行性: 算法的每一步都必须是可行的,也就是说,每一步都能通过执行有限次数完成。
2.4、算法设计的要求
2.4.1、正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
但是算法的“正确”通常在用法上有很大的差别,大体分为以下四个层次:
- 算法程序没有语法错误;
- 算法程序对于合法的输入数据能够产生满足要求的输出结果;
- 算法程序对于非法的输入数据能够得出满足规格说明的结果;
- 算法程序对于精心选择的,甚至刁钻的测试数据都有满足要求的输出结果;
2.4.2、可读性
可读性: 算法设计的另一个目的是为了便于阅读、理解和交流。
2.4.3、健壮性
一个好的算法还应该能对输入数据的不合法的情况做出合适的处理.考虑边界性,也是在写代码经常要做的一个处理; 比如,输入时间或者距离不应该是负数;
健壮性: 当输入数据不合法时,算法也能做出相关处理,而不是产生异常和莫名其妙的结果。
2.4.4、时间效率高和存储量低
设计算法应该尽量满足时间效率高和存储量低的需求
生活中,人们都希望花最少的钱,用最短的时间,办最大的事。 算法也是一样的思想,用最少的存储空间,花最少的时间,办成同样的事就是好算法!
2.5、算法效率的度量方法
2.5.1、事后统计方法
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
但是这种方法显然是有很大的缺陷:
- 必须实现编制好程序,花费大量时间和精力;
- 对计算机硬件和软件的环境有要求。
- 测试数据设计困难。
所以这种方法不科学、不准确,我们考虑不予采纳。
2.5.2、事前分析估算方法
事前分析估算方法:在计算机程序编制前,依据统计的方法对算法进行估算。
高级程序语言编写的程序在计算机上运行时锁消耗的时间取决于下列因素:
- 算法采用的策略、方法。
- 编译产生的代码质量。
- 问题的输入规模。
- 机器执行指令的速度。
第1条当然是算法好坏的根本,第2条要由软件来支持,第4条要看硬件性能。也就是说,抛开这些与计算机硬件、软件有关的因素,一个程序运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
我们在分析一个算法的运行时,重要的是把基本操作的数量与输入规模关联起来,即是基本操作的数量必须表示成输入规模的函数;如图:
问题输入规模.png
随着n值的越来越大,算法在时间效率上的差异会越来越大;
2.6、算法时间复杂度
2.6.1、算法时间复杂度定义
在进行算法分析时,语句的总执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。
2.6.2、推导大O阶算法
- 用常数1取代运行时间中所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果在最高阶项存在且不是1,则去除与这个项相乘的常数。
2.6.3、常数阶
void testSum(int n){
int sum = 0;
sum = (1 + n)*n/2;
printf("%d",sum);
}
这个算法的运行次数函数是f(n) = 3。根据我们大O推导方法,第一步先把常数项改成1,在保留最高阶时发现,它根本没有最高阶,所以这个算法的时间复杂度为O(1);
void testSum2(int n){
int sum = 0; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
sum = (1 + n)*n/2; //执行1次
printf("%d",sum); //执行1次
}
事实上无论常数n是多少,以上的代码执行3次还是9次的差异,执行时间恒定。我们的都称之为具有O(1)的时间复杂度,又称为常数阶
。
2.6.4、线性阶
线性阶的循环结构会复杂很多。 要确定某个算法的阶次,我们常常需要先确定某个特定语句或某个语句集的运行次数。 因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。
void addSum(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
以上这段代码,它的训话的时间复杂度为O(n),因为循环体重的代码须执行n次。
2.6.5、对数阶
int count = 1;
while(count < n){
count = count * 2;
/* 时间复杂度为O(1)的程序步骤序列*/
}
count = count * 2 ; 每次执行这句代码,就会距离n更近一步; 也就是说, 有多少个2相乘后大于n,则会退出循环。2^x = n,得到x = log2^n.
所以,这个循环时间复杂度为: O(logn).
2.6.6、平方阶
void add(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
/* 时间复杂度为O(1)的程序步骤序列*/
}
}
}
对于外层循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以,以上代码的时间复杂度为O(n^2)。
void add(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = i; j < n ; j++) {/* 注意j = i,而不是0*/
x=x+1;
/* 时间复杂度为O(1)的程序步骤序列*/
}
}
}
由于当i = 0,内循环执行了n次,当i=1时,执行n-1次,......当i=n-1次,就执行1次。所以总执行次数为:n+(n-1)+(n-2)+...+1 = n(n-1)/2 = (n^2) /2 + n/2
i = 0,循环执行次数是 n 次。
i = 1,循环执行次数是 n-1 次。
i = 2,循环执行次数是 n-2 次。
...
i = n-1,循环执行的次数是 1 次。
换算成:
result = n + (n - 1) + (n - 2) … + 1
被加数递减,抽象为一个等差数列求n项和的问题,公差为1,带入公式,Sn = n(a1 + an ) ÷2
result = (n(n+1))/2 = (n^2+n)/2 = (n^2)/2 + n/2
那我们采用大O阶方法,第一条,没有加法常数不予考虑;第二条,只保留最高阶项,因此保留 (n^2) /2;第三条,去除这个相乘的常数,也就是去除1/2, 最终这段代码的时间复杂度为O(n^2)。
2.7、常用的时间复杂度
常见的时间复杂度
常见的时间复杂度.png
指数阶O( 2^n ) 和 阶乘阶 O(n!) 等除非是非常小的n值,否则哪怕n只有100,都会造成噩梦般的运行时间。所以这种不切实际的算法时间复杂度,一般都不会考虑且讨论。
2.8、最坏情况与平均情况
我们查找一个n个随机数字数组中的某个数字,最好的情况是第一个数字就是, 那么算法的时间的复杂度为O(1). 但也有可能这个数字就在最后一个位置上,也就是算法时间复杂度为O(n). 这是最坏的情况了.
最坏的情况运行时间是一种保证,那就是运行时间将不会比这更坏了。在应用中,这是一种最重要的需求,通常除非特别指定,我们提到的运行时间都是最坏情况下的运行时间。
而平均运行时间也就是从概率的角度来看,这个数字在每个位置的可能性都是相同的, 所以平均的查找时间为n/2次后会发现这个目标元素。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。 现实中,平均运行时间都是通过一定数量的分析估算出来。
对于算法的分析,一种方法就是计算所有情况的平均值,这种时间复杂度的计算的方法称为平均时间复杂度。 另一种方法是计算最坏的情况下时间复杂度,这种方法称为最坏时间复杂度。一般没有特殊情况下,都是指最坏时间复杂度。
2.9、算法空间复杂度
算法设计有一个重要原则,即空间/时间权衡原则(space/time tradeoff)。
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记做: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
一般情况下,一个程序在机器上执行时,除了需要寄存本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的辅助存储空间。 其中,对于输入数据所占的具体存储量取决于问题本身,与算法无关。这样只需要分析该算法在实现时所需要的辅助空间就可以了。
如果算法执行时所需要的辅助空间相对于输入数据量是一个常数,则成这个算法原地工作,辅助空间为O(1).
程序空间计算因素:
- 寄存本身的指令
- 常数
- 变量
- 输入
- 对数据进行操作的辅助空间
在考量算法的空间复杂度,主要考虑算法执行时所需要的辅助空间.
空间复杂度计算:
问题: 数组逆序,将一维数组a中的n个数逆序存放在原数组中.
注意,算法的空间复杂度指的并不是整个算法在内存占用空间,而是指的是该算法在实现时所需要的辅助空间就可以.
对一个算法,其时间复杂度和空间复杂度往往会互相影响. 当追求一个较好的时间空间复杂度时,可能会导致占用较多的存储空间. 即可能会使用空间复杂度的性能变差.反之亦然. 不过,通常情况下,鉴于运算空间较为充足,人们都以算法时间空间复杂度作为算法优先的衡量指标.
参考:
001--数据结构与算法之美(基础)
大话数据结构-程杰