什么是数据结构
数据结构的定义
数据 :
数据是描述客观事物的数和字符的集合,所有能输入到计算机并可以被计算机处理的文字,数字和符号叫做数据。
数据元素 :
数据元素是数据的最基本的单位,也称为元素,结点,顶点,记录。。。
数据项 :
数据项是具有独立含义的数据的最小单位,也成为字段或者域。
数据对象 :
数据对象是性质的相同的数据元素的集合,是数据的一个子集。
数据结构 :
相互之间存在某种关系得到数据元素的集合。
数据结构通常包括以下三个方面 :
-
逻辑结构 (面向人)
-
物理结构(面向机器)
-
数据的运算
逻辑结构
数据的逻辑结构是从数据元素的逻辑关系上来描述数据的,是指逻辑关系之间的整体关系,通常是从求解问题中提炼出来的。
数据的逻辑结构与数据的存储无关,是独立于计算机的,因此数据的逻辑结构可以看作是从具体问题抽象出来的数据模型。
逻辑结构的类型
- 集合
集合是指元素之间除了只属于一个集合外没有其他的关系。
- 线性结构
线性结构是指元素之间存在一对一的关系。
特点 :
开始元素和结束元素是唯一的。
除了开始元素和结束元素之外,其余的元素都有且仅有一个前驱元素和后继元素。
- 树形结构
树形结构是指元素之间存在一对多的层次关系。
特点 :
- 除了起始元素之外,其他元素有且仅有一个前驱结点。
- 除了结束元素之外,其他元素有一个或者多个结点。
- 图形结构
图形结构是值元素之间存在多对多的关系。
特点 :
每个节点的起始元素和后继元素都有多个结点。
逻辑结构的表示
- 图表表示 :图表表示就是采用表格或者图形直接描述数据的逻辑关系。
- 二元组表示
二元组是一种通用的逻辑结构表示方式, 表示如下 :
B = (D,R)
B 是一种数据逻辑结构,它由数据元素的集合 D 以及 D 上的二元关系的集合 R 所表示。
D = {di | 1 <= i <= n, n >= 0} R = {ri | 1 <=j<=m m>=0}
R 中的关系 r 是一个序偶的集合,对于 r 中的任一序偶 <x,y> (x,y ∈ D),表示 x 和 y 是相邻的,且 x 在 y 之前,y 在 x 之后,x 称为该序偶的第一元素,y 称为该序偶的第二元素,而且 x 为 y 的前驱元素,y 为 x 的后继元素,若某个元素没有前驱元素,则该元素为开始元素,若某个元素没有后继元素,则该元素称为终端元素。
注意 : (x,y)表示无向关系,等价于 : <x,y> , <y, x> 两个。
看定义理解较困难,通过例子来理解 :
- 线性结构
二元组表示为 :
B = (D,R)
D = (a,b,c,d,e)
//只有一种关系即线性结构
R = (r)
//表示元素之间的线性结构
r = {<a,b>,<b,c>,<c,d>,<d,e>}
- 树形结构
二元组表示为 :
B = (D,R)
D = (a,b,c,d,e)
//只有一种树形结构
R = (r)
//表示元素之间的树形结构
r = {<a,b>,<a,c>,<b,d>,<b,e>,<c,f>}
- 练习 : 有一种数据结构 B = (D,R) , 指出是什么类型的数据结构
B = (D,R)
D = (a,b,c,d,e)
//只有一种树形结构
R = (r1.r2)
//表示元素之间的树形结构
r1 = {<a,b>,<a,c>,<b,d>,<b,e>,<c,f>}
r2 = {<a,b>,<a,c>,<d,e>,<e,f>,<b,e>,<e,c>,<c,f>}
画图为 :
解 :
由 R 可以知道有两种逻辑关系。
且 由 r1 可知为一对多的关系,即树形逻辑结构,由 r2 可知为多对多的关系,即图形逻辑结构。
存储结构
数据的逻辑结构在计算机存储器中的存储表示称为数据的存储结构, 也就是逻辑结构在存储器中的实现。
数据的存储结构不仅要存储每一个数据元素,也要存储数据元素之间的逻辑关系。
同一个逻辑结构在存储结构上有不同的实现。
顺序存储结构
顺序存储结构是使用一块连续的区域来存放所有的数据元素,也就是说所有的数据元素占用一整块存储空间,且逻辑上相邻的两个元素在存储结构上也相邻。 即顺序存储结构将数据的逻辑结构直接映射在存储结构。
优点 :
-
存储效率高。
-
可实现对元素的随机存取。
缺点 :
- 修改 插入 或 删除困难,因为如果要插入或删除可能需要移动一系列的元素。
链式存储结构
每个元素占用一个内存单元,但是结点的地址不一定是连续的,因此无需一整块内存。
为了表示逻辑关系,需要为每一个结点添加一个指针指向相邻元素。
优点 :
- 删除,添加效率高
缺点 :
- 由于元素需要有一块空间来保存相邻元素的地址,因此会使存储器利用率低。
- 不能对元素进行随机存取。
索引存储结构
索引存储结构使在存储元素的同时建立一张索引表用来存储每个元素的关键字及其地址。
索引表中的每一项称为索引项,一般的形式为 “关键字,地址”,关键字使用来唯一标识元素,地址则使该元素的地址。
优点 :
- 查询效率高
缺点 :
- 因为建立索引表,因此增加了内存的开销。
哈希存储结构
哈希存储结构是根据元素的关键字通过哈希算法直接计算出一个值并作为该元素的地址。
优点 :
- 查询速度快。
缺点 :
- 不能存储元素之间的逻辑关系。
数据运算
数据运算是指对数据的操作。
数据运算包括运算定义和运算实现两个层面。
运算定义是描述运算功能,是抽象的。
运算实现是程序员完成算法的实现,是具体的,是基于存储结构的。
数据类型和抽象数据类型
数据类型
数据类型是一组性质相同的值的集合和定义此集合上的一组操作的总称,是某种编程语言已经实现的数据结构。
比如基本数据类型 int …
抽象数据类型
抽象数据类型指用户进行软件系统设计时从问题的数学模型中抽象出来的逻辑数据结构和逻辑数据结构上的运算,而不考虑计算机具体的存储结构和运算的具体实现算法。
抽象数据类型包含以下三个方面 :
一般包括数据对象,数据关系和基本运算三方面内容,可以用 (D,S,P) 三元组表示,其中 D 时数据对象,S 是 D 上的关系集,P 是 D 中数据运算的基本运算集。
抽象数据类型的格式为 :
ADT 抽象数据类型名
{
数据对象:数据对象的声明,
数据关系:数据关系的声明,
基本运算:基本运算的声明
}
基本运算的声明格式为 :
基本运算名(参数表):运算功能的描述
基本运算中参数类型有两种,一种是值,一种是引用参数以 & 开头,除了可以提供输入值之外,还可以返回运算结果。
抽象数据类型有两个重要的特征 : 数据抽象 和 数据封装。
其中数据抽象是指 ADT 描述程序处理的实体时强调的时本质的特征以及其所能完成的功能以及他的外部用户的接口。所谓数据封装是指实体的外部特性与实体的内部实现细节分离,并且对外部用户隐藏其内部实现细节。
算法及其描述
什么是算法
算法是对特定问题求解步骤的描述,是指令的有限集合。
算法的 5 大重要特性 :
- 有穷性 :一个算法总是在有穷步后结束,并且每一步都在有穷时间内完成。
- 确定性 :对于相同的输入结果只能得出相同的输出,不能由二义性。
- 可行性 :算法可以通过有限次基本操作完成其功能,也就是说算法的每一个步骤都可以被机械地执行。
- 有输入 :有 0 个或者 多个输入。
- 有输出 :有 1 个或者多个输出。
算法的设计目标
- 正确性 : 算法能正确的执行预先规定的功能和性能需求。
- 可使用性 :算法能够很方便的使用。
- 可读性 :算法应该使人容易理解。
- 健壮性 :要求算法有很好的容错性。
- 高效率与低存储量的需求 :通常算法的效率主要指的是算法的执行时间,对于同一个问题,如果有多种算法可以求解执行时间短的算法效率高,算法的存储量指的是算法执行过程中所需的最大存储空间。
算法描述
算法描述的方式 :
- 编程语言
- 自然语言伪代码
- 流程图
- 表格
算法描述的一般步骤 :
- 分析算法的功能
- 确定算法有哪些输入,有哪些输出 作为参数
- 设计函数体完成从输入到输出的操作过程
输出型参数设计 : 当传递指针时,可以使用如下方法
int &x = a;
int &y = b;
void swap(int &x, int &y){
int temp = x;
x = y;
y = temp;
}
& :
- 取地址符
- 运算符,用于条件表达式
- 字符串连接运算 例如 :“123”&“456”
- 引用运算符,上面的算法中使用的就是这个符号,例如 int & x = a, x 与 a 等价。
算法分析
算法分析就是分析算法对计算机资源占用的多少.而计算机资源主要是CPU的时间和内存的占用情况,分析算法占用 CPU 的时间称为时间性能分析, 对内存空间的多少称为空间性能分析.
算法时间性能分析
用高级语言编写程序,运行时间取决于如下因素 :
1)硬件速度
2) 编写程序的语言,级别越高,效率越低
3) 编译产生的目标代码的质量
4) 算法策略
5) 问题规模
性能分析方法
-
事后统计法: 即将算法编写完成之后运行然后统计其执行时间.
-
事前统计法: 即撇开计算机硬件软件的因素,仅考虑算法本身的效率高低,可以认为一个算法 的运行工作量的大小只依赖于问题的规模.
算法时间复杂度分析
- 计算算法的频度 T(n)
一个算法是由控制结构(顺序 分支 循环 三种结构)和原操作构成的. 而算法的执行时间取决于控制结构和原操作的综合效果,但是在一个操作中,一个算法执行原操作的次数越多, 执行的时间越长,执行原操作的次数越少, 执行的时间越短, 换句话说, 一个算法的执行时间与可以由其中的原操作的执行次数来计量.
假设算法的问题规模是 n , 如果对 10 个整数排序, 则问题规模就是 10, 算法分析就是求出算法所有原操作的执行次数(也称为频度), 他是问题规模 n 的函数, 用 T(n) 表示。
算法执行的时间大约等于原操作所需的时间 X T(n), 也就是说 T(n) 与算法的执行时间成正比.
例如 : 如下 ① ② ③ ④ 就是原操作.
void fun(int a[], int n){
int i; // ①
for(i = 0; i < n; i++)
a[i] = 2 * i; // ②
for(i = 0; i < n; i++)
printf("%d",a[i]); // ③
printf("\n"); // ④
}
- 用 O 表示 T(n)
由于算法的分析不是绝对的时间分析,因此在求出 T(n) 之后还需要进一步采用时间复杂度来表示.算法时间复杂度就是用 T(n) 的数量级来表示, 记作 T(n) = O(f(n)), O 读作 “大O”, 含义是为 T(n) 找到一个上界 f(n). 即求出 T(n) 的最大阶, 忽略其低阶项和常系数. 例如 : T(n) = 2n^2 + 4n + 2 = O(n^2)
一般一个没有循环的算法中原操作执行的次数与问题规模 n 无关, 记作 O(1), 也称为常数阶, 例如定义变量的语句, 赋值语句和输入输出语句其执行时间都可以看做 O(1).
一个只有一重循环的算法中原操作的执行次数与问题规模 n 的增长呈线性增大关系, 记作 O(n) , 也称为线性阶.
常用的时间复杂度关系 :
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)
其中 O(log2n),O(n),O(nlog2n),O(n2),O(n3)称为多项式时间复杂度.
其中 O(2^n) O(n!) 称为指数时间复杂度.
- 简化的算法时间复杂度分析
另外一种简化的方式是仅仅考虑算法中基本操作, 所谓的基本操作是算法中最深层循环内的原操作.算法执行时间大致等于基本操作所需的时间 * 运算次数.
- 时间复杂度的求和求积定理
求和定理 : 假设 T1(n) 和 T2(n) 是程序段 p1 p2 的执行时间, 并且 T1(n) = O(f(n)) T2(n) = O(g(n)) , 那么先执行 P1, 然后执行 P2 总执行时间为 T1(n) + T2(n) = O(f(x))+O(g(x)), O(MAX(f(n),g(n))) 表示并列循环的情况.
求积定理: 假设 T1(n) 和 T2(n) 是程序段 p1 p2 的执行时间, 并且 T1(n) = O(f(n)) T2(n) = O(g(n)) , 那么 T1(n) X T2(n) = O(f(n)Xg(n)), 例如嵌套循环就是这种情况.
一般我们将最坏的空间复杂度作为为准.
递归算法的时间复杂度分析
递归算法是指算法中出现自己调用自己的情况, 递归算法分析不能采用简单的分析, 递归算法分析也称为变长时空分析, 非递归算法分析称为定长时空分析.
在递归算法分析中首先写出对应的递推式, 然后求解递推式得出算法的执行时间或者空间.
例如 : 调用下面算法的语句为 fun(a,n,0) , 求时间复杂度.
void fun(int a[], int n, int k){
int i;
if(k == n-1){
for(int i = 0; i < n; i++){
printf("%d\n",a[i]);
}
}
else{
for(i = k; i < n; i++){
a[i] = a[i] + i * i;
}
fun(a,n,k+1);
}
}
解:
简要说明 :
分为两种情况,
-
把该题目的时间复杂度设为 T1(n,k)
-
第一种是当 k == n -1 的时候, 很容易得出时间复杂度为 n.
-
第二种是当 k != n - 1 的时候, 先求出 for 循环的时候的时间复杂度为 n - k, 然后再求出下面的递归句, 递归句的时间复杂度又为 T1(n,k), 然后再次进入 fun 方法, 再次先求出 for 循环的时候的时间复杂度为 n - k, 然后再求出下面的递归句 .
-
直到第二种情况下的 fun 方法为 fun(a,n,n-1)
具体做法 :
根据题目中调用下面算法的语句为 fun(a,n,0) 直接代入上述的公式:
假设一开始的时候 k != n - 1 , 则有:
//由题目可知 k = 0
T(n)
//因为一开始的时候假设 k != n-1 , 代入 T(n,k) = n-k+T1(n,k+1)
//k == n - 1 的前一项 , 代入 T(n,k) = n - n + 2
//k == n - 1 是时间复杂度为 n
= n - 0 + n - 1 + n - 2 + n + ... + n - n + 2(倒数第二项,根据前面的规律得出) + n(最后一项)
= (n+2)(n-1)/2(等差数列求和公式) + n
= O(n^2)
算法空间性能分析
算法空间复杂度分析
一个算法的存储量包括输入数据所占的空间, 程序本身所占的空间以及临时变量所占的空间, 这里在对存储空间分析的时候只考虑临时变量所占的空间.因此, 空间复杂度是对一个算法在运行中临时占用的存储空间的大小的度量.
一般也作为问题规模 n 的函数, 以数量计形式给出 :
S(n) = O(g(n))
例如 : 下面仅计算 i 与 maxi 的空间.
int max(int a[], int n){
int i, maxi = 0;
for(i = 1; i <= n; i++){
if(a[i] > a[maxi]){
maxi = i;
}
}
return a[maxi];
}
递归算法的空间复杂度分析
分析调用语句 fun(a, n, 0) 的空间复杂度.
void fun(int a[], int n, int k){
int i;
if(k == n-1){
for(i = 0; i < n; i++){
printf("%d\n",a[i]);
}
}
else{
for(i = k; i < n; i++){
a[i] = a[i] + i * i;
}
fun(a,n,k+1);
}
}
解 :
与计算时间复杂度相似, 先递推式, 然后进行代入.