chapter_1 绪论
1. 什么是数据结构
数据:所有能够输入到计算机中,且能被计算机处理的符号的集合。
数据元素:是数据(集合)中的一个“个体”,它是数据的基本单位。
数据项:数据项是用来描述数据元素的,它是数据的最小单位。
数据对象:具有相同性质的若干个数据元素的集合,如整数数据对象是所有整数的集合。
数据结构:是指带结构的数据元素的集合。
数据元素之间的逻辑关系 -> 数据的逻辑结构。
数据元素及其关系在计算机存储器中的存储方式 -> 数据的存储结构(或物理结构)。
施加在该数据上的操作 -> 数据运算。
1、数据的逻辑结构表示:表格、二元组、图形
2、数据的存储结构表示:顺序存储结构、链式存储结构
逻辑结构用二元组表示为:B=(D, R)
其中,B是一种数据结构,它由数据元素的集合D和D上二元关系的集合R所组成。其中:
D={ di | 1≤i≤n, n≥0}:数据元素的集合
R={ rj | 1≤j≤m, m≥0}:关系的集合
序偶<x,y>(x,y∈D): x为第一元素,y为第二元素。
x 为 y 的前驱元素。
y 为 x 的后继元素。
若某个元素没有前驱元素,则称该元素为开始元素;
若某个元素没有后继元素,则称该元素为终端元素。
序偶<x,y>表示x、y是有向的,序偶(x,y)表示x、y是无向的
如下矩阵:
a[][4]={
2, 6, 3, 1,
8, 12, 7, 4,
5, 10, 9, 11};
对应的二元组表示为B=(D,R),其中:
D={2, 6, 3, 1, 8, 12, 7, 4, 5, 10, 9, 11};
R={r1, r2} 其中,r1表示行关系,r2表示列关系
r1={<2,6 >, < 6,3>, <3,1 >,
<8,12>, <12,7>, <7,4 >,
<5,10>, <10,9>,<9,11>}
r2={< 2,8 >, <8,5>, <6,12>,
<12,10>, <3,7>, <7,9>,
< 1,4 >, <4,11>}
逻辑结构类型:
1、集合
元素之间关系:无。
特点:数据元素之间除了“属于同一个集合”的关系外,别无其他逻辑关系。
是最松散的,不受任何制约的关系。
2、线性结构
元素之间关系:一对一。
特点:开始元素和终端元素都是唯一的,除此之外,其余元素都有且仅有一个前驱元素和一个后继元素。
3、树形结构
元素之间关系:一对多。
特点:开始元素唯一,终端元素不唯一。
除终端元素以外,每个元素有一个或多个后续元素;
除开始元素外,每个元素有且仅有一个前驱元素。
4、图形结构
元素之间关系:多对多。
特点:所有元素都可能有多个前驱元素和多个后继元素。
存储结构类型:
1、顺序存储结构
2、链式存储结构
3、索引存储结构
4、哈希(散列)存储结构
数据类型和抽象数据类型
在高级程序语言中提供了多种数据类型。不同数据类型的变量,其所能取的值的范围不同,所能进行的操作不同。
数据类型是一个值的集合和定义在此集合上的一组操作的总称。
数据类型和数据结构的关系:数据类型就是已经实现了的数据结构。
抽象数据类型(ADT)指的是从求解问题的数学模型中抽象出来的数据逻辑结构和运算(抽象运算),而不考虑计算机的具体实现。
抽象数据类型 = 逻辑结构 + 抽象运算
例如,定义复数抽象数据类型Complex
一个复数的形式:e1+e2i 或(e1, e2)
ADT Complex
{
数据对象:D={ e1, e2 | e1,e2均为实数 }
数据关系:R={<e1, e2> | e1是复数的实部, e2是复数的虚部 }
基本运算:
AssignComplex(&z, v1, v2):构造复数Z。
DestroyComplex(&z):复数z被销毁。
GetReal(z, &real):返回复数z的实部值。
GetImag(z, &Imag):返回复数z的虚部值。
Add(z1, z2, &sum):返回两个复数z1、z2的和。
} ADT Complex
2. 什么是算法
数据元素之间的关系有逻辑关系和物理关系,对应的运算有基于逻辑结构的运算描述和基于存储结构的运算实现。
通常把基于存储结构的运算实现的步骤或过程称为算法。
算法的五个重要的特性
(1)有穷性: 一个算法必须保证执行有限步之后结束;
(2)确切性: 算法的每一步骤必须有确切的定义,无二义性;
(3)输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定除了初始条件;
(4)输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
(5)可行性:可通过基本运算有限次执行来实现,也就是算法中每一个动作能够被机械地执行。
【例】设计一个算法:求一元二次方程 ax2+bx+c=0 的根。
算法可以采用自然语言、流程图或者表格方式等来描述。
int solution(float a, float b, float c, float &x1, float &x2){
float d,x1,x2;
d=b*b-4*a*c;
if (d>0){
x1=(-b+sqrt(d))/(2*a);
x2=(-b-sqrt(d))/(2*a);
return 2; //2个实根
}else if (d==0){
x1=(-b)/(2*a);
return 1; //1个实根
}else{ //d<0的情况
return 0; //不存在实根
}
}
3. 算法分析
分析算法占用的资源 = CPU时间(时间性能分析) + 内存空间(空间性能分析)
算法分析目的:分析算法的时空效率以便改进算法性能。
一个算法是由控制结构(顺序、分支和循环三种)和原操作(指固有数据类型的操作,如+、-、*、/、++和--等)构成的。
算法执行时间取决于两者的综合效果。
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"); //原操作
}
算法分析方式:
-
事后分析统计方法:编写算法对应程序,统计其执行时间。
编写程序的语言不同、执行程序的环境不同、其他因素
所以不能用绝对执行时间进行比较。 -
事前估算分析方法:撇开上述因素,认为算法的执行时间是问题规模n的函数。
求出算法所有原操作的执行次数(也称为频度),它是问题规模n的函数,用T(n)表示。
算法执行时间大致 = 原操作所需的时间×T(n)。
所以T(n)与算法的执行时间成正比,为此用T(n)表示算法的执行时间。
比较不同算法的T(n)大小得出算法执行时间的好坏。
【例】求两个n阶方阵的相加C=A+B的算法如下,分析其时间复杂度。
#define MAX 20 //定义最大的方阶
void matrixadd(int n, int A[MAX][MAX], int B[MAX][MAX], int C[MAX][MAX]){
int i,j;
for (i=0;i<n;i++){ //①,频度为 n+1,循环体执行 n次
for (j=0;j<n;j++){ //②,频度为 n(n+1)
C[i][j]=A[i][j]+B[i][j]; //③,频度为 n^2
}
}
}
解:除变量定义语句外,该算法包括3个可执行语句①、②和③。
T(n) = n+1 +n(n+1) +n^2
= 2n^2 +2n +1
算法中执行时间T(n)是问题规模n的某个函数f(n),记作:T(n) = O(f(n))
记号 "O" 读作 "大O",它表示随问题规模 n 的增大算法执行时间的增长率和f(n)的增长率相同。
"O"的形式定义为:
T(n) = O(f(n))表示存在一个正的常数M,使得当 n ≥n0 时都满足:|T(n)|≤M|f(n)|
f(n)是T(n)的上界,这种上界可能很多,通常取最接近的上界,即紧凑上界
也就是只求出T(n)的最高阶,忽略其低阶项和常系数,这样既可简化T(n)的计算,又能比较客观地反映出当n很大时算法的时间性能。
例如 :T(n) = 2n^2 + 2n + 1 = O(n^2)
一个没有循环的算法的执行时间与问题规模n无关,记作O(1),也称作常数阶。
一个只有一重循环的算法的执行时间与问题规模n的增长呈线性增大关系,记作O(n),也称线性阶。
其余常用的算法时间复杂度还有平方阶O(n2)、立方阶O(n3)、对数阶O(log2n)、指数阶O(2n)等。
各种不同算法时间复杂度的比较关系如下:
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)
算法时间性能比较:假如求同一问题有两个算法:A和B,
如果算法A的平均时间复杂度为O(n),而算法B的平均时间复杂度为O(n2)。
一般情况下,认为算法A的时间性能好比算法B。
算法中的基本操作一般是最深层循环内的原操作。
算法执行时间大致 = 基本操作所需的时间 × 其运算次数。
在算法分析时,计算T(n)时仅仅考虑基本操作的运算次数。
【例】下列程序段的时间复杂度是( )。
count=0;
for(k=1;k<=n;k*=2){
for(j=1;j<=n;j++){
count++;
}
}
答案:O(nlog2n)
【例】下列程序段的时间复杂度是( )。
void func(int n){
int i=0,s=0;
while (s<n){
i++;
s=s+i;
}
}
答案:O(sqrt(n))
空间复杂度:用于量度一个算法在运行过程中临时占用的存储空间大小。
一般也作为问题规模n的函数,采用数量级形式描述,记作:S(n) = O(g(n))
若一个算法的空间复杂度为O(1),则称此算法为原地工作或就地工作算法。
【例】分析如下算法的空间复杂度。
int fun(int n){
int i, j, k, s;
s=0;
for (i=0;i<=n;i++){
for (j=0;j<=i;j++){
for (k=0;k<=j;k++){
s++;
}
}
}
return(s);
}
答案:算法中临时分配的变量个数与问题规模n无关,所以空间复杂度均为O(1)。
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];
}
void maxfun(){
int b[]={1, 2, 3, 4, 5}, n=5;
printf("Max=%d\n", max(b, n));
}
maxfun算法中为b数组分配了相应的内存空间,其空间复杂度为O(n)
如果max函数中再考虑形参a的空间,就重复累计了执行整个算法所需的空间。
最好、最坏和平均时间复杂度分析
...
4. 递归算法的时间复杂度与空间复杂度分析
【例】分析下列程序段的时间复杂度与空间赋复杂度。
void fun(int a[], int n, int k) {//数组a共有n个元素
int i;
if (k==n-1){
for (i=0; i<n; i++){
printf("%d\n", a[i]);//执行n次
}
}else {
for (i=k; i<n; i++){
a[i]=a[i]+i*i; //执行n-k次
}
fun(a, n, k+1);
}
}
分析调用fun(a, n, 0)的时间复杂度。
解:设 fun(a,n,0) 的执行时间为 T(n),fun(a,n,k) 的执行时间为 T1(n,k)
T(n)=T1(n,0)。
则有如下关系式:
T1(n,k) = n 当k=n-1时
T1(n,k) = (n-k)+T1(n,k+1) 其他情况
则:
T(n) = T1(n,0) = n+T1(n,1) = n+(n-1)+T1(n,2)
= …= n+(n-1)+…+2+T1(n,n-1)
= n+(n-1)+ …+2+n
= O(n^2)
所以调用fun(a,n,0)的时间复杂度为O(n^2)。
分析调用fun(a, n, 0)的空间复杂度。
解:设fun(a,n,0)的空间为S(n),fun(a,n,k)的空间为S1(n,k)
S(n) = S1(n,0)。
S1(n,k) = 1 当k=n-1时
S1(k) = 1+S1(n,k+1) 其他情况
则:
S(n) = S1(n,0) = 1+S1(n,1) = 1+1+S1(n,2)
= … = 1 + 1 + … + 1
= O(n)
所以调用fun(a,n,0)的空间复杂度为O(n)。
【例】分析下列程序段的时间复杂度。
int max(int a[],int i,int j){
int mid=(i+j)/2,max1,max2;
if (i<j){
max1=max(a,i,mid);
max2=max(a,mid+1,j);
return (max1>max2)?max1:max2;
}else return a[i];
}
分析调用max(a,0,n-1)的时间复杂度。
解:设调用max(a,0,n-1)的执行时间为T(n)
递归算法max(a,i,j)的执行时间为T1(n) (n=j-i+1)
有T(n)=T1(n)
T(n)=O(1) 当n=1(i=j的情况)
T(n)=2T(n/2)+1 当n>1(i<j的情况)
T(n) = 2T(n/2) + 1
= 2[2T(n/2^2) + 1] + 1 = 2^2T(n/2^2) + 2 + 1
= …
= 2kT(n/2k) + 2k-1 + … + 2 + 1 (k=log2n)
= 2k + 2k-1 + … + 2 + 1
= 2*2k - 1
= O(n)
调用max(a,0,n-1)的时间复杂度为O(n)