数据结构绪论

**

第一章

数据结构绪论

**
数据结构所要研究的问题:计算机主要用于非数值运算,包括处理字符、表格、和图像等具有一定结构的数据。这些数据内容存在着某种联系,只有分清数据之间的内在联系,合理地组织数据,才能对它们进行有效的处理,设计出高效的算法。究竟该如何合理地组织数据、高效地处理数据,这就是“数据结构”主要研究的问题。
数据的存储可以是一种线性关系,例如学生学籍管理系统,学生的基本信息,包括学生的学号、姓名、性别、籍贯、专业等基本信息。每个学生的基本情况按照不同的顺序号,依次存放在学生基本信息表中,根据需要对这张表进行查找。每个学生的基本信息记录按顺序号排列,形成了学生基本信息记录的线性序列,呈一种线性关系。例如这种线性关系的还有书目管理系统、库存管理系统等。在这种类型中,计算机处理的对象是各种表,元素之间是简单的一对一的关系,施加于对象上的操作有查找、插入和删除等。这类数据模型称为一种“线性”的数据结构。
比如人机对弈问题,其数据模型就是如何用树结构表示棋盘和棋子,算法是博弈的规则和策略。此外,诸如树结构模型的对象如计算机的文件系统,一个单位的组织机构。
最短路径问题的数学模型就是图结构,算法是求解两点之间的最短的路径。诸如此类的图结构还有网络工程图和网络通信图,在这类问题中,元素之间是多对多的网状关系,施加于对象上的操作依然有查找,插入和删除。
非数值计算问题的数学模型不再是数学方程,而是诸如线性表、图和树的数据结构,因此,可以简单地来说,数据结构是一门研究非数值计算程序设计中的操作对象,以及这些对象之间的关系和操作的学科。
我们可以认为数据结构是介于数学、计算机硬件和软件三者之间的一门核心课程。
数据:是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。如数学计算中用到的整数和实数,多媒体程序处理的图形、声音及动画等通过特殊编码定义后的数据。
数据元素:是数据的基本单位,在计算机中通常作为一个整体来进行考虑和处理,在有些情况下,数据元素也称元素、记录等。数据元素用于完整地描述一个对象,如信息表中的一个学生的信息,棋盘中的一个格局,图中的一个结点。
数据项:是组成数据元素、有独立含义的、不可分割的最小单位。例如学生信息表中的学号、姓名、性别等都是数据项。
数据对象:是性质相同的数据元素的集合,是数据的一个子集。也可以这么理解,如果集合内元素的性质均相同,都可称之为一个数据对象。
数据结构是相互存在的一种或多种特定关系的数据元素的集合,换句话来说,数据结构就是带“结构”的数据元素的集合,“结构”就是数据元素之间存在的关系。
数据结构包括逻辑结构与存储结构两个层次。
1.逻辑结构
数据的逻辑结构是从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。因而数据的逻辑结构可以看做从具体问题中抽象出来的数学模型。
数据的逻辑结构有两个要素:一个是数据元素,第二个是关系。
数据元素的含义如前所述,关系是指数据元素之间的逻辑关系。根据元素之间关系的不同特性,通常有四类基本结构(集合、线性、树、图)其复杂关系依次递进。
集合结构:比如根据确定一名学生是否为班级成员,只需要将班级看做一个集合结构。
线性结构:数据元素之间的一对一的关系,如将学生的数据信息按照学生的报道时间的先后顺序来进行排列,进而组成一个线性结构。
树结构:数据元素之间存在一对多的关系,比如在一个班级里,班长管理多个组长,组长管理多个组员,从而构成树形结构。
图结构或网状结构:数据元素之间存在多对多的关系,如多位同学之间的朋友关系,任何两位同学都可以是朋友。
非线性结构:集合结构、树结构、图结构。
线性表包括线性表、栈和队列、字符串、数组、广义表。
非线性结构包括树和二叉树,有向图和无向图。
存储结构:数据对象在计算机中的存储表示称为数据的存储结构,也称为物理结构。把数据对象存储到计算机时,通常要求既要存储各个元素的数据,又要存储数据元素之间的逻辑关系,数据元素在计算机内用一个结点表示。数据元素在计算机中有两种存储结构,分别是顺序存储结构和链式存储结构。
1.顺序存储结构,顺序存储结构是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系,通常借助程序设计语言的数组类型来描述,可以假定结点,那么每个结点占用的存储单元是相等且一致的。
2.链式存储结构:顺序存储结构要求所有元素依次存放在一片连续的存储空间中,而链式存储结构,无需占用一整块存储空间。但为了表示结点之间的关系,需要给每个结点附加指针字段,用于存放后继元素的存储地址,所以其需要借助程序设计语言中的指针类型来表示。
每个数据元素相当于一个结点,并且每个结点占用两个连续的存储单元,一个用于存放结点的信息,一个用于存放后继结点的首地址。
1.2.3数据类型和抽象类型
1.数据类型:是高级程序设计语言中的一个基本的概念,所以数据结构与数据类型的概念密切相关。
一方面,每一个数据都属于一种数据类型,类型明显或隐含地规定了数据的取值范围、存储方式以及允许进行的运算,数据类型是一个值的集合和定义在这个值集上的一组操作的总称。
2.抽象数据类型:一般指由用户定义的,表示应用问题的数学模型,以及定义在这个模型上的一组操作的总称,具体包括三个部分:数据对象,数据对象上的关系的集合以及对数据对象的基本操作的集合。
抽象数据类型的定义格式如下:
ADT 抽象类型数据名:
{
数据对象:{数据对象的定义}
数据关系:{数据关系的定义}
基本操作:{基本操作的定义}
}ADT抽象数据类型名
其中,数据对象和数据关系的定义采用数学符号和自然语言描述,基本操作的定义格式为:
基本操作名(参数表)
初始条件:{初始条件描述}
操作结果:{操作结果描述}
基本操作有两种参数:赋值操作只为操作提供输入值,引用参数以“&”打头,除可提供输入值外,还将返回操作结果。“操作结果”说明了操作正常完成之后,数据结构的变化状况和应返回的结果。
1.3抽象数据类型的表示和实现
运用抽象数据类型描述数据结构,有助于在设计一个软件系统时,不必首先考虑其中包含的数据对象,以及操作在不同处理器中的表示和实现细节,而是在构成软件系统的每个相对独立的模块上定义一组数据和相应的操作,把这些数据的表示和操作细节留在模块内部解决,在更高层次上进行软件的分析和设计,从而提高软件的整体性能和利用率。
抽象数据类型的概念与面向对象方法的思想是一致的。抽象数据类型独立于具体实现,将数据和操作封装在一起,使得用户程序只能通过抽象数据类型定义的某些操作来访问其中的数据,从而实现了信息的隐藏。
最终表示和实现抽象数据类型,最好使用面向对象的方法。
(1)、预定义常量及类型:
//函数结果状态代码
#define OK 1
#define ERROR 0
#define OVERFLOW -2
//status 是函数返回值类型,其值是函数结果的状态代码
typedef int Status;
(2)、数据结构的表示(存储结构)用类型定义(typedef)描述;数据元素类型约定为Elemtype,由用户在使用该数据类型时自行定义。
(3)、基本操作的算法都用如下格式的函数来表示:
函数类型 函数名 (函数参数表)
{
//算法说明
语句序列
}//函数名
当函数返回值为函数结果状态代码时,函数定义为Status类型。为了便于描述算法
(4)、内存的动态分配与释放
使用new和delete动态分配和释放内存空间:
分配空间:指针变量=new数据类型;
释放空间:delete指针变量;
(5)、赋值语句
简单赋值:变量名=表达式;
串联赋值:变量名1=变量名2=变量名3=…=变量名n;
成组赋值:(变量名1…变量名n)=(表达式1…表达式n);
结构赋值:结构名1=结构名2;结构名=(值1,值2,值3)
条件赋值:变量名=条件表达式?表达式T:表达式F;
交换赋值:变量名1<—>变量名2;
(6)、条件语句
条件语句1 if(表达式)语句;
条件语句2 if(表达式)语句;
else 语句;
开关语句 switch(表达式)
{
case 值1:语句序列 1;break;
case 值2:语句序列 2;break;

case 值n:语句序列 n;break;
default: 语句序列 n+1;
}
(7)、循环语句:
for语句 for(表达式1;条件;表达式2) 语句;
while语句 while(条件)语句;
do-while语句 do {
语句序列;
}while(条件);
(8)、结束语句:
函数结束语句 return 表达式;
return;
case或循环结束语句break;
异常结束语句 exit (异常代码);
(9)、输入输出语句使用C++流式输入输出的形式:
输入语句 cin>>变量1>>…>>变量n
输出语句 cout<<表达式1<<…<<表达式n
(10)、基本函数
求最大值 Max(表达式 1,…,表达式 n)
求最小值 Min(表达式1,…,表达式n)
下面以复数为例,给出一个完整的抽象数据类型的定义、表示和实现。
(1)、定义部分:
ADT Complex {
数据对象:D={e1,e2|e1,e2∈R,R为实数集}
数据关系:S={<e1,e2>|e1为复数的实部,e2为复数的虚部}
基本操作:
Create(&C,x,y)
操作结果:构造复数C,其实部和虚部分别被赋予参数x和y的值。
GetReal©
初始条件:复数C已经存在;
操作结果:返回复数C的实部值;
GetImag©
初始条件:复数C已经存在;
操作结果:返回复数C的虚部值;
Add(c1,c2)
初始条件:c1,c2是复数;
操作结果:返回两个复数C1和C2的和;
Sub(c1,c2)
初始条件:c1,c2是复数;
操作结果:返回两个复数C1和C2的差;
}ADT Complex

