一、线性表的定义
零个或多个相同数据类型的数据元素的有限序列
二、线性表的抽象数据类型
ADT 线性表(List)
DATA
线性表的数据对象集合为{a1,a2,…,an),每个元素的类型均为DataType。
其中,除第一个元素a1外,每一个元素有且只有一个直接前驱;除最后一个元素an外,每一个元素有且只有一个直接后继。
每个元素直接的关系是一对一。
Operation
InitList(*L):初始化操作,建立一个空的线性表
ListEmpty(L):若表空则返回true;否则false
ClearList(*L):清空线性表
GetElem(L,i):按位查找操作,获取第i个位置的元素的值
LocateElem(L,e):按值查找操作,查找具有给定关键字的元素
ListInsert(&L,i,e):插入,插入e的值到第i个元素
ListDelete(&L,i,&e):删除,删除表中第i个位置的元素,并用e返回值
ListLength(L):返回线性表元素个数
PrintList(L):输出操作
DestroyList:销毁操作,释放表所占内存
endADT
三、线性表的存储结构
线 性 表 { 顺 序 存 储 链 式 存 储 线性表\left\{ \begin{matrix} 顺序存储\\链式存储\end{matrix} \right. 线性表{顺序存储链式存储
-
顺序存储——顺序表
-
链 式 存 储 { 单 链 表 ( 指 针 实 现 ) 双 链 表 ( 指 针 实 现 ) 循 环 链 表 ( 指 针 实 现 ) 静 态 链 表 ( 数 组 实 现 ) 链式存储\left\{ \begin{matrix} 单链表(指针实现)\\双链表(指针实现)\\循环链表(指针实现)\\静态链表(数组实现)\end{matrix} \right. 链式存储⎩⎪⎪⎨⎪⎪⎧单链表(指针实现)双链表(指针实现)循环链表(指针实现)静态链表(数组实现)
四、线性表(顺序表)的顺序存储
1、定义
用一段地址连续的存储单元依次存储线性表的数据元素
特点是:逻辑顺序与其物理顺序一致
2、存储方式
线性表的每个数据元素的类型都相同,故可以用一维数组来实现顺序存储。
跟图书馆占座的原理一样,先在内存中找地儿占了一段空间,再把数据类型相同的元素放在这空地上。
但由于随着数据的插入或删除,线性表的空间大小是随时在改变的。
线性表的顺序存储的代码:
(假设线性表的元素类型为ElemType)
- 静态分配:(空间一旦占满会溢出)
#define MAXSIZE 20 //存储空间初始分配量(线性表的最大长度)
typedef struct{
ElemType data[MAXSIZE]; //数组。存数据元素
int length; //线性表当前长度
}SqList
- 动态分配:
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int Maxsize,length; //数组的最大容量和当前个数
}SqList;
Status InitList_Sq(SqList &L) //构成一个空的线性列表L
{
L.elem=(ElemType *)malloc(InitSize *sizeof(ElemType));
if(!L.elem) exit(OVERFLOW); //为该数据元素分配空间失败
L.length=0; //目前元素个数为0
L.listsize=InitSize; //目前线性表的存储空间有InitSize
return OK;
}
注意:动态分配并不是链式存储!
顺序存储结构的三个属性:
- 存储空间的起始位置:数组data的存储位置
- 线性表的最大存储容量:数组长度MAXSIZE
- 线性表的当前长度:length
数组长度与线性表长度的区别:
数组的长度是存放线性表的存储空间的长度,分配之后一般是不变的;
线性表的长度是线性表中数据元素的个数,随着对表的操作时刻改变;
3、地址计算方法
存储器中的每个存储单元都有自己的编号(即地址)
假设每个数据元素的存储空间都为c,我们可以得出任意数据元素ai的存储位置:
LOC(Ai)=LOC(a1)+(i-1)*c
这个公式对计算机来说时间复杂度为O(1).
我们将有这种特点的存储结构称为随机存取结构(每个数据元素的存储位置都和线性表的起始位置相差一个和该元素的位序成正比的常数), 这也是顺序表最主要的特点。
4、顺序存储结构的插入与删除
· 插入操作
在顺序表L的第i个位置插入新元素e。若i的输入不合法,则返回false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移动一个位置,腾出一个新位置给e,顺序表长度增加1,返回true
bool ListInsert(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1) //判断i的范围是否有效(注意是L.length+1!!!)
return false;
if(L.length>=MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length;j>=i;j--) //将第i个元素及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e; //在位置i放入e
L.length++; //线性表长度加1
return true;
}
插入操作的时间复杂度为O(n).
· 删除操作
删除顺序表L中第i个位置的元素,用引用变量e返回。若i的输入不合法,则返回false;否则,将被删元素赋给引用变量e,并将第i+1个元素及其后的所有元素依次往前移动一个位置,返回true。
bool ListDelete(SqList &L,int i,ElemType &e){
if(i<1||i>L.length) //判断i的范围是否有效
return false;
e=L.data[j-1]; //将被删元素的值赋给e
for(int j=i;j<L.length;j++) //将第i个元素之后的元素前移
L.data[j-1]=L.data[j];
L.length--; //线性表长度减1
return true;
}
删除操作的时间复杂度为O(n).
· 按值查找(顺序查找)
在顺序表L中查找第一个元素值为e的元素,并返回其位序
int LocateElem(SqList L,ElemType e){
int i;
for(i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1;
return 0; //退出循环,说明查找失败
}
按值查找操作的时间复杂度为O(n).
5、顺序存储结构的优缺点
-
优点
- 无需为表示元素之间的逻辑关系而另外增加存储空间
- 随机存取元素(快速)
-
缺点
- 插入和删除需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
五、线性表的链式存储
[一] 单链表
1、定义
通过一组任意的存储单元来存储线性表中的数据元素。
对每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。
(结点结构体类型的描述)
typedef struct LNode{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList; //结构体指针
对结构体指针的补充说明:
LinkList->data (*LinkList).data LinkList.data //是等价的
-
头指针:链表中第一个结点的存储位置(头指针为null时表示一个空表)
-
头结点:有时会在单链表的第一个结点前附设一个结点,它的数据域可以不设任何信息,也可以记录表长等信息。
-
==区分:==不管有没有头结点,头指针都指向第一个结点。
==优点:==第一个结点的地址放在头结点的指针域中,因此对于链表第一个结点的处理与其他结点一致,不需特殊考虑。空表和非空表也得到了统一(非空表头结点指针域为空)。
2、基本操作
· 单链表的建立
-
头插法建立单链表
LinkList List_HeadInsert(LinkList &L){ LNode *s; int x; L=(LinkList)malloc(sizeof(LNode)); //创建头结点 L->next=NULL; //初始为空链表 scanf("%d",&x); //输入结点的值 while(x!=9999){ //输入9999表示结束 s=(LNode*)malloc(sizeof(LNode)); //创建新结点 s->data=x; //!!!!存数据域!!!! s->next=L->next; //!!!!新结点的后继指向原结点的后继结点!!!! L->next=s; //!!!!原结点指向新结点!!!! scanf("%d",&x); } return L; //返回头指针 }
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。
时间复杂度:O(n)
-
尾插法建立单链表
必须先建立一个尾指针,指向单链表的尾部。
LinkList List_TailInsert(LinkList &L){ int x; L=(LinkList)malloc(sizeof(LNode)); LNode *s,*r=L; //r为表尾指针,s是新结点 scanf("%d",&x); //输入结点的值 while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; //r指向新的表尾结点 scanf("%d",&x); } r->next=NULL; //尾结点指针置空 return L; //返回头指针 }
时间复杂度:O(n)
· 单链表的查找
- 按序号查找
在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL
LNode *GetElem(LinkList L,int i){
int j=1; //计数,初始为1
LNode *p=L->next; //头结点指针赋给p
if(i==0)
return L; //若i等于0,则返回头结点
if(i<1)
return NULL; //若i无效,则返回NULL
while(p&&j<i){ //从第1个结点开始找,查找第i个结点
p=p->next;
j++;
}
return p; //返回第i个结点的指针,若i大于表长,则返回NULL
}
时间复杂度:O(n)
- 按值查找
从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&&p->data!=e) //从第1个结点开始查找data域为e的结点
p=p->next;
return p; //找到后返回该结点的指针,否则返回NULL
}
· 单链表的插入
将值为x的新结点插入到单链表的第i个位置上
先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点。
-
后插操作
p=GetElem(L,i-1); //查找插入位置的前驱结点 s->next=p->next; p->next=s;
-
前插操作
p=GetElem(L,i-2); //查找插入位置的前驱结点,要前插所以往前一个i-2 s->next=p->next; p->next=s;
或 交换数据域
p=GetElem(L,i-1); //查找插入位置的后继结点 s->next=p->next; p->next=s; temp=p->data; //交换数据域 p->data=s->data; s->data=temp;
· 单链表的删除
将单链表的第i个结点删除。
p=GetElem(L,i-1); //查找删除位置的前驱结点
q=p->next; //令q指向被删除结点
p->next=q->next; //将*q结点从链中“断开”
free(q); //释放结点的存储空间
将单链表中指定结点*p删除
q=p->next; //令q指向*p的后继结点
p->data=p->next->data; //和后继结点交换数据域
p->next=q->next; //
free(q); //(删p的后继结点)
[二] 双链表
1、定义
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序向后遍历。
为了克服上述单链表的缺点,引入了双链表。
双链表结点中有两个指针prior和next,分别指向其前驱结点和后继结点。
(双链表结点类型的描述):
typedef struct DNode{
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode, *DLinklist;
###2、基本操作
双链表的求长度、查找、获得元素位置等操作与单链表相同,但在插入、删除、建立上略有不同。
· 双链表的插入
在双链表中p所指的结点之后插入结点*s
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s; //顺序不能错!!!不唯一,但1、2步必须在4步之前
· 双链表的删除
删除双链表中结点*p的后继结点 *q
p->next=q->next;
q->next->prior=p;
free(q);
[三] 循环链表
1、定义
循环链表与单链表的区别在于:
表中最后一个结点的指针不是NULL,而改为指向头结点。
2、基本操作
插入、删除与单链表基本一致,不同之处在于:
- 若在表尾进行,则执行的操作不同,须让表保持循环的性质。
- 操作时,无需判断是否是表尾(循环无差别)
循环链表的操作 设尾指针比设头指针更方便、时间复杂度更低。
[四] 静态链表
###1、定义
用数组描述的链表
我们让数组的元素用两个数据域构成,data和cur。data存放数据元素,cur我们称之为游标,相当于链表中的next指针,存放该元素的后继数组元素的下标。
静态链表也需要预先分配一块连续的存储空间。
结构类型的描述如下:
#define Maxsize 50 //静态链表的最大长度
typedef struct{
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
静态链表以next == -1作为结束的标志。
###2、基本操作
与动态链表基本一致
总体来说,静态链表没有单链表使用方便,但对于不支持指针的高级语言(如Basic),这是一种非常巧妙的方法。