第一章—绪论
一、基本概念
1.1数据、数据元素、数据项和数据对象
1.数据(Data)—能够输入计算机且能被计算机处理的各种符号集合。
数据是信息的载体,也是对客观事物符号化的表示,能够被计算机识别、存储和加工。
数据包括:
*数值型数据:整数、实数等可以进行加减乘除的数。
*非数值型数据:文字、图像、图形、声音等。
2.数据元素(Data Element)—是数据的基本单位(个体)
在计算机程序中通常作为一个整体进行考虑和处理。例如学生表中,一个数据元素中包含了学号、姓名、性别等信息。或者说,计算机对这些信息作为一个整体继续考虑与处理,这个整体成为一个数据元素。
*也称为元素,有时候称为记录(一个表中的一个记录)、结点(树中的一个结点)或者顶点(图中的一个顶点)。
*一个数据元素由若干个数据项组成。
3.数据项(Data Item)—构成数据元素不可分割的最小单位。
例如:学生表中,有学号、姓名、性别等数据项。
关系:数据 > 数据元素 > 数据项,如:学生表 > 个人记录 > 学号、姓名……
即数据是由数据元素组成,数据元素是由数据项组成。
4.数据对象(Data Object)—性质相同数据元素的集合,是数据的一个子集。
例如:*整数集合N
*字母字符集合(ASCII)
*学籍表可看做一个数据对象
1.2数据结构(Data Structure)
1.数据结构:数据元素相互之间存在着某种关系,这种相互之间的关系称为结构(Structure)。即相互之间存在一种或多种特定关系的数据元素集合。换句话来说,数据结构是带结构的数据元素集合。
数据结构包括三个内容:
(1)逻辑结构—数据元素之间的逻辑关系
(2)存储结构—数据元素及关系在计算机内存中的表示(映像),称为数据的物理结构或数据结构。
(3)数据的运算和实现—对数据元素施加操作以及这些操作在存储结构上的实现。
注:逻辑结构是数据结构的抽象,存储结构是数据结构的实现。二者综合起来建立了数据元素之间的结构关系。
2.逻辑结构
*逻辑结构是描述数据元素之间的逻辑关系
*与数据的存储无关,独立于计算机
*从具体问题抽象出来的数学模型
划分方法一:
(1)线性结构:有且仅有一个开始和一个终端结点,并且所有结点最多只有一个前驱和一个直接后继。
如:线性表、栈和队列(有限制的线性表)、串
(2)非线性结构:一个结点可能有多个直接前驱和直接后继。(一对多或者多对多的关系—1:n或m:n)
如:树、图
划分方法二:
(1)集合结构:结构中数据元素之间除了同属于一个集合外无任何其他的关系。
(2)线性结构:结构中存在一对一的线性关系。
(3)树结构:结构中存在一对多的层次关系。
(4)图结构:结构中存在多对多的任意关系。
3.存储结构(物理结构)
*数据元素及其关系在计算机存储器中的结构(存储方式)
*是数据结构在计算机中的表示
四种基本存储结构:
*顺序存储:用一组“连续”的存储单元按照顺序依次存储数据元素。逻辑关系用“元素位置”表示。例如C语言中用“数组”实现顺序存储结构。
*链式存储:用一组“任意”的存储单元存储数据元素,逻辑关系用“指针”表示。例如C语言中“指针”来实现链式存储结构。(指针就是一个地址)
*索引存储:在存储结点信息同时,建立附加的索引表。一般形式是:(关键字,地址)
*散列存储:根据结点的关键字直接计算出结点的存储地址。也称”哈希存储“。
二.算法
2.1数据类型和抽象数据类型
*在使用高级程序设计语言编写程序时,必须对程序中出现的每个变量、常量或表达式,明确说明它们的数据类型。
如C语言中:
(1)int,char,float,double等基本数据类型
(2)数组,结构,共用体,枚举等构造数据类型
(3)指针,空类型
(4)也可以用typedef自己定义数据类型
*一些基本数据结构可以用数据类型来实现,如数组,字符串等
*一些常用的数据结构,如栈、队列、树、图等,不能直接用数据类型表示。
*主句类型的作用:约束变量或常量的取值范围;约束变量或常量的操作。
1.数据类型(Data Type):一组性质相同的值的集合以及定义域这个值集合上一组操作的总称。
数据类型 = 值的集合+值集合上的一组操作。
2.抽象数据类型(Abstract Data Type , ADT):是指一个数学模型以及定义在此数学模型上的一组操作。
*由用户定义,从问题抽象出数据模型(逻辑结构)
*包括定义在数据模型上的一组抽象运算(相关操作)
*不考虑在计算机内部如何存储。
3.抽象数据类型的形式定义:(D,S,P)三元组表示,D数据对象,S是D上关系集,P是对D的基本操作集。
ADT 抽象数据类型名{
数据对象:<数据对象定义> //伪代码描述
数据关系:<数据关系定义> //伪代码描述
基本操作:<基本操作定义>
//基本操作定义格式:
//基本操作名(参数表)
//初始条件:<初始条件描述>
//操作结果:<操作结果描述>
}ADT 抽象数据类型名
基本操作:
参数表:*值传递:赋值参数,只为操作提供输入值。
*引用传递:引用参数,以&开头,除了提供输入值以外,还将返回操作结果。
初始条件:描述操作执行之前数据结构和参数应满足的条件,若不满足,则操作失败,并返回出错信息。若初试条件为空,则忽略。
操作结果:说明操作正常完成之后,数据结构的变化状况和返回的结果。
//ADT定义格式
ADT 抽象数据类型名{
Data
数据对象的定义
数据元素之间逻辑关系的定义
Operation
操作1
初试条件
操作结果描述
操作2
……
操作n
……
}ADT 抽象数据类型名
举例1:抽象数据类型定义一个圆
ADT Circle{
数据对象:D={r,x,y| r,x,y均为实数}
数据关系:S={<r,x,y>| r是半径,<x,y>是圆心坐标}
基本操作:
Circle(&C,r,x,y)
操作结果:构造一个圆。
double Area(C)
初试条件:圆已存在。
操作结果:计算面积。
double Circumference(C)
初试条件:圆已存在
操作结果:计算周长。
……
}ADT Circle
举例2:抽象一个复数定义
ADT Complex{
D = {r1,r2 | r1,r2都是实数}
S={<r1,r2>|r1是实部,r2是虚部}
assign(&C,v1,v2)
初始条件:空的复数C已存在
操作结果:构造复数C,r1、r2分别被赋值v1、v2
destory(&C)
初始条件:复数C已存在
操作结果:复数C被销毁
getReal(Z,&realPart)
初始条件:复数Z已存在
操作结果:用realPart返回实部值
getImag(Z,&imagPart)
初始条件:复数Z已存在
操作结果:用imagPart返回虚部值
Add(z1,z2,&sum)
初始条件:复数z1和z2已存在
操作结果:用sum返回两个复数z1,z2的和。
}ADT Complex
2.2抽象数据类型的表示与实现
用C语言真正实现抽象数据类型的定义
例如:抽象数据类型“复数”的实现
typedef struct{
float realpart; //实部
float imagpart; //虚部
}Complex //定义“复数”抽象类型
void assign(Complex *A,float real,float imag); //赋值
void add(Complex *C,Complex A,Complex B)
用C语言实现"assign"和"add"操作
void assign(Complex *A,float real,float imag){
A->realpart = real; //实部赋值
A->imagpart = imag; //虚部赋值
} //end of assign
void add(Complex *C,Complex A, Complex B){
C->realpart = A.realpart + B.realpart; //实部相加
C->imagpart = A.imagpart + B.imagpart; //虚部相加
} //end of add
注:Complex是我们定义的一个结构体类型
带*:表示这是一个指针变量,它是指向Complex类型的指针。调用例子“ C->realpart ”。
不带*:表示这是一个Complex的普通变量。调用例子“ A.realpart ”。
用C语言实现实现求上面复数 z= (8 + 6i)(4 + 3i) / [(8 + 6i) + (4 + 3i)]
complex z1,z2,z3,z4,z;
float realPart,imagPart;
assign(z1,8.0,6.0); //构造复数 8+6i
assign(z2,4.0,3.0); //构造复数 4+3i
add(&z3,z1,z2); //z1,z2两复数相加赋值给z3
multiply(&z4,z1,z2); //z1,z2两复数想乘赋值给 z4
if(divide(z4,z3,&z)){ //加判断,若分母为0,除数不存在为false/0,没法获取。
//若可以进行除操作,为true/1。
Getreal(z,realPart); //获取结果实部
GetImag(z,ImagPart); //获取结果虚部
}
2.3算法和算法分析
*算法:解决问题的一种方法或过程,考虑的是如何输入转换成输出。且一个问题可以有多种算法。
*程序:用某种程序设计语言对算法的具体实现。
程序 = 数据结构(通过算法实现操作) + 算法(根据数据结构设计程序)
*算法分析:通过考虑算法效率(时间、空间)来判断算法的优劣。
1.算法时间复杂度(时间效率)
通常,算法时间复杂度随“问题规模”增长而增长。
例如:两个n*n的矩阵相乘的算法可描述为
for(i=1;i<=n;i++) //执行了n+1次
for(j=1;j<=n;j++){ //执行了n(n+1)次
c[i][j] = 0; //执行了n*n次
for(k=0;k<n;k++) //执行了n*n*(n+1)次
c[i][j] = c[i][j] + a[i][k]*b[k][j]; //执行了n*n*n次
}
我们把算法所耗费时间定义"为该算法中每条语句的频度之和",则上述算法时间消耗T(n)=2n³ + 3n² + 2n + 1
为了方便比较不同算法的时间效率,我们仅比较它们的“数量级”。这里n*n的矩阵相乘的算法的时间复杂度为“ O(n³) ”。(只看幂最高的)
数学解释(极限):设有辅助函数f(n),当n趋向于无穷大时(执行次数无穷次),T(n) / f(n)的极限值为不等于“零”的常数,则称f(n)是T(n)的同数量级。记作T(n) = O(f(n)),这里称O(f(n))为算法的渐进时间复杂度,简称“时间复杂度”。
定理1.1:忽略所有低次幂项和高次幂系数。只考虑最高次幂。
若
f
(
n
)
=
a
m
n
m
+
a
m
−
1
n
m
−
1
+
…
…
+
a
1
n
+
a
0
是
m
次多项式,则
T
(
n
)
=
O
(
n
m
)
。
若 f(n) = a_m n^m + a_{m-1} n^{m-1} +…… +a_1 n + a_0是m次多项式,则T(n) = O(n^m)。
若f(n)=amnm+am−1nm−1+……+a1n+a0是m次多项式,则T(n)=O(nm)。
分析算法时间复杂度基本方法:
(1)找出语句频度最大的语句作为基本语句(即找嵌套最多的)
(2)计算基本语句的频度得到问题规模n的某个函数f(n)
(3)取其数量级用符号"O"表示
【例1.1】常量阶示例:O(1)
x++;
s=0;
算法执行时间是一个与问题规模n无关的常数,所以算法时间复杂度T(n)=O(1)。
实际上,如果算法的执行时间不随问题规模n的增加而增长,算法中语句频度就是某个常数,即使这个常数再大,时间复杂度也是O(1)。
for(i=0;i<10000;i++){
x++;
s=0;
}
这个算法的时间复杂度也是O(1)
【例1.2】线性阶示例:O(n)
for(i=0;i<n;i++){
x++;
s=0;
}
循环内两条基本语句频度均f(n)=n,所以时间复杂度是O(n)。
【例1.3】平方阶示例:O(n²)
x=0;y=0;
for(k=1;k<=n;k++)
x++;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
y++;
这里,x++的时间复杂度为O(n),y++的时间复杂度为O(n²)。而总体的时间复杂度为O(n²)。
这里有一个加法规则:T(n) = T1(n) + T2(n)=O( f(n) ) +O( g(n) ) = O(max { f(n) , g(n) })。
【例1.4】立方阶示例:O(n³)
x=1;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
for(k=1;k<=j;j++)
x++;
这里计算语句频度
Σ
i
=
1
n
Σ
j
=
1
i
Σ
k
=
1
j
1
=
Σ
i
=
1
n
Σ
j
=
1
i
j
=
Σ
i
=
1
n
i
(
i
+
1
)
2
=
[
n
(
n
+
1
)
(
2
n
+
1
)
6
+
n
(
n
+
1
)
2
]
2
\Sigma_{i=1}^{n}\Sigma_{j=1}^{i}\Sigma_{k=1}^{j}1=\Sigma_{i=1}^{n}\Sigma_{j=1}^{i}j=\Sigma_{i=1}^{n}\frac{i(i+1)}{2}=\frac{[\frac{n(n+1)(2n+1)}{6}+\frac{n(n+1)}{2}]}{2}
Σi=1nΣj=1iΣk=1j1=Σi=1nΣj=1ij=Σi=1n2i(i+1)=2[6n(n+1)(2n+1)+2n(n+1)]
可见语句频度最高次是 n³。所以算法时间复杂度T(n)= O(n³)。
【例1.5】对数阶示例:O(㏒₂n)
for(i=1;i<n;i=i*2){
x++;
s=0;
}
这里设循环内两条基本语句频度f(n),则2^f(n) ≤ n,所以f(n) ≤ ㏒₂n。因此这个算法的时间复杂度为O(㏒₂n)。
i=1;
while(i<=n)
i=i*2;
这里与上面的同理,只是循环语句不同。所以算法时间复杂度也是O(㏒₂n)。
【总结】
除了【例1.3】中有一个加法规则:
加法规则:T(n) = T₁(n) + T₂(n)=O( f(n) ) +O( g(n) ) = O(max { f(n) , g(n) })
还有一个乘法规则:
T(n) = T₁(n) * T₂(n) = O(f(n)) * O(g(n))=O(f(n) * g(n))
常见的时间复杂度高低关系(常对幂指阶)n作为数:常数<对数<幂数<指数<阶层
O
(
1
)
<
O
(
log
2
n
)
<
O
(
n
)
<
O
(
n
log
2
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1)< O(\log_2n)<O(n)<O(n\log_2n)< O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
例题1:
x=0;y=0;
for(int k = 0;k<n;k++) // n+1次
x++; // n次
for(int i = 0; i< n ;i++) // n+1次
for(int j = 0; j < n ;j++) //n*(n+1)次
y++; // n*n次
所以以上算法时间复杂度T(n) = O(n²)。
例题2:
void exam(float x[][],int m,int n ){
float sum[];
for(int i = 0; i< m; i++){
sum[i] = 0.0;
for(j = 0;j < n;j++)
sum[i] += x[i][j];
}
for(i = 0;i<m;i++)
println("i:%f",sum[i]);
}
以上算法时间复杂度T(n) = O(m *n)。
例题3:
for(i=1;i<=n;i++)
for(j=1;j<=n;j++){
c[i][j]=0;
for(k=1;k<=n;k++)
c[i][j] = c[i][j] + a[i][k]*b[i][j];
}
以上算法时间复杂度T(n)=O(n³)。
2.算法空间复杂度(空间效率)
算法所需要耗费的存储空间度量,记作S(n) = O(f(n))。其中n为问题规模。
算法要耗费(占据)空间:
*本身占据的空间:输入/输出,指令,常数,变量
*算法使用的辅助空间
【例】将一组数组a中的n个数逆序存放到原数组中。
//算法1:把第一个和最后一个交换,把第二个和倒数第二个交换,以此类推。
for(i=0;i<n/2;i++){
t=a[i]; //辅助空间只有一个t变量
a[i]=a[n-i-1];
a[n-i-1]=y;
}
//算法2:把a数组倒置放到b数组中,再把b全部搬到a数组中。
for(i=0;i<n;i++)
b[i]=a[n-i-1]; //辅助空间是一整个b[i]数组
for(i=0;i<n;i++)
a[i]=b[i];
算法1是原地工作:S(n)=O(1)。也称常数阶算法是原地工作
算法2:S(n) =O(n)。
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