线性表
什么是线性表?
线性表:由零个或多个数据元素足证的有限序列。
- 首先,它是一个序列,元素之间有一个先来后到。
- 若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他元素都有一个前驱和后继。
- 另外,线性表强调是有限的,事实上无论计算机发展到多强大,它所处理的数据都是有限的。
- 允许有空表,线性表元素的个数n=0时,称为空表。
抽象数据类型:
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
例如:整型、浮点型、字符型这些指的就是数据类型。
例如在C语言中,按照取值的不同,数据类型可以分为两类:
- 原子类型:不可以再分解的基本类型,例如:整形、浮点型、字符型等。
- 结构类型:由若干个类型组合而成,可以再分解,例如整型数组是由若干个整型数据组成的。
- 抽象类型:是指一个数学模型及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,与其在计算机内部如何表示和实现无关。例如我们定义一个坐标point由x、y、z三个整型数据组合,那么point就是一个抽象数据类型。
抽象数据类型标准格式:
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Opreration
操作
endADT
线性表的类型定义
ADT LIST{
数据对象:D={ai|ai属于elemset,(i=1,2,...n,n>=0)}
数据关系:R ={<ai-1,ai>|ai-1,ai属于D,(i=2,3,...,n)}
基本操作:
initList(&L); //初始化
DestroyList(&L); //销毁
ListInsert(&L,i,e);//插入
ListDelete(&L,i,&e);//删除
...等等
}ADT List
基本操作(一)
-
InitList(&L)
构造一个空的线性表L。 -
DestroyList(&L)
初始条件: 线性表L必须存在
操作结果:销毁线性表L -
ClearList(&L)
初始条件:线性表L必须存在
操作结果:将线性表L重置为空表
基本操作(二)
- ListEmpty(L)
初始条件: 线性表L必须存在
操作结果:若线性表L为空表(n=0),则返回TURE;否则返回FALSE。 - ListLength(L)
初始条件: 线性表L必须存在
操作结果:返回线性表L中的数据元素个数。
基本操作(三)
- GetElem(L,i,&e)
初始条件: 线性表L必须存在 ,1<=i<=ListLength(L).
操作结果:用e返回线性表L中第i个数据元素的值。 - LiocateElem(L,e,compare())
初始条件: 线性表L必须存在 ,compare()是数据元素判定元素。
操作结果:返回线性表L中第一个与e满足compare()的数据元素的位序,若不存在返回0.
基本操作(四)
- PriorElem(L,cur_e,&pre_e)
初始条件: 线性表L必须存在
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pre_e无意义 - NextElem(L,cur_e,&next_e)
初始条件: 线性表L必须存在
操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的前驱,否则操作失败;next_e无意义
基本操作(五)
- ListInsert(&L,i,e)
初始条件: 线性表L必须存在,1<=i<=ListLength(L)+1.
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一
基本操作(六)
- ListDelete(&L,i,&e)
初始条件: 线性表L必须存在,1<=i<=ListLength(L).
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一。 - ListTraverse(&L,visited())
初始条件: 线性表L必须存在
操作结果:遍历,依次对线性表中每个元素调用visited()
线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
线性表的第一个数据元素的存储位置称为起始位置或基地址。
依次存储,地址连续,中间没有空的存储单元。
1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|
是一个典型的线性表顺序存储结构。
若地址不连续,中间存在空的存储单元。
1 | 2 | 3 | 6 | 7 |
---|
这就不是一个线性表顺序存储结构。
所以说,顺序表顺序存储结构必须占用一片连续的存储空间,只要知道某个元素的存储位置就可以知道其他元素的存储位置。
假设线性表的每个元素需要L个存储单元,则第i+1个数据元素的存储位置和第i个数据元素的存储位置之间的关系满足:
LOC(ai+1)=LOC(ai)+L
由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
LOC(ai)=LOC(ai)+(i-1)*L
我们可以发现,顺序表的存储方式与数组类似,所以,我们可以用一维数组表示顺序表,但是我们的数组不能动态分配。所以我们用一个变量表示顺序表的长度属性。
所以我们可以这样定义:
typedef char ElemType;
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量
typedef struct{
ElemType elem[LIST_INIT_SIZE]; //静态分配数组
int length;//当前长度
}SqList;
typedef char ElemType;
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量
typedef struct{
ElemType *elem;//动态分配数组
int length;//当前长度
}SqList;
//动态分配
SqList L;
L.elem=(ElemType*)malloc(sizeof(ElemType)*LIST_INIT_SIZE);
ELemType是你需要用到的数据类型,根据问题进行修改,或者定义。
线性表的基本操作:
操作算法中用到的预定义常量和类型
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型。其值是函数结果状态代码
typedef int Status;
typedef char ElemType;
- 线性表L的初始化(参数用引用)
Status InitList_Sq(SqList &L){ //构造一个空的顺序表L
L.elem=new ElemType[MAXSIZE]; //为顺序表分配空间
if(!L.elem)exit(OVERFLOW); //存储分配失败
L.length=0; //空表长度为0
return OK;
}
- 销毁线性表
void DestroyList(SqList &L){
if(L.elem)
{
free(L.elem);//释放存储空间
}
}
- 清空线性表
void ClearList(SqList &L){
L.length=0; //将线性表的长度置为0
}
- 求线性表的长度
int GetLength(SqList L){
return L.length;
}
- 判断线性表L是否为空
int IsEmpty(SqList L){
if(L.length==0)return 1;
else return 0;
}
- 顺序表的取值(根据位置i获取相应位置数据元素的内容)
int GetElem(SqList L,int i,ElemType &e){
if(i<1||i>L.length) //判断i值是否合理,若不合理,返回ERROR
retrun ERROR;
else e = L.length[i-1]; //将i-1存储单元的数据给e
return OK;
}
- 顺序表的查找(按值查找,在L中与指定值e相同的数据元素的位置)
从表的一端,逐个进行记录的关键字和给定值比较,找到返回元素位置序号,未找到,返回0。
int LocateElem(SqList L,ElemType e){
//在线性表L中查找值为e的数据元素,返回其序号(是第几个元素)
for(i=0;i<L.length;i++)
{
if(L.elem[i]==e)
{
return i+1; //查找成功,返回序号
}
}
return 0; //查找失败,返回0
}
这里引出一个概念,平均查找长度ASL(Average Search Length):
-为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。
ASL=∑Pi(找到第i给个记录需要比较的次数) * Ci(第i个记录被查找的概率);
所以,顺序查找的平均查找长度是:ASL=P1+2P2+…+(n-1)Pn-1+nPn
假设每个记录的查找概率相等:Pi=1/n
则:ASL=(1/n)([n(n+1)]/2)=(n+1)/2
- 插入算法:线性表的插入运算是指在第i(1<=i<=n+1)个位置上,插入一个新结点e,使长度为n的线性表(a1,…ai-1,ai…an)变成长度为n+1的线性表(a1,…ai-1,e,ai…an)
算法思想:
- 判断插入位置i是否合法
- 判断顺序表的存储空间是否已满,若满了返回ERROR。
- 将第n到i位的元素依次向后移动一个位置,空出第i个位置。
- 将要插入的新元素e放入第i个位置。
- 将表长度+1,插入成功返回OK
Status ListInsert_Sq(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1)return ERROR; //i值不合法
if(L.length==MAX_SIZE)return ERROR; //当前存储空间已满
for(j=L.length-1;j>=i-1;j--)
{
L.elem[j+1]=L.elem[j]; //将i-1之后所有元素后移
}
L.elem[i-1]=e; //将新元素e放入第i个位置
L.length++; //表长+1;
return OK;
}
ASL=1/(n+1)*0+1/(n+1)*1+1/(n+1)*2+…+1/(n+1)*n = n/2.
*删除算法:线性表的删除运算是指将表的第i(1<=i<=n)个节点删除,使长度为n的线性表(a1,…,ai-1,ai,…,an)变为长度为n-1的线性表(a1…,ai-1,ai+1,…an)
算法思想:
- 判断删除位置i是否合法
- 将预删除的元素保留在e中。
- 将第i+i至n位的元素依次向前移动一个位置。
- 表长-1,删除成功返回OK
Status ListDelete_Sq(SqList &L,int i){
if(i<1||i>L.length) return ERROR; //i不合法
for(j=i;j<L.length;j++)
{
L.elem[j]=L.elem[j+1]; //被删除元素之后的元素前移一位
}
L.length--; //表长-1
return OK;
}
ASL=1*(n-1)+2*(n-2)+…+n*0 = (n-1)/2
小结: 顺序表的特点
- 利用数据元素的存储位置表示线性表中相邻数据元素的前后关系,既线性表的逻辑结构与存储结构一致。
- 在访问线性表时,可以快速的计算出任何一个数据元素的存储地址。因此可以粗略的认为,访问每个元素所花的时间相等。
- 这种存取元素的方法称为随机存取法。
顺序表的操作算法分析:
- 时间复杂度O(n)
- 空间复杂度O(1)
顺序表的优缺点:
- 优点
- 存储密度大(节点本身所占存储量/结点结构所占存储量)
- 可以随机存取表中任意元素
- 缺点
- 在插入、删除运算中,需要移动大量元素。
- 浪费存储空间
- 输入静态存储形式,数据元素的个数不能自由扩充
线性表的链式表示和实现
- 链式存储结构:
用一组物理位置任意的存储单元来存放线性表的数据元素。
链表中元素的物理位置和逻辑位置不一定相同。
-与链式存储有关的术语
- 结点:数据元素的存储映像。由数据域和指针域两部分组成。
- 链表:n个结点由指针链组成一个链表。
- 单链表、双链表、循环链表:
结点只有一个指针域的链表,称为单链表(存后继)
每一个结点有两个指针域,称为双链表(第一个指针域存前驱,后一个指针域存后继)
首尾相接的链表称为循环链表。 - 头指针、头结点和首元结点:
头指针(head):是指向链表中第一个结点的指针。
首元结点:是指链表中存储第一个数据元素a1的结点。
头结点:是在链表的首元结点之前附设的一个结点。
- 所以链表的存储结构有两种形式:
- 不带头结点
- 带头结点
- 带头结点的单链表
typedef struct Lnode{ //声明结点类型和指向节点的指针类型
ElemType data; //结点的数据域
struct Lnode* next; //结点的指针域
}Lnode,*LinkList;
//LinkList为指向结构体Lnode的指针类型
- 单链表的初始化,即构造一个空表。
步骤:1.生成新结点作为头结点,用头指针L指向头结点。2.将头结点的指针域置空。
Status InitList_L(LinkList &L){
L = (LinkList)malloc(sizeof(LNode));
L->next=NULL;
return OK;
}
- 判断链表是否为空
Status IsEmpty(LinkList L){//若L为空,返回1,否则返回0
if(L->next==1){//非空
return 0;
}
else return 1;
}
- 单链表的销毁:链表销毁后不存在
从头指针开始,依次释放所有结点
Status DestroyList_L(LinkList &L){
Lnode* p;
while(L){
p=L;
L=L->next;
free(P);
}
return OK;
}
- 清空单链表:链表仍存在,但链表中无元素,成为空链表。
Status ClearList_L(LinkList &L){
Lnode* p;
Lnode* q;
p=L->next;
while(p){
q=p->next;
free(p);
p=q;
}
L->next=NULL; //头结点指针域为空
return OK;
}
- 求单链表长
int ListLength_l(LinkList L){
Lnode*p;
int count=0;
p=L->next;
if(p==NULL){
return 0;
}else{
while(p){
count++;
p=p->next;
}
}
return count;
}
- 读取第i个元素
status List(LinkList L,int i,ElemType &e){
int j=1;
Lnode* p;
p=L->next;
while(p&&i<j){//向后扫描,直到p指向第i个元素,或p为空
p=p->next;
j++;
}
if(!p||j>i)return ERROR;//第i个元素不存在
e=p->data; // 取第i个元素
return OK;
}
- 单链表的按值查找——根据指定数据获取该数据所在的位置(地址)
//返回地址
Lnode *LocateElem_L(LinkList L,ElemType e){
LinkList p;
p=L->next;
while(P&&p->data!=e){
p=p->next;
}
return p;
}
//返回序号
int LocateElem_L(LinkList L,ElemType e){
LinkList p;
p=L->next;
j=1;
while(p&&p->data!=e){
p=p->next;
j++;
}
if(p) return j;
else return 0;
}
- 插入——在第i个结点前插入值为e的新节点
status ListInsert_L(LinkList &L,int i,ElemType e){
Lnode* p,s;
p=L;
int j=0;
while(p&&j<i-1){
p=p->next;
j++;
}// 寻找第i-1个结点,p指向i-1结点
if(!p||j>i-1)return ERROR;//i大于表长或小于一,插入位置非法
s=(Linklist)malloc(sizeof(Lnode));
s.data=e;
s->next=p->next;
p->next=s;
}
- 删除结点
Status ListDelete_L(LinkList &L,int i,ElemType &e){
Lnode* p,q;
p=L;
int j=0;
while(p->next&&j<i-1){
p=p->next;
j++;
}//寻找第i个结点,并令p指向其前驱
if(!(p->next)||j>i-1)return ERROR;//删除位置不合理
q=p->next;//q指向要删除的结点
p->next=q->next;//p指向删除之后的结点
e=q->data;//将删除的元素保存在e中
free(q);//释放要删除的结点
return OK;
}
- 头插法建立单链表:元素插入在链表头部,所以也叫前插法
- 从一个空表开始,重复读入数据。
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将各结点插入到链表的前端
void CreateList_L(LinkList &L,int n){
L=(LinkList)malloc(siziof(Lnode));
L->next=NULL;//建立一个带头结点的单链表
for(i=n;i<0;i--){
p=(LinkList)malloc(sizeof(Lnode));//生成新结点p
scanf("%d",&p->data);//输入元素值
p->next=L->next;//插入到表头
L->next=p;
}
}
- 尾插法建立单链表–元素插入到链表尾部,也叫后插法
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点
2.初始时,r和L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
void CreatList_R(LinkList &L,int n){
L=(LinkList)malloc(siziof(Lnode));
L->next=NULL;//建立一个带头结点的单链表
r=L; //尾指针r指向头结点;
for(i=0;i<n;i++){
p=LinkList)malloc(siziof(Lnode));
scanf("%d",&p->data);//生成新结点,输入元素值
p->next=NULL;
r->next=p;//插入到表尾
r=p;//让r指向新的尾结点
}
}
循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
- 优点:从表中任一结点出发均可找到表中其他结点。
- 注意:由于循环链表中没有NULL指针,所以遍历操作时,其终止条件就不像非循环链表那样判断p或p->next是否为空,而是判断他们是否等于头指针。
表的操作常常是在表的首尾进行,所以可以使用尾指针表示单循环链表,a1的存储位置是:r->next->next; an的存储位置是r;
如何将两个带尾指针的循环列表合并
LinkList Connect(LinkList Ta,LinkList Tb)
{
p=Ta->next; //p存表头结点
Ta->next=Tb->next->next;//Tb表头链接Ta表尾
free(Tb->next);//释放Tb的表头结点
Tb->next=p;//修改指针
return Tb;
}
双向链表:
prior | data | next |
---|---|---|
指向前驱结点 | 数据元素 | 指向后继结点 |
双向链表的结构可定义如下
typedef struct DuLNode{
ElemType data;
struc DuLNode *prior,*next;
}DuLNode,*DuLinkList;
和单链的循环链表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指向头结点
双向链表的插入操作:
void ListInsert_DuL(DuLinkList &L,int i,ElemType e){
if(!p=GetElemP_DuL(L,i))return ERROR;
s=(DuLNode*)malloc(sizeof(DuLNode));
s->data=e;
s-prior=p->prior;
p->piror->next=s;
s->next=p;
p->prior=s;
return OK;
}
双向链表的删除操作:
void ListDelete_DuL(DuLink &L,int i,ElemType &e)
{
if(!p=GetElem_DuL(L,i)) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return OK;
}
顺序表和链表的比较
- 链式存储结构的优点:
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
- 链式存储结构的缺点:
- 存储密度比较小,每个结点的指针域需额外占用存储空间,当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
- 链式存储结构是一种非随机存取的结构,对于任一结点的操作都需要从头指针依指针链查找到该结点,增加了算法的复杂度。
- 存储密度比较小,每个结点的指针域需额外占用存储空间,当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
在这里插入图片描述