1.1 什么是数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.2 数据结构的起源和意义
- 一般认为,一个数据结构是由数据元素依据某种逻辑联系组织起来的。对数据元素间逻辑关系的描述称为数据的逻辑结构;数据必须在计算机内存储,数据的存储结构是数据结构的实现形式,是其在计算机内的表示;此外讨论一个数据结构必须同时讨论在该类数据上执行的运算才有意义。一个逻辑数据结构可以有多种存储结构,且各种存储结构影响数据处理的效率。
- 在许多类型的程序的设计中,数据结构的选择是一个基本的设计考虑因素。许多大型系统的构造经验表明,系统实现的困难程度和系统构造的质量都严重的依赖于是否选择了最优的数据结构。许多时候,确定了数据结构后,算法就容易得到了。有些时候事情也会反过来,我们根据特定算法来选择数据结构与之适应。不论哪种情况,选择合适的数据结构都是非常重要的。
- 选择了数据结构,算法也随之确定,是数据而不是算法是系统构造的关键因素。这种洞见导致了许多种软件设计方法和程序设计语言的出现,面向对象的程序设计语言就是其中之一。
、数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
1.3 数据结构的概念
1.数据:数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被程序识别和处理的符号的集合。
2.数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。
3.数据对象:数据对象是具有相同性值的数据元素的集合,是数据的一个子集。
4.数据类型:数据类型是一个值的集合和定义再此集合上的一组操作的总称。
1)原子类型。其值不可再分的数据类型。如bool 和int 类型。
2)结构类型。其值可以再分解为若干成分(分量)的数据类型。
3)抽象数据类型。抽象数据组织及与之相关的操作。
1.4 数据结构三要素
1.逻辑结构:
逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。逻辑结构包括:
1.A 集合结构:数据元素除了同属于一个集合外,它们之间没有其他关系。
1.B 线性结构:数据元素之间是一对一关系。
1.C 树形结构:数据元素之间呈现一对多关系 。
1.D 图形结构:数据元素是多对多关系。
2.物理结构:是指数据的逻辑结构在计算机中的存储形式(又称映像),因此也称为 存储结构,是面向计算机的。
物理结构包括:
2.A 顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
2.B 链式存储:逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
2.C 索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
2.D 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。3.数据的运算:施加在数据上的运算包括运算的定义何实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
1.5 算法的基本概念
程序 = 数据结构 + 算法。算法是解决特定问题求解步骤的描述在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作,简而言之,算法是描述解决问题的方法。
算法的特性:
1.有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
2.确定性:算法中每条指令必须有确定的含义,对于相同的输入只能得到相同的输出。
3.可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
4.输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
5.输出:一个算法有一个多个输出,这些输出是与输入有着某种特定关系的量。
好的算法达到的目标:
1.正确性:算法应能够正确的求接问题。
2.可读性:算法应具有良好的可读性,以帮助人们理解。
3.健壮性:输入非法数据时,算法能适当地做出反应或进行处理,不会产生莫名奇妙地输出结果。
4.效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。
1.6 算法的复杂度
1.6.1 复杂度表示法
使用大O复杂度表示法,公式为T(n) = O(f(n))。
其中T(n)表示代码执行的时间; n表示数据规模的大小;f(n) 表示每行代码执行的次数总和。因为这是一个公式, 所以用f(n)来表示。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。
1.6.2 复杂度分析法则
1.单段代码看高频
2.多段代码取最大
3.嵌套代码求乘积
4.多个规模求加法
1.6.3 时间复杂度分析
1.只关注循环执行次数最多的一段代码
2.加法法则:总复杂度等于量级最大的那段代码的复杂度
3.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
(1)可以忽略加法常数:O(2n + 3) = O(2n)
(2)与最高次项相乘的常数可忽略:O(2n^2) = O(n^2)
(3) 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快:O(n^3) > O(n^2)
(4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数:O(2n^2) = O(n^2+3n+1)、O(n^3) > O(n^2)
多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。包括,
O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n^2)(平方阶)、O(n^3)(立方阶)。
非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括,
O(2^n)(指数阶)、O(n!)(阶乘阶)。
O(1) :常量级时间复杂度,只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。
O(logn)、O(nlogn):如下程序所示,x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。
i=1;
while(i<=n) {
i = i*2;
}
O(m+n)、O(m*n):从代码中可以看出,m和n是表示两个数据规模。我们无法事先评估m和n谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复 杂度就是0(m+n)。针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为: T1(m) + T2(m) = O(f(m) + g(n))。但是乘法法则继续有效: T1(m)*T2(n) = O(f(m) * f(n))。
int cal(int m, int n) {
int sum_1=0;
int i=1;
for(;i<m;++i){
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j=1;
for (;j<n;++j){
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
1.6.4 空间复杂度分析
空间复杂度:表示算法的存储空间与数据规模之间的增长关系。
void print(int n) {
inti=0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] =i* i;
}
for(i=n-1;i>=0;--i){
print out a[i]
}
}
跟时间复杂度分析一样,我们可以看到,第2行代码中,我们申请了一个空间存储变量i,但是它是常最阶的,跟数据规模n没有关系,所以我们可以忽略。第3行申请了一个大小为n的int类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是O(n)。我们常见的空间复杂度就是O(1)、O(n)、 O(n2), 像O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。
1.6.5 复杂度分析的概念
1.最坏情况时间复杂度:代码在最坏情况下执行的时间复杂度。
2.最好情况时间复杂度:代码在最理想情况下执行的时间复杂度。
3.平均时间复杂度:代码在所有情况下执行的次数的加权平均值。
4.均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。