考点:
- 数据结构相关的概念和术语
- 数据结构的三要素:逻辑结构,物理结构和数据运算
- 算法时间复杂度和空间复杂度的分析与计算(重点)
- 算法设计题通常都要求分析时间,空间复杂度
- 考查时间复杂度的选择题
知识框架
1.1 数据结构的基本概念和术语
1.1.1 概念和术语
- 比较好理解的例子:
- 定义
- 数据:数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。
- 数据元素:
- 数据元素是数据的__基本单位__,通常作为一个整体进行考虑和处理。一个数据元素可由__若干数据项(>=1)__组成,数据项是构成数据元素的不可分割的最小单位
- 在不同的条件下,数据元素又可称为元素、结点、顶点、记录等。数据元素是用一组属性描述定义、标识、表示和允许值的一个数据单元。
- 数据对象:
- 数据对象是具有__相同性质的数据元素的集合__,是数据的一个子集。例如,整数数据对象是集合N={0,土1,土2,…}
- 只是一个集合,元素间的关系除了是同一个集合(虽然同为集合,但是和逻辑结构中的非线性结构的集合不要混淆,它不能代表一种数据结构),别无其他
- 缺少关系不可以用来定义一个完整的数据结构
- 数据类型
数据类型是一个值的集合和定义在此集合上的一组操作的总称。- 原子类型:其值不可再分的数据类型
- 结构类型:其值可以再分解为若干成分(分量)的数据类型
- 固定聚合类型:值由确定数目的成分按某种结构组成.如:复数是由两个实数依确定的次序关系构成的
- 可变聚合类型:"值"的成分的数目不确定.如:定义一个"有序整数序列"的抽象数据类型,其中序列的长度是可变的
- 抽象数据类型:是指一个数学模型以及定义在该模型上的一组操作。(抽象数据组织及与之相关的操作)
- 数据结构:是相互之间存在一种或多种特定关系的数据元素的集合,在任何问题中,数据元素都不是孤立存在的,它们之间存在某种关系,这种数据元素相互之间的关系称为结构(Structure)。
1.1.2 上述术语的联系与区别
- 数据结构和数据类型是所属关系知乎
一种数据结构+定义在此种数据结构上的一组操作=结构类型
一种值的集合+定义在此种值的集合上的一组操作=原子类型
结构类型+原子类型=数据类型
一句话总结,数据结构是一种值的集合,这种值集+值集上的操作就是结构类型,而结构类型是数据类型中的一种,所以数据结构属于数据类型。 - 数据类型和抽象数据类型的关系
它们的异同其实就在字面上了抽象。
抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关,即无论其内部结构如何变化,只要它的数学特性不变,都不影响其外部的使用。”抽象“的意义在于数据类型的数学抽象特性。 - 知乎上的理解:“数据类型”是计算机发展出来的一种重要思想,它将程序设计一分为二:实现基本数据类型和使用抽象数据类型。实现基本数据类型是指在物理层面实现数据类型的逻辑特性;使用抽象数据类型是指仅仅关注数据类型的逻辑特性,而不关注硬件实现。这样两边的人都可以更集中注意力于自己的工作范围,进行更深的钻研。
1.1.3 数据结构三要素的详细解释
- 逻辑结构
逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。- 与数据的存储无关,是独立于计算机的.
- 数据的逻辑结构独立于存储结构
- __数据的存储结构是逻辑结构在计算机上的映射,不能独立于逻辑结构而存在
- 分为线性结构和非线性结构,线性表是典型的线性结构;集合、树和图是典型的非线性结构。
- 集合。结构中的数据元素之间除“同属一个集合”外,别无其他关系。
- 线性结构。结构中的数据元素之间只存在__一对一__的关系。
- 树形结构。结构中的数据元素之间存在__一对多__的关系。
- 图状结构或网状结构。结构中的数据元素之间存在__多对多__的关系。
- 与数据的存储无关,是独立于计算机的.
- 存储结构
存储结构是指数据结构在__计算机中的表示(又称映像),也称物理结构__。它包括__数据元素的表示和关系的表示__。数据的存储结构是用计算机语言实现的逻辑结构,它依赖于计算机语言。数据的存储结构主要有顺序存储、链式存储、索引存储和散列存储。- 1)顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。其优点是可以实现随机存取,每个元素占用最少的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
- 2)链式存储。不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。其优点是不会出现碎片现象,能充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
- 3)索引存储。在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。
- 4)散列存储。根据元素的关键字直接计算出该元素的存储地址,又称__哈希(Hash)存储__。
其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
注意:XX表中,XX带有顺序,链,哈希之类的都代表了物理(存储)结构,XX抽象一点如有序之类的是逻辑结构
- 数据的运算
施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。 - 总结
数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的__设计__取决于所选定的__逻辑结构__,而算法的__实现__依赖于所采用的__存储结构__。
1.2 自己的部分理解和解题思路
1.2.1 概念
- 数据结构三要素的任意一种不同,就不是同一种数据结构
- 因而提问"逻辑或物理,逻辑或运算,……不同时,是不是同一种数据结构"的题,就可以回答这两种相同,但是另一个不一样的情况下也不是同一种数据结构.例如(王道p5)二又树和二又排序树,二叉排序树可以采用二叉树的逻辑表示和存储方式,前者通常用于表示层次关系,而后者通常用于排序和查找。虽然它们的运算都有建立树、插入结点、删除结点和查找结点等功能,但对于二叉树和二叉排序树,这些运算的定义是不同的,以查找结点为例,二叉树的时间复杂度为O(n),而二叉排序树的时间复杂度为O(log2n)。
- 可以用__抽象数据类型__来定义一个完整的数据结构
- 解析:抽象数据类型描述了数据的逻辑结构和抽象运算,通常用(数据对象、数据关系、基本操作集)这样的三元组来表示,从而构成一个完整的数据结构定义。
- 个人理解,是可以用更细节的来定义较抽象的(小定大)
- 1.数据结构可以用(数据元素的有限集、数据元素的关系)二元组定义。
- 2.抽象数据类型是用三元组(数据对象、数据关系、基本操作集)
- 3.数据对象是性质相同的数据元素的集合。所以数据结构主要是比抽象数据类型少了一个操作集,所以可以用抽象数据类型定义一个完整的数据结构。
1.3 算法和算法评价
1.3.1 算法的及设计要求
- 算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。此外,一个算法还具有下列5个重要特性:
1)有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
2)确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
3)可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
4)输入。一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
5)输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。 - 通常,设计一个“好”的算法应考虑达到以下目标:
- 正确性。算法应能够正确地解决求解问题。
- a.程序不含语法错误;
- b.程序对于几组输入数据能够得出满足规格说明要求的结果;
- c.程序对于精心选择的典型、苛刻而带有刁难性的几组输入数据能够得出满足规格说明要求的结果
- d.程序对于一切合法的输入数据都能产生满足规格说明要求的结果。
- 显然,达到第d层意义下的正确是极为困难的,所有不同输入数据的数量大得惊人,逐一验证的方法是不现实的。对于大型软件需要进行专业测试,而一般情况下,通常以第c层意义的正确性作为衡量一个程序是否合格的标准
- 可读性。算法应具有良好的可读性,以帮助人们理解。
- 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 效率与低存储量需求。效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。
1.3.2 算法效率的度量
- 时间复杂度
- 一个算法是由__控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)__构成的,则算法时间取决于两者的综合效果。
- 语句的频度:该语句重复执行的次数
- T(n)
- 王道上以及查到的一些资料称:算法中所有语句的频度之和记为T(n)
- 书上:执行时间/时间量度
- 对于这两种不用太抠字眼,就是对时间复杂度的描述或者说符号,毕竟__分析时间复杂度的时候都是分析T(n)的数量级__(但是还是王道上的好理解一些)
- 算法中基本运算(最深层循环内的语句)的频度与T(n)同数量级(就挑一个基本操作),因此通常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。因此,算法的时间复杂度记为
T(n)=O(f(n)) 式中,O的含义是T(n)的数量级,其严格的数学定义是:若T(n)和f(n)是定义在正整数集合上的两个函数,则存在正常数C和n0,使得当n≥n0时,都满足0≤T(n)小于等于C(n)。 - 渐进时间复杂度:简称是时间复杂度
- 一般总是考虑最坏情况下的时间复杂度(除特殊指明)
- 算法的时间复杂度不仅依赖于问题的规模n,也取决于待输入数据的性质(如输入数据元素的初始状态)。例如,在数组A[0..n-1]中,查找给定值k的算法大致如下:
(1)i=n-1;
(2)while(i>=0&&(A[i]!=k))
(3)i--;
(4)return i;该算法中语句3(基本运算)的频度不仅与问题规模n有关,而且与输入实例中A的各元素的取值及k的取值有关:
①若A中没有与k相等的元素,则语句3的频度f(n)=n。
②若A的最后一个元素等于k,则语句3的频度f(n)是常数0。
最坏时间复杂度是指在最坏情况下,算法的时间复杂度。
平均时间复杂度是指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度是指在最好情况下,算法的时间复杂度。
一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。
- 算法的时间复杂度不仅依赖于问题的规模n,也取决于待输入数据的性质(如输入数据元素的初始状态)。例如,在数组A[0..n-1]中,查找给定值k的算法大致如下:
- 在分析一个程序的时间复杂性时,有以下两条规则:
- a)加法规则
T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(maxf(n),g(n))) - b)乘法规则
T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(n)xg(n) - 常见的渐近时间复杂度为
O(1)<Olog2n)<O(n)<O(nlogan)<O(n2)<O(n3)<O(2)<O(n!)<O(nn)
- a)加法规则
- 空间复杂度
类似于算法的时间复杂度,本书中以空间复杂度(space complexity)作为算法所需存储空间的量度,记作S(n)=O(f(n)) 其中n为问题的规模(或大小)。一个上机执行的程序除了需要存储空间来寄存本身所用指令、常数、变量和输入数据外,也需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的额外空间,否则应同时考虑输入本身所需空间(和输入数据的表示形式有关)。若额外空间相对于输入数据量来说是常数,则称此算法为原地工作,第10章讨论的有些排序算法就属于这类。又如果所占空间量依赖于特定的输入,则除特别指明外,均按最坏情况来分析。
算法原地工作是指算法所需要的辅助空间为常量,并非是不需要任何额外的辅助空间.
同一个算法,实现的语言的级别越高,执行效率越低
1.3.3 计算
- 时间复杂度
- 常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
- 线性阶O(n)
这个在最开始的代码示例中就讲解过了,如:
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。
- 对数阶O(logN)
还是先来看代码:
int i = 1;
while(i<n)
{
i = i * 2;
}
从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n
也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
- 线性对数阶O(nlogN)
线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
就拿上面的代码加一点修改来举例:
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
- 平方阶O(n²)
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。
举例:
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
那它的时间复杂度就变成了 O(m*n)
- 立方阶O(n³)、K次方阶O(n^k)
参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。 - 除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。
- 空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:
- 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1),举例:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
- 空间复杂度 O(n)
我们先看一个代码:
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
以上,就是对算法的时间复杂度与空间复杂度基础的分析,欢迎大家一起交流。
3. 递归算法和非递归算法在分析时间复杂度上是不同的
在算法分析中,当一个算法中包含递归调用时,其时间复杂度的分析会转化为一个递归方程求解。实际上,这个问题是数学上求解渐近阶的问题,而递归方程的形式多种多样,其求解方法也是不一而足,比较常用的有以下四种方法:
(1)代入法(Substitution Method)
代入法的基本步骤是先推测递归方程的显式解,然后用数学归纳法来验证该解是否合理。
(2)迭代法(Iteration Method)
迭代法的基本步骤是迭代地展开递归方程的右端,使之成为一个非递归的和式,然后通过对和式的估计来达到对方程左端即方程的解的估计。
(3)套用公式法(Master Method)
这个方法针对形如“T(n) = aT(n/b) + f(n)”的递归方程。这种递归方程是分治法的时间复杂性所满足的递归关系,即一个规模为n的问题被分成规模均为n/b的a个子问题,递归地求解这a个子问题,然后通过对这a个子间题的解的综合,得到原问题的解。
(4)差分方程法(Difference Formula Method)
可以将某些递归方程看成差分方程,通过解差分方程的方法来解递归方程,然后对解作出渐近阶估计。
下面就以上方法给出一些例子说明。
一、代入法
大整数乘法计算时间的递归方程为:T(n) = 4T(n/2) + O(n),其中T(1) = O(1),我们猜测一个解T(n) = O(n2 ),根据符号O的定义,对n>n0,有T(n) < cn2 - eO(2n)(注意,这里减去O(2n),因其是低阶项,不会影响到n足够大时的渐近性),把这个解代入递归方程,得到:
T(n) = 4T(n/2) + O(n)
≤ 4c(n/2)2 - eO(2n/2)) + O(n)
= cn2 - eO(n) + O(n)
≤ cn2
其中,c为正常数,e取1,上式符合 T(n)≤cn2 的定义,则可认为O(n2 )是T(n)的一个解,再用数学归纳法加以证明。
二、迭代法
某算法的计算时间为:T(n) = 3T(n/4) + O(n),其中T(1) = O(1),迭代两次可将右端展开为:
T(n) = 3T(n/4) + O(n)
= O(n) + 3( O(n/4) + 3T(n/42 ) )
= O(n) + 3( O(n/4) + 3( O(n/42 ) + 3T(n/43 ) ) )
从上式可以看出,这是一个递归方程,我们可以写出迭代i次后的方程:
T(n) = O(n) + 3( O(n/4) + 3( O(n/42 ) + ... + 3( n/4i + 3T(n/4i+1 ) ) ) )
当n/4i+1 =1时,T(n/4i+1 )=1,则
T(n) = n + (3/4) + (32 /42 )n + ... + (3i /4i )n + (3i+1 )T(1)
< 4n + 3i+1
而由n/4i+1 =1可知,i<log4 n,从而
3i+1 ≤ 3log4 n+1 = 3log3 n*log4 3 +1 = 3nlog4 3
代入得:
T(n) < 4n + 3nlog4 3,即T(n) = O(n)。
三、套用公式法
这个方法为估计形如:
T(n) = aT(n/b) + f(n)
其中,a≥1和b≥1,均为常数,f(n)是一个确定的正函数。在f(n)的三类情况下,我们有T(n)的渐近估计式:
1.若对于某常数ε>0,有f(n) = O(nlogb a-ε ),则T(n) = O(nlogb a )
2.若f(n) = O(nlogb a ),则T(n) = O(nlogb a *logn)
3.若f(n) = O(nlogb a+ε ),且对于某常数c>1和所有充分大的正整数n,有af(n/b)≤cf(n),则T(n)=O(f(n))。
设T(n) = 4T(n/2) + n,则a = 4,b = 2,f(n) = n,计算得出nlogb a = nlog2 4 = n2 ,而f(n) = n = O(n2-ε ),此时ε= 1,根据第1种情况,我们得到T(n) = O(n2 )。
这里涉及的三类情况,都是拿f(n)与nlogb a 作比较,而递归方程解的渐近阶由这两个函数中的较大者决定。在第一类情况下,函数nlogb a 较大,则T(n)=O(nlogb a );在第三类情况下,函数f(n)较大,则T(n)=O(f (n));在第二类情况下,两个函数一样大,则T(n)=O(nlogb a *logn),即以n的对数作为因子乘上f(n)与T(n)的同阶。
但上述三类情况并没有覆盖所有可能的f(n)。在第一类情况和第二类情况之间有一个间隙:f(n)小于但不是多项式地小于nlogb a ,第二类与第三类之间也存在这种情况,此时公式法不适用。