typedef struct              //复数类型
{
       float Realpart;  //实部
       float Imagepart;  //虚部
}Complex;
void Create(&Complex C,float x,float y)
{
  C.Realpart=x;
  C.Imagepart=y;//构造一个复数
}
float GetReal(Complex)
{
  return C.Realpart;   //取复数的虚部
}
float GetImag(Complex)
{
  return C.Imagepart;    //取复数的虚部
}
Complex Add(Complex C1,Complex C2)
{
  Complex sum;
  sum.realpart=C1.realpart+c2.realpart;
  sum.Imagepart=C1.Imagepart+C2.Imagepart;
  return sum;
}
Complex Sub(Complex C1,Complex C2)
{
  complex difference;
  difference.Realpart=C1.Realpart-C2.Realpart;
  difference.Imagepart=C1.Imagepart-C2.Imagepart;
  return difference;
}

通过这样定义后,就可以在主程序中通过调用Create函数构造一个复数,通过调用Add或Sub函数实现复数的加法或者减法运算,用户可以像使用整数类型那样使用复数类型了。
1.4******算法和算法分析
在数据结构中,将遇到大量的计算问题,因为算法联系着数据在计算过程中的组织方式,为了实现某种操作,常常需要设计算法,因而算法是研究数据结构的重要途径。
1.4.1算法的定义及其特性
算法是为了解决某类问题而规定的一个有限长的操作序列。
一个算法必须满足以下5个重要特性。
(1)、有穷性:一个算法必须总是在执行有穷步后结束,且每一步都必须在有穷时间内完成,
(2)、确定性:对于每种情况下所应执行的操作,在算法中都有确切的规定,不会产生二义性。使算法的执行者与阅读者都能够明确其含义及如何执行。
(3)、可行性:算法中所有操作都可以通过已经实现的基本操作运算执行有限次来实现。
(4)、输入:一个算法有零个或多个输入。当用函数描述算法时,输入往往是通过形参表示的,在它们被调用时,从主调函数中获得输入值。
(5)、输出:一个算法有一个或多个输出,它们是算法进行信息加工后得到的结果,无输出的算法没有任何意义。当用函数描述算法时,输出多用返回值或引用类型的形参来表示。
1.4.2评价算法优劣的基本标准
(1)、正确性:在合理的数据输入下,能够在有限的运行时间内得到正确的结果。
(2)、可读性:一个好的算法,首先应便于人们理解与相互交流,其次才是机器的可执行性。可读性强的算法有助于人们对算法的理解,而难懂的算法易于隐藏错误,且难于调试和修改。
(3)、健壮性:当输入的数据非法时,好的算法能适当地做出正确反应或进行相应处理,而不会产生一些莫名其妙的输出结果。
(4)、高效性:高效性包括时间和空间两个方面,时间高效是指算法的设计合理,执行效率高,可以用时间复杂度来度量;空间高效是指算法占用存储容量合理,可以用空间复杂度来度量。时间复杂度和空间复杂度是衡量算法的两个主要指标。
1.4.3算法的时间复杂度
算法效率的分析的目的是看算法实际是否可行,并在同一问题存在多个算法时,可进行时间和空间上的性能的比较,以便从中挑出最优的算法。
衡量算法效率的方法主要有两类:事后统计法和事前分析估算法,事后统计法需要先将算法实现,然后测算其时间和空间的开销,这种衡量方法存在两个缺陷,一个是需要把算法转换为可执行的程序,二是时空开销的测算结果依赖于计算机的软硬件等环境因素,这种算法易于掩盖算法本身的优劣,所以我们经常采用事前分析估算法,通过计算算法的渐进复杂度来衡量算法的效率。
1.问题规模和语句频度
不考虑计算机软硬件的因素,影响算法时间代价的最主要因素是问题规模。问题规模是算法求解问题中输入量的多少,是问题大小的本质表示,一般用整数n表示。问题规模n对不同问题的含义不同,例如在排序问题中n为参加排序的记录数,在矩阵运算中n为矩阵的阶数,在多项式运算中n为多项式的系数,在集合运算中n为集合中元素的个数,在树的运算中n为图的顶点数或边数,显然,n越大,算法的执行时间越长。
一个算法的执行时间大致上等于其所有语句执行时间的总和,而语句的执行时间则为该条语句的重复执行次数和执行一次所需要时间的乘积。
一条语句的重复执行次数称作语句频度。
由于语句的执行需要由源程序经编译程序翻译成目标代码,目标代码再经装配执行,执行一次实际所需的时间是与机器的软硬件(机器速度、编译程序质量)密切相关。所以,所谓的算法分析并非精确统计算法执行所需时间,而是针对算法中语句的执行次数做出估计,从而得到算法执行时间的信息。
设每条语句执行一次所需的具体时间均为单位时间,则一个算法的执行时间可用该算法中所有语句频度之和来衡量。
如下列问题(求两个n阶矩阵的乘积算法)

for(i=1;i<=n;i++)                  //语句频度为n+1
  for(j=1;j<=n;j++)                //语句频度为n*(n+1)
  { 
    c[i][j]=0;                     //频度为n^2
    for(k=1;k<=n;k++)              //频度为n^2*(n+1)
    c[i][j]=c[i][j]+a[i][k]*b[k][j]//频度为n^3
   }

该算法中所有语句的频度之和,是矩阵阶数n的函数,用f(n)表示之,换句话说,上述算法的执行时间与f(n)成正比。f(n)=2n^3+ 3*n^2+2n+1
通常,算法的执行时间是是随问题规模的增长而增长的,因此对算法的评价通常只需考虑其随问题规模增长的趋势。我们只需要考虑当问题规模充分大时,算法语句的执行次数在渐进意义下的阶。
T(n)=Of((n))
其表示随问题规模n的扩大,算法执行时间的增长率和f(n)的增长率相同,称做算法的渐进时间复杂度,简称时间复杂度。该定义说明了函数T(n)与f(n)具有相同的增长趋势,并且T(n)的增长至多趋向于函数f(n)的增长,符号“O”表示增长率的上限。
若算法可以用递归方法表示,,则算法的时间复杂度通常可以用递归方程来表示,此时将涉及递归方程求解问题。
常量阶示例:

for(i=0;i<10000;i++)
{
  x++;
  s=0;
}

线性阶示例:

for(i=0;i<n;i++)
{
  x++;
  s=0;
}

平方阶示例:

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=1;
for(i=1;i<=n;i++)
  for(j=1;j<=i;j++)
    for(k=1;k<=j;k++)
      x++;

对数阶示例:

for(i=1;i<=n;i=i*2)
{
   x++;
   s=0;
}

4.最好、最坏和平均时间复杂度
对于某些问题的算法,其基本语句的频度不仅仅与问题的规模相关,还依赖于其他的因素。
如在一维数组a中顺序查找某个值等于e的元素,并返回其所在位置

for(i=0;i<n;i++)
   if(a[i]==e)
   return i+1;
   return 0;

容易看出,此算法的语句2的频度不仅仅与问题规模有关,还与输入实例中数组a[i]的各个元素值及e的取值有关,若在第一个位置取到,那么其是最好的时间复杂度,若是最后一个位置,那么是最坏的时间复杂度,取二者的平均值,就是平均时间复杂度,指算法在所有可能情况下,按照输入实例以等概率出现时,算法计算量的加权平均值。
对算法时间复杂度的度量,我们更关心的是最坏情况下和最坏情况下的时间复杂度,然而在很多情况下,算法的平均时间复杂度难以确定。因此,通常只讨论算法在最坏情况下的时间复杂度,,即分析在最坏的情况下,算法执行时间的上界。在下面所讨论的时间复杂度,除了特别指明外,均指最坏情况下的时间复杂度。
1.4.4算法的空间复杂度
关于算法的存储空间需求,类似于算法的时间复杂度,我们采用渐进空间复杂度作为算法所需存储空间的量度。简称空间复杂度,它也是问题规模n的函数,记作 S(n)=O(f(n))
一般情况下,一个机器在机器上执行时,除了需要寄存本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的辅助存储空间。其中,对于输入数据所占的具体存储量取决于问题本身,与算法无关,这样只需要分析存储空间就可以了。若输入数据为一个常数,则称这个算法在原地工作,辅助空间为O(1),与问题规模有关。
如下所示例子可以帮助我们理解算法的空间复杂度
例:数组逆序,将一维数组a中的n个数逆序存放到原数组中
算法1:

for(i=0;i<n/2;i++)
{
    t=a[i];
    a[i]=a[n-i-1];
    a[n-i-1]=t;
}

算法2:

for(i=0;i<n;i++)
   b[i]=a[n-i-1];
for(i=0;i<n;i++)
   a[i]=b[i];

算法1需要另外借助一个变量t,与问题规模n大小无关,所以其空间复杂度为O(1)。
算法2需要另外借助一个大小为n的辅助数组b,所以其空间复杂度为O(n)。
对于一个好的算法,其时间复杂度和空间复杂度是相互影响的,当追求一个较好的复杂度时,可能会导致占用更多的存储空间,即可能会使空间复杂度的性能变差,反之亦然,不过,通常情况下,鉴于运算空间较为充足,人们都以算法的时间复杂度作为衡量算法优劣的指标。
1.5 小结
数据结构包括两个方面的内容:数据的逻辑结构和存储结构。同一个逻辑结构采用不同的存储方法,可以得到不同的存储结构,
存储结构是逻辑结构在计算机中的存储表示,有两类存储结构:集合结构、线性结构。
算法执行时间的数量级称为算法的渐进时间复杂度,T(n)=O(f(n)),它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,简称时间复杂度。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值