数据结构初步了解
1.数据结构的定义
- 数据(data)是描述客观事物的数和字符的集合。
- 数据项(data item)是具有独立含义的数据最小单位,也称为字段或域。
- 数据对象(data object)是指性质相同的数据元素的集合,它是数据的一个子集。
- 数据结构(data structure)是指所有数据元素以及数据元素之间的关系,可以看作是相互之间存在某种特定关系的数据元素的集合。
数据结构通常包括:
- 数据的逻辑结构(logical structure):由数据元素之间的逻辑关系构成。
- 数据的存储结构(storage structure):数据元素及其关系在计算机存储器中的存储表示,也称为数据的物理结构(physical structure)。
- 数据的运算(operation):施加在该数据上的操作。
1.1逻辑结构
需要注意的是,数据逻辑结构与数据的存储无关,数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。
(1)逻辑结构的表示
常见的两种表示方式是图表和二元组。
- 图表表示:就是采用表格或者图形直接描述数据的逻辑关系。
- 二元组表示:比如B=(D,R)其中B是一种数据结构,D是数据元素的集合,R是D上二元关系的集合。(需要注意的是,假设n是D中数据元素的个数,m是R中二元关系的个数,则当n=0时,D为空集即B无结构,此时为数据具有任意结构;当m=0时,R是空集D中的数据元素间不存在任何逻辑关系,彼此相互独立。)
(这里具体什么是二元组我就不写解释了,这个应该算数学知识,不理解的话可以百度一下)
(2)逻辑结构的类型(逻辑结构主要包括哪些?)
- 集合(set):数据元素之间除了“同属于一个集合”的关系以外别无其他关系。
- 线性结构(linear structure):是指该结构中的数据元素之间存在一对一的关系。
- 树形结构:是指该结构中的数据元素之间存在一对多的关系。
- 图形结构:是指该结构中的数据元素之间存在多对多的关系。
四个类型简单做个总结比较:
集合 | 线性结构 | 树形结构 | 图形结构 | |
---|---|---|---|---|
元素间的关系 | 同属一个集合 | 一对一 | 一对多 | 多对多 |
特点 | / | 开始元素和终端元素是唯一的,其余元素都是有且仅有一个前驱元素和后继元素。 | 除了开始元素,其余元素有且仅有一个前驱元素;除了终端元素,其余元素都有一个或多个后继元素。 | 每个元素的前驱和后继元素个数都无限制。(故开始元素和终端元素既可以一个也没有,也可以有多个) |
典例 | / | 线性表、栈和队列、串 | 二叉树 | / |
- 树形结构和图形结构统称非线性结构,该结构中的元素之间存在一对多或多对多的关系。
- 线性结构是树形结构的特殊情况。
- 树形结构是图形结构的特殊情况。
1.2存储结构
数据逻辑结构在计算存储器中的存储表示称为数据的存储结构(也称为映像或物理结构),也就是逻辑结构在计算机中的存储实现。
将数据对象存储到计算机时:1.要存储逻辑结构中的每一个数据元素。2.要存储数据元素之间的逻辑关系。
常用的4种存储结构类型:顺序存储结构、链式存储结构、索引存储结构和哈希(或散列)存储结构。
- 顺序存储结构(sequential storage structure):采用一组连续的存储单元存放所有的数据元素。所有数据元素在存储器中占有一整块存储空间,而且两个逻辑上相邻的元素在存储器中的存储位置也相邻。
- 链式存储结构(linked storage structure):每个逻辑元素用一个内存节点存储,每个节点是单独分配的。所有的节点地址不一定是连续的,所以无需占用一整块存储空间。(加了指针)
- 索引存储结构(indexed storage structure):指在存储数据元素信息的同时还建立附加的索引表。(存储所有数据元素信息的表称为主数据表,其中每个数据元素还有一个关键字和对应的存储地址,索引项一般就包括关键字和地址。)
- 哈希(或散列)存储结构(hashed storage structure):基本思想是根据元素的关键字通过哈希(或散列)函数直接计算出一个值,并将这个值作为该元素的存储地址。(是一个对数据附加有权值的存储方法)
四个存储结构的优缺点做个简单的总结比较:
顺序存储结构 | 链式存储结构 | 索引存储结构 | 哈希存储结构 | |
---|---|---|---|---|
优点 | 存储效率高、可实现对元素的随机存取 | 便于数据修改,在对元素进行插入或删除操作时仅需修改相应节点的指针域,不必移动节点。 | 查找效率高 | 查找速度快 |
缺点 | 不便于数据修改,对数据的插入或删除操作可能需要移动一系列的元素。 | 存储空间的利用率低、不能随机存取(因为逻辑上相邻的元素存储空间中不一定相邻)。 | 需要建立索引表,从而增加了空间开销。 | 只存储数据的元素,不存储元素之间的逻辑关系。(所以一般只适合要求对数据能够进行快速查找和插入的场合) |
4种基本的存储方法既可以单独使用,也可以组合使用。同一种逻辑结构采用不同的存储方法可以得到不同的存储结构。具体怎么选择要考虑到运算方便及算法的时空要求。
1.3数据运算
数据运算是指对数据实施的操作。
每组数据结构都有一组相应的运算,最常用的运算有检索、插入、删除、更新和排序等。
数据运算最终需要在对应的存储结构上用算法实现,所以数据运算分为运算定义和运算实现两个层面。
- 运算定义是运算功能的描述,是抽象的,是基于逻辑结构的。
- 运算实现是程序员完成运算的实现算法,是具体的,是基于存储结构的。
需要注意的是,对于一种数据结构,它的逻辑结构总是唯一的,但是它可能对应多种存储结构,并且在不同的存储结构中同一运算的实现过程可能不同。
(数据运算:1.设计实现运算的算法 2.分析算法的效率)
1.4数据类型和抽象数据类型
- 数据类型(data type):一组性质相同的值的集合和定义在此集合上的一组操作的总称,是某种程序设计语言中已实现的数据结构。
- 抽象数据类型(Abstract Data Type,ADT):指的是用户进行软件系统设计时从问题的数学模型中抽象出来的逻辑数据结构和逻辑结构上的运算,而不考虑计算机的具体存储结构和运算的实现算法。
抽象数据类型 = 逻辑结构 + 抽象运算
(1)数据类型
C/C++语言的数据类型按照取值的不同分为原子类型和结构类型。
- 原子类型是不可以再分解的基本类型。
- 结构类型是由若干数据类型组合而成的,是可以再分解的。(如:数组、结构体等)
①C/C++语言中的基本数据类型:有int型、bool型、float型、double型、char型。int型可以有三个修饰符即short(短整数)、long(长整数)和unsigned(无符号整数)。
// 举个例子:定义一个整型变量并为其赋值为10。
int n = 10;
②C/C++语音中的指针类型:C/C++允许直接对存放变量的地址进行操作。指针变量就是用来存放某个变量的地址。
// 举个例子:
#include<stdio.h>
int main(){
// 定义一个整型变量和指针
int i, *p;
// 给变量赋值
i = 2;
// 指针p指向整型变量i(&i是变量i的地址)
p = &i;
// 打印指针变量看看,当然打印的结果肯定是2没错,这里就不截图展示了
printf("%d\n", *p);
return 0;
}
③C/C++语言中的数组类型:数组是同一数据类型的一组数据元素的集合。
// 举个例子:定义一个数组变量
int a[10]; // 需要注意的是,定义数组变量时需要指定数组大小(这里定义的是10),a为数组名,用于标识数组
④C/C++语言中的结构体类型:是由一组被称为结构体成员的数据项组成的,每个结构体成员都有自己的标识符,也称为数据域。一个结构体类型中所有成员的数据类型可以不同。
// 举个例子:声明一个结构体类型
struct Teacher{
int no; // 成员编号,占4个字节
char name[8]; // 成员姓名,占8个字节
int age; // 成员年龄,占4个字节
}; // 最后的这个分号不要忘记了哦
// 定义一个结构体类型Teacher的一个结构体变量并为其赋值
struct Teacher t;
t.no = 85;
strcpy(t.name, "张三");
t.age = 42;
⑤C/C++语言中的共用体类型:是把不同的成员组织成一个整体,它们在内存中共享一段存储单元,但不同成员以不同的方式被解释。
// 举个例子:
#include<stdio.h>
int main(){
// 声明一个共用体类型Tag
union Tag
{
short int n; // 成员n, 占2个字节
char ch[2]; // 成员ch数组,占2个字节
}; // 分号别忘了哦
// 定义一个共用体类型Tag的一个共用体变量并为其赋值
union Tag u;
// 这里0x4142为16进制数
u.n = 0x4142; // 转换成10进制:2*16^0 + 4*16^1 + 1*16^2 + 4*16^3 = 2+64+256+16384 = 16706
printf("u.n = %d\n", u.n); // 这里输出结果为10进制数
printf("ch[0] = %c\n", u.ch[0]); // 这里ch[0] = 0x42 对照ASCII码表可知ch[0]是B
printf("ch[1] = %c\n", u.ch[1]); // 这里ch[1] = 0x41 对照ASCII码表可知ch[1]是A
return 0;
}
运行结果展示:
⑥C/C++语言中的自定义类型:C/C++中允许使用typedef关键字来制定一个新的数据类型名。
// 举个例子:这个意思就是可以用自定义的ElemType来代替char类型
typedef char ElemType;
- 主要应用之一: 将代码较长的结构体类型声明用自定义类型标识符来代替,可以起到一个简化代码的作用
// 举个例子:
typedef struct Student{ // Student结构体类型
int no; // 学号
char name[10]; // 姓名
char sex; // 性别
int cno; // 班号
}NewType; // 用NewType别名表示Student结构体类型
// 这样当我们想定义一个Student结构体类型变量时,就可以用自定义的类型NewType来表示了
NewType s1,s2; // 等同于:struct Student s1,s2;
在程序设计中,定义变量就是使用内存空间,而存储空间的分配主要有两种方式:静态存储空间分配和动态存储空间分配。
两种分配方式做个简单的比较:
静态存储空间分配方式 | 动态存储空间分配方式 | |
---|---|---|
定义 | 在程序编译期间分配固定的存储空间的方式。 | 在程序运行期间根据需要动态地分配存储空间的方式。 |
特点 | 通常在变量定义时接分配存储单元并一直保持不变,直至整个程序结束。 | C/C++中可以使用malloc()函数为一个指针变量分配一片连续的空间,不需要时用free()函数释放指针所指向的空间。 |
优点 | 定义的变量属于自动变量。(自动变量:当超出其作用范围时系统自动释放其内存空间。) | 不需要预先分配存储空间,分配的空间可以根据程序的需要扩大或缩小。(比如:链式存储结构) |
缺点 | 一旦定义了,无论是否向变量中存放元素,这一片空间都会被占用。 | 需要程序员简单地管理内存空间(即malloc分配后一定要free释放空间,否则动态分配的空间对于程序而言就丢失了,久而久之可能会造成内存泄露)。 |
给两种分配方式都举个简单的例子:
// 静态存储空间分配方式:比如定义一个数组变量
int a[10];
// 动态存储空间分配方式:
char *p; // 定义一个指针变量(注意:指针变量p属于自动变量)
p = (char *)malloc(10*sizeof(char)); // 动态分配10个连续的字符空间
strcpy(p, "China"); // 将字符串"China"存放到p指向的空间中
printf("%c\n", *p); // 输出字符'C'
printf("%s\n", p); // 输出字符串"China"
free(p); // 释放p所指向的空间(注意:malloc分配的空间不会被系统自动释放所以必须自己free)
(2)抽象数据类型
抽象数据类型中的数据对象和数据运算的声明与数据对象的表示和数据运算的实现相分离。(类似封装的意思)
一个抽象数据类型可以用(D,S,P)三元组表示。其中,D:数据对象,S:D上的关系集,P:D中数据运算的基本运算集。
ADT 抽象数据类型名
{
数据对象:数据对象的声明
数据关系:数据关系的声明
基本运算:基本运算的声明
}
其中基本运算的声明格式为:
基本运算名(参数表):运算功能描述
抽象数据类型两个重要的特征:数据抽象和数据封装。
- 数据抽象:指用ADT描述程序处理的实体时强调的是其本质的特征、其所能完成的功能以及它和外部用户的接口(即外界使用它的方法)。
- 数据封装:指将实体的外部特性和其内部实现细节分离,并且对外部用户隐藏其内部实现细节。
2.算法及其描述
算法(algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列。
算法的5个重要特性:
- 有穷性
- 确定性(即对于相同的输入只能得到相同的输出,不能有二义性)
- 可行性
- 有输入
- 有输出
其中,有输入、有输出表示存在数据处理。
算法的5个设计目标:
- 正确性
- 可使用性
- 可读性
- 健壮性(要求算法有很好的容错性,即提供异常处理,能够对不合理的数据进行检查,不经常出现异常中断或死机现象。)
- 高效性与低存储量需求(高效率通常指算法的执行时间短,低存储量指算法执行过程中所需的最大存储空间。效率和存储量都与问题的规模有关。)
算法设计的一般步骤:
- 分析算法的功能。
- 确定算法的输入、输出,并将这些输入输出分别设计成输入型参数和输出型参数。
- 设计函数体,完成从输入到输出的操作过程。
3.算法分析
算法分析就是分析算法占用计算机资源的多少。
算法分析的目的是分析算法的时空性能以便改进算法。
计算机资源主要是:CPU时间 + 内存空间。
- 时间性能分析:分析算法占用CPU时间的多少。
- 空间性能分析:分析算法占用内存空间的多少。
(1)算法时间复杂度分析
一个算法是由**控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)**构成的。
在一个算法中执行原操作的次数越少,其执行时间也相对越少;执行原操作次数越多,其执行时间也就相对越多。即:一个算法的执行时间可以由其原操作的执行次数来计量。
T(n):问题规模n的函数,即算法所有原操作的执行次数(也称为频度)。
算法执行时间 = 执行原操作所需的时间 × T(n)
由此可知,T(n)与算法执行时间成正比,所以我们可以通过比较T(n)大小得出算法执行时间的多少。
时间复杂度O(n):就是用T(n)的数量级来表示。
T(n) = O(f(n))
其中,f(n)是T(n)的上界。(可以理解f(n)为T(n)的最高阶)
时间复杂度也称渐进时间复杂度,它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同。
举个例子,我们依次算出T(n)、f(n)并用O(f(n))表示T(n):
void test(int n){
int a = 0;
for(int i = 0; i < n; i++) // 频度:n+1 (即原操作执行次数为n+1)
for(int j = 0; j < n; j++) // 频度:n(n+1) (循环本身n+1并且它是子循环执行n次,相乘)
a++; // 频度:n的平方 (作为两个循环的循环体各n次,故为n方)
}
则:
T(n) = n+1 + n(n+1) + n^2 = 2n^2 + 2n + 1
f(n) = n^2
T(n) = O(n^2)
- 常数阶O(1):指的是一般来说,一个没有循环(或有循环,但循环次数与问题规模n无关)的算法中原操作执行次数与问题规模n无关。(比如:定义、赋值、输入输出语句都是O(1))
- 线性阶O(n):指的是一个只有一重循环的算法中原操作执行次数与问题规模n的增长呈线性增大关系。
简化的算法时间复杂度分析:指仅仅考虑算法中的基本操作。
基本操作:指算法中最深层循环内的原操作。
即:算法执行时间 = 基本操作所需时间 × 其运算次数。
- 时间复杂度求和:T1(n)=O(f(n)),T2(n)=O(g(n)),则T1(n)+T2(n)=O( Max( f(n), g(n) ) )
- 时间复杂度求积:T1(n)=O(f(n)),T2(n)=O(g(n)),则T1(n)×T2(n)=O( f(n) × g(n) )
递归算法分析也称为变长时空分析,非递归算法分析也称为定长时空分析。
(2)算法空间性能分析
一个算法的存储量 = 输入数据所占的空间 + 程序本身所占的空间 + 临时变量所占的空间。
这里对算法的存储空间分析只包括临时变量所占的空间。
形参的空间在调用该算法的算法中考虑。
即:算法空间复杂度(space complexity)是对一个算法在运行过程中临时占用的存储空间大小的量度。
- 原地工作算法(或就地工作算法):所需临时空间相对与问题规模来说是常数。即占用存储空间大小与问题规模n无关。
空间复杂度中:S(n)= O(g(n))
递归算法的空间复杂度需要根据递归的深度得到。
(3)数据结构 + 算法 = 程序
4.总结回顾
问题:
- 逻辑结构和存储结构是什么?
- 逻辑结构的4个类型和其中元素关系?
- 存储空间的2中分配方式和他们的区别?
- 算法的5个特性和5个设计目标?
- 会算算法的时间与空间复杂度。
解答:
1.逻辑结构是抽象的,是由数据元素之间的逻辑关系构成。存储结构是实体的,是数据元素及其关系在计算机存储器中的存储表示。(也称为物理结构)
2.逻辑结构的4个类型是:集合、线性结构、树形结构和图形结构。其中元素关系集合就是元素属于该集合,线性是一对一,树形是一对多,图是多对多。详细区别看上面的表。
3.存储空间的2中分配方式是静态存储空间分配和动态存储空间分配。主要区别一个是在编译期间固定分配存储空间的,另一个是在程序运行期间动态地分配的。详细区别看上面的表。
4.算法的5个特性:有穷、确定、可行、有输入、有输出;5个设计目标:正确、可使用、可读、健壮、高效性与低存储。
参考文档:
数据结构教程(第5版)