数据结构《第2章 线性表及其应用》.docx
《数据结构《第2章 线性表及其应用》.docx》由会员分享,可在线阅读,更多相关《数据结构《第2章 线性表及其应用》.docx(50页珍藏版)》请在冰豆网上搜索。
数据结构《第2章线性表及其应用》
第2章线性表及其应用
本章学习要点
◆掌握线性表的逻辑结构及相关概念。
◆掌握线性表的两种基本存储结构,即线性顺序表(顺序表)和线性链表(链表)的存储结构。
体会线性表在各种存储方式之间的差异及其各自的优缺点。
◆熟练掌握顺序表和链表上各种基本操作的实现过程。
◆灵活运用顺序表和链表的特点解决实际应用问题。
线性表(LinearList)是一种最基本、最常用的数据结构。
它有两种基本存储表示方法:
顺序表和链表。
线性表的基本操作主要有插入、删除和查找等。
本章将具体给出线性表的定义、存储结构、基本运算及其算法实现,最后给出线性表的应用实例。
2.1线性表的定义和基本运算
2.1.1线性表的定义
线性表是n(n≥0)个同类型数据元素(记录或结点)的有限序列:
。
记为:
。
其中:
a0是开始结点,an-1是终端结点,ak是ak+1的直接前驱结点,而ak+1是ak的直接后继结点(k=0,1,2,…,n-2);终端结点an-1没有后继结点,开始结点a0没有前驱结点;元素ai(i=0,1,2…,n-1)在表中的位置为i+1;元素的个数n称为线性表L的长度,长度为零的线性表叫空表。
需要说明的是:
在C/C++语言中,数组元素的下标是从0开始,有n个元素的数组A的所有元素为A[0],A[1],…,A[n-1]。
定义中第i个元素的下标为i-1,即元素的位置等于其下标加1。
日常生活中,线性表的例子很多:
【例2.1】L10=(1,3,5,7,9,2,4,6,8,10)是一个线性表。
其中的数据元素是整数,该线性表的长度为10。
按表中的排列顺序,元素1是第一个元素没有前驱,元素10是最后一个元素没有后继。
【例2.2】L36=(0,1,2,3,4,5,6,7,8,9,A,B,C,D,…,X,Y,Z)是一个线性表。
其中的数据元素是所有数字字符和所有大写字母字符,线性表的长度为36。
按表中的排列顺序,元素‘0’是第一个元素没有前驱,元素‘Z’是最后一个元素没有后继。
【例2.3】由图2.1所示的学生基本情况表L7也是一个线性表。
线性表中的元素是由5个数据项组成的纪录,表的长度为7。
2.1.2线性表的基本操作
线性表的基本运算主要有以下几种:
(1)初始化InitList(&L):
构造一个空线性表L的运算。
(2)提取GetElem(L,i,&e):
提取表L中第i个元素的值到e的运算。
(3)修改SetElem(&L,i,e):
修改表L中第i个元素的值为e的运算。
(4)查找LocateElem(L,e):
查找表L中第一个值与e相同的元素的位置。
(5)插入InsertList(&L,i,e):
将e插入到表L中的第i个位置。
(6)删除DeleteList(&L,i,&e):
删除表L中的第i元素,并用e返回该元素的值。
(7)求表长Length(L):
返回线性表L的长度。
(8)遍历TraverseList(L):
依次输出L中每个元素的值。
在线性表的其它操作中,有些操作可以通过调用基本操作来实现,有些操作比如线性表的排序和归并等操作的实现将在以后的章节中进行。
2.2线性表的顺序存储表示与实现
2.2.1线性表的顺序存储
1.顺序表概念
一个线性表在计算机中可以用不同的方法来存储,其中最简单和最常用的一种存储方式是将线性表中的所有元素,按照其逻辑顺序依次存储到计算机内存中的从指定位置开始的一块连续的存储单元中。
用这种存储方式表示的线性表称为线性顺序表简称顺序表。
设顺序表
中元素
的首地址为
,每个元素需要占用的字节数为
,则表中任一元素
的存储位置
可用下面的公式算出:
顺序表中各元素在内存中所占的位置如图2.2所示。
2.顺序表的结构定义
顺序表的存储结构和一维数组的存储结构极为相近;但是由于在线性表中允许执行插入和删除等操作,也就是说一个线性表的长度是可变的,为适应这种情况在C++语言中用以下结构类型来表示顺序表。
typedefintElemType;//为了方便上机调试程序,本章假定数据元素的类型为整型
constintMAXLEN=100;//定义顺序表存储空间大小的初始分配常量
constintINCSIZE=50;//定义顺序表存储空间的扩充常量值
structSqList//定义顺序表的结构类型为SqList
{
ElemType*elem;//存储空间的基址
intlength;//线性表的长度
intlistsize;//当前表的存储容量
intincsize;//一次增补的容量值
};
在以上结构表示下,许多关于线性表的运算(如存、取、求线性表的长度和置表空等操作)极易实现。
以下主要讨论关于顺序表的插入、删除、查找和遍历等操作。
2.2.2顺序表中基本操作的实现
1.初始化操作InitList_Sq(&L)
该操作的功能是构造一个空线性顺序表L。
算法思想
(1)在堆空间中为线性表动态分配一块连续的存储单元,返回首地址到elem;
(2)置线性表的长度值length为0;
(3)置表的当前容量listsize为动态分配时的大小;
(4)置容量扩充值incsize的大小为INCSIZE。
线性表L的初始化存储结构如图2.3所示。
算法实现
voidInitList_Sq(SqList&L,intmaxlength=MAXLEN,intincsize=INCSIZE)
{
L.elem=newElemType[maxlength];
//动态分配一个长度为maxlength的连续存储空间,类型为ElemType,返回首地址到L.elem
L.length=0;
L.listsize=maxlength;
L.incsize=incsize;
}
2.提取操作GetElem_Sq(L,i,&e)
该操作将顺序表L中第i个元素的值赋值到e中,如果提取成功返回1否则返回0。
算法实现
intGetElem_Sq(SqListL,inti,ElemType&e)
{
if(i<1||i>L.length)return0;//如果位置i不合理时返回0值
else
{e=L.elem[i-1];//通过参数e得到第i个元素的值
return1;
}
}
3.修改操作SetElem_Sq(&L,i,e)
修改操作将线性表L中第i个元素的值修改为e,如果修改成功返回1否则返回0。
算法实现
intSetElem_Sq(SqList&L,inti,ElemTypee)
{
if(i<1||i>L.length)return0;
else
{L.elem[i-1]=e;
return1;
}
}
4.查找操作LocateElem_Sq(L,e)
查找操作的功能是查找顺序表L中第一个值与e相同的元素的位置,如果查找成功返回1查找失败返回0。
算法实现
intLocateElem_Sq(SqListL,ElemTypee)
{
inti=0;
while(i=e)i++;
if(ielsereturn0;
}
算法分析
查找运算的基本操作为表中元素与数据e的比较。
假定表的长度为n,每个位置找到的机会都是相同的,即第i个位置找到的概率为
,那么查找操作的平均比较次数为:
。
查找的时间复杂度是按最坏的情况来考虑,即查找的是最后一个元素或找不到该元素,其比较次数为n次,所以查找的时间复杂度为:
。
5.插入操作InsertList_Sq(&L,i,e)
插入操作的功能是将元素e插入到顺序表L中L.elem的第i个位置,同时修改L的长度。
如果插入成功返回1,不成功返回0。
算法思想
(1)先编写函数IncSize(&L),其功能是将顺序表L的最大存储量增加L.incsize个元素的空间;
(2)如果插入的位置i不合理返回0,表示插入不成功;
(3)如果当前线性表的长度已经达到最大容量值L.listsize,则调用函数IncSize(&L)扩充容量;
(4)将L.elem中第i个以后的所有元素都依次向后移动一个位置,为第i个元素的插入留出一个空位;
(5)置L中第i个元素的值为e;
(6)将表的长度加1;
(7)返回值1表示插入成功。
算法实现
voidIncSize(SqList&L)
{//该函数的功能是另分配一个存储区并将L的最大存储量增加L.incsize个记录的空间
ElemType*a=newElemType[L.listsize+L.incsize];
for(inti=0;ia[i]=L.elem[i];//将L.elem中的元素复制到数组a中
delete[]L.elem;//收回L.elem所占空间
L.elem=a;//让L.elem指向a
L.listsize+=L.incsize;//修改最大存储空间
}
intInsertList_Sq(SqList&L,inti,ElemTypee)
{
intk;
if(i<1||i>L.length+1)return0;//插入位置不合理时返回0值
if(L.length==L.listsize)
IncSize(L);
for(k=L.length,i--;k>i;k--)
L.elem[k]=L.elem[k-1];//为第i个元素的插入留出一个空位
L.elem[i]=e;
L.length++;//修改长度
return1;
}
算法分析
从以上算法可见,在不考虑扩容的情况下,插入运算的时间主要花费在元素的向后移动上。
假定表的长度为n,每个位置插入的机会都是相同的,即第i个位置插入的概率为
,那么插入操作平均移动元素的次数为:
。
插入操作的时间复杂度是按最坏的情况考虑,即插入到表中开始的位置,所以元素的移动次数为n次,时间复杂度为
。
6.删除操作DeleteList_Sq(&L,i,&e)
删除操作的功能是删除顺序表L中的第i元素,并返回该元素的值到e中。
如果操作成功返回1,否则返回0。
算法思想
(1)如果删除的位置i不合理返回0,表示删除操作不成功;
(2)置e的值为L中第i个元素的值;
(3)将L中第i个以后的所有元素都依次向前移动一个位置;
(4)将表的长度减1;
(5)返回值1表示删除成功。
算法实现
intDeleteList_Sq(SqList&L,inti,ElemType&e)
{
if(i<1||i>L.length)return0;//如果删除的位置i不合理返回0
e=L.elem[i-1];//置e的值为L中第i个元素的值
while(i{L.elem[i-1]=L.elem[i];
i++;
}
L.length--;//将表的长度减1
return1;
}
算法分析
删除运算的时间主要花费在元素的向前移动上。
假定表的长度为n,每个位置删除的机会都是相同的,即第i个位置删除的概率为
,那么删除操作平均移动元素的次数为:
。
删除操作的时间复杂度是按最坏的情况考虑,即删除表中第一个元素,此时的元素移动次数为n-1次,时间复杂度为:
。
7.求表长的操作Length_Sq(L)
该操作的功能是返回顺序表L的长度,如果当前L是空表则返回0。
算法实现
intLength_Sq(SqListL){returnL.length;}
8.遍历操作TraverseList_Sq(L)
该操作的功能是顺序显示输出顺序表L中每个元素的值。
算法实现
#include“iostream.h”//将C++的输入输出流库头文件包含进来
voidTraverseList_Sq(SqListL)
{if(!
L.length)
cout<<"当前线性表为空表.\n";//将提示串输出到标准输出设备
else
{for(inti=0;icout<cout<}
}
2.2.3顺序表的综合演示程序
在前面顺序表的结构定义及其基本操作算法的基础上,本节给出顺序表的各个基本操作的综合演示程序代码以及运行结果。
1.建立一个有n个元素的顺序表
voidCreate_Sq(SqList&L)
{ElemTypee;
inti,n;
InitList_Sq(L);
cout<<"输入表的长度n:
";
cin>>n;
cout<<"输入"<\n";
for(i=1;i<=n;i++)
{cin>>e;
InsertList_Sq(L,i,e);
}
}
2.综合演示主程序
voidmain_Sq()
{
SqListL;
ElemTypee;
inti,num;
Create_Sq(L);
cout<<"顺序表的初始数据为:
\n";
TraverseList_Sq(L);
while
(1)
{cout<<"1--提取第i个元素,2--修改第i个元素的值,3--插入第i个元素\n";
cout<<"4--删除第i个元素的值,5--查找元素e的位置,6--显示当前表的内容\n";
cout<<"7--退出演示程序:
";
cout<<”输入选择:
”;cin>>num;
switch(num)
{
case1:
cout<<"输入位置i:
";cin>>i;
if(GetElem_Sq(L,i,e))cout<<"第"<
elsecout<<"输入的位置不合理\n";
break;
case2:
cout<<"输入修改位置i和值e:
";cin>>i>>e;
if(!
SetElem_Sq(L,i,e))cout<<"输入的位置不合理,修改不成功\n";
break;
case3:
cout<<"输入插入位置i和值e:
";cin>>i>>e;
if(!
InsertList_Sq(L,i,e))cout<<"插入的位置不合理,操作不成功\n";
break;
case4:
cout<<"输入删除位置i:
";cin>>i;
if(!
DeleteList_Sq(L,i,e))cout<<"删除位置不合理,删除操作失败\n";
elsecout<<"位置:
"<
"<break;
case5:
cout<<"输入要查找的值e:
";cin>>e;
if(!
(i=LocateElem_Sq(L,e)))cout<<"查找不成功\n";
elsecout<<"表中位置:
"<
"<break;
case6:
cout<<"顺序表的当前值为:
\n";
TraverseList_Sq(L);
break;
case7:
cout<<"欢迎使用顺序表演示程序\n";
return;
default:
cout<<"选择不合理重新选择\n";
}//end_switch
}//end_while
}//end_main_Sq()
(运行结果略)
2.2.4顺序存储结构小结
在顺序表的存储结构中,由于表中数据元素的存储顺序与元素之间的逻辑顺序完全一致,因此确定表中元素的存储位置非常容易,访问表中任一元素也就非常方便。
这是顺序存储方式的特点,也是顺序表的主要优点。
但是,这种存储方式也同时存在以下缺点:
(1)对顺序表进行插入或删除操作时,往往需要向后或向前移动大量元素,这一操作的时间开销很大,特别当每个元素中所含的信息量很大时,这个问题显得尤为突出;
(2)对顺序表的最大长度listsize以及一次增扩的长度incsize事先不易确定一个比较合适的大小。
如果选取的值较小而在实际运行时出现大量的插入操作,这将导致多次调用扩容函数IncSize(&L)来批量移动记录,从而浪费大量的时间;如果选取的值较大,但是在实际运行时很少进行插入操作,这将导致存储空间的大量浪费;
(3)顺序存储要求一次分配足够大的连续的内存空间,如果当前系统中没有连续的足够大的空闲空间可用,这将导致内存分配失败。
另一方面,这种分配方法会使系统空间中产生许多不可利用的空闲碎块,降低了空间资源的利用率。
鉴于以上顺序表存在的问题,后面将给出线性表的另一种存储结构——链式存储结构。
2.3线性顺序表的应用举例
【例2.4】有n个人围成一圈,顺序排号:
1、2、3、…、n。
从第一个人开始报数(从1到m报数),凡报到m的人退出圈子并记下此人的排号,当最后一个人退出圈子时,按出圈次序依次输出所有人原来的排号序列(用线性顺序表编程实现)。
voidmain()
{
intm,n,i,k;//定义人数n、间隔数m、循环变量i和出圈人下标k
ElemTypee;//定义出圈人编号e
SqListL;//定义线性顺序表L
cout<<"inputnm:
";
cin>>n>>m;//输入n、m的值
InitList_Sq(L);//初始化L
for(i=1;i<=n;i++)//置第i个人的编号为i
InsertList_Sq(L,i,i);
cout<<"所求编号序列为:
\n";
k=0;
for(i=0;i{
k=(k+m-1)%L.length;//计算出圈人在数组L.elem中的下标k
DeleteList_Sq(L,k+1,e);//删除第k+1个人并提取其编号到e中
cout<}
cout<}
运行演示结果为:
inputnm:
104
所求编号序列为:
48273109165
【例2.5】试编写一算法,实现对线性顺序表L的逆置操作。
即利用原表的存储空间将线性表L=(a0,a1,a2,…,an-1)逆置为L=(an-1,an-2,an-3,…,a0)。
voidInverst(SqList&L)//逆置顺序表中的元素
{
inti=0,j=L.length-1;//定义指向第一个元素和指向最后一个元素的下标i、j
ElemTypet;//定义临时变量t
while(i{
t=L.elem[i];
L.elem[i]=L.elem[j];
L.elem[j]=t;
i++,j--;//交换以后修改下标的相应值
}
}
voidmain()
{SqListL;
cout<<"建立一个线性顺序表:
\n";
Create_Sq(L);
Inverst_Sq(L);
cout<<"交换后的结果为:
\n";
TraverseList_Sq(L);
}
程序运行演示结果为:
建立一个线性顺序表:
输入表的长度n:
10
输入10个元素的值:
135791113151719
交换后的结果为:
191715131197531
2.4线性表的链式存储表示与实现
2.4.1线性表的链式存储
1.线性链表的概念
线性链表的存储结构是可以用任意一组存储单元来存放线性表中的数据元素,即这些存储单元在内存中可以是连续的也可以是不连续的。
为了能正确表示线性表中的每个数据元素以及它们之间存在的逻辑关系(前驱与后继关系),对于一个数据元素
来说,除了要存储它本身的信息之外,还需要存储指示其直接后继元素
的指针(地址)信息。
由以上两部分信息结合起来构成线性链表中的一个数据元素,称之为结点(node)。
结点中存储数据信息的区域称为数据域(data),而存储其直接后继元素存储位置的区域称为指针域(next)。
结点的结构如图2.4(a)所示。
含有n个数据元素的线性表
,按图2.4(b)所示的方法构成一个具有n个结点的链表,称为线性表的链式存储结构。
由于此种存储结构的链表中,每个结点只包含一个指针域,所以称之为线性链表或单链表。
在图2.4(b)中,h称为单链表的表头指针,它指向链表中第一个结点的存储位置。
由于最后一个结点没有直接后继,它的指针域为空,通常用NULL(或^)来表示。
为了对单链表的各种操作进行统一处理和方便表示,在链表的开头增设一个结点称为头结点,它的结构与链表中其他结点的结构一样,但在data域中不存放数据。
如图2.5(a)表示空表,而图2.5(b)表示非空表,它们在形式上是一致的。
2.单链表结点结构的定义
根据单链表中结点结构的特点,在C++语言中用以下结构类型来表示一个结点的结构类型:
typedefstructNode//定义单链表的结点结构类型Node
{
ElemTypedata;
Node*next;
}*LinkList;//定义指向结点Node的指针类型LinkList
以上语句同时定义了结点的结构类型Node和指向该类型结点的指针类型LinkList。
在这种结构的表示下,关于线性表的插入和删除操作相对于顺序表而言简单了许多。
2.4.2带头结点的单链表中基本操作的实现
1.初始化操作InitList_L(&L)
该操作是构造一个只含有头结点的空链表L。
其中L为指向头结点的指针。
算法实现
voidInitList_L(LinkList&L)
{L=newNode;//L指向头结点
L->next=NULL;//置头结点的指针域为空
}
2.提取操作GetElem_L(L,i,&e)
提取操作的功能为提取链表L中第i个结点的数据域(data)的值到e中。
如果提取成功(即位置i合理)返回1否则返回0。
算法实现
intGetElem_L(LinkListL,inti,ElemType&e)
{
intk;
LinkListp=L->next;
for(k=1;k
p=p->next;
if(i<1||!
p)return0;//位置i不合理返回0
e=p->data;
return1;
}
3.修改操作SetElem_L(&L,i,e)
修改操作修改单链表L中第i结点数据域data的值为e的值。
如果修改成功(即位置i