线性表
定义与特征
线性表(Linear List):是具有相同特性的数据元素的一个有限序列
(
a
1
,
a
2
,
.
.
.
a
i
−
1
,
a
i
,
a
i
+
1
,
.
.
,
a
n
)
(a_1,a_2,...a_{i-1},a_i,a_{i+1},..,a_n)
(a1,a2,...ai−1,ai,ai+1,..,an)
其中
a
1
a_1
a1是线性起点——起始结点 ,
a
n
a_n
an是线性终点——终端结点 ,
a
i
−
1
a_{i-1}
ai−1是
a
i
a_i
ai的直接前驱,
a
i
+
1
a_{i+1}
ai+1是
a
i
a_i
ai直接后继 ,
n为元素总个数,即为表长,当n=0时,为空表,
这里的数据元素
a
i
a_i
ai(1<=i<=n)只是一个抽象的符号,其具体含义在不同情况下不同
线性表的逻辑特征:
非空的线性表有且只有一个起始结点,它没有直接前驱而且仅有一个直接后继;有且仅有一个终端结点,它没有直接后继,有且仅有一个直接前驱;其余内部结点都有且仅有一个直接前驱和一个直接后继。线性表是一种典型的线性结构。
抽象数据类型
A
D
T
L
i
s
t
{
ADT\, List\{
ADTList{
数据对象:
D
=
a
i
∣
a
i
∈
E
l
e
m
s
e
t
,
(
i
=
1
,
2
,
.
.
,
n
,
n
≥
0
)
D={a_i |a_i∈Elemset,(i=1,2,..,n,n≥0)}
D=ai∣ai∈Elemset,(i=1,2,..,n,n≥0)
数据关系:
R
=
<
a
i
−
1
,
a
i
>
∣
a
i
,
a
i
−
1
∈
D
,
(
i
=
1
,
2
,
.
.
,
n
)
R={<a_{i-1},a_i> |a_i,a_{i-1}∈D,(i=1,2,..,n)}
R=<ai−1,ai>∣ai,ai−1∈D,(i=1,2,..,n)
基本操作:
InitList(&L)
操作结果:构造一个空的线性表
DestroyList(&L)
初始条件:线性表L已经存在
操作结果:销毁线性表L
ClearList(&L)
初始条件:线性表L已经存在
操作结果:将线性表L重置为空表
ListEmpty(L)
初始条件:线性表L已经存在
操作结果:若线性表为空表返回TURE,否则返回FALSE
ListLength(L)
初始条件:线性表L已经存在
操作结果:返回线性表L中的数据元素个数,即表长
GetElem(L,i,&e)
初始条件:线性表L存在,
1
≤
i
≤
L
i
s
t
L
e
n
g
t
h
(
L
)
1≤i≤ListLength(L)
1≤i≤ListLength(L)
操作结果:用e返回线性表L中第i个数据元素的值
LocateElem(L,e,compare())
初始条件:线性表L已经存在,compare()是数据元素判定函数
操作结果:返回L中第一个与e满足compare()的数据元素的位序,若这样的数据元素不存在,则返回0
PriorElem(L,cur_e,&pre_e)
初始条件:线性表已经存在
操作结果:若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
≤
L
i
s
t
L
e
n
g
t
h
(
L
)
+
1
1≤i≤ListLength(L)+1
1≤i≤ListLength(L)+1
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一
ListDelete(&L,i,&e)
初始条件:线性表L已经存在,
1
≤
i
≤
L
i
s
t
L
e
n
g
t
h
(
L
)
1≤i≤ListLength(L)
1≤i≤ListLength(L)
操作结果:删除L的第i个位置上的数据元素,并用e返回其值,L的长度减一
ListTraverse(&L,visit())
初始条件:线性表L已经存在
操作结果:依次对线性表中的每个数据元素调用visit()
…
}
A
D
T
L
i
s
t
\}ADT\, List
}ADTList
以上所提及的操作都是逻辑结构上定义的操作,只给出这些操作的功能,是“做什么”的,至于“如何做”等实现细节,只有待确定了存储结构后才考虑
表示和实现
顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像
顺序存储:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构
线性表的顺序存储结构占用一片连续的存储空间,知道某个元素的存储位置就可以计算其他元素的存储位置
特点: 逻辑相邻的元素物理次序相邻;任一元素可随机存取
用一维数组(可以是结构数组)来表示顺序表,因为线性表的表长可变(元素可删除),则另用一个变量表示顺序表的长度属性。注意逻辑位序和物理位序差1
#define MAXSIZE 100
#define LISTINCREMENT 10 //空间分配增量
typedef struct{ //无名结构体
ElemType elem[MAXSIZE];
int length; //当前长度
int listsize; //存储容量
}SqList; //别名
或者
typedef struct{
ElemType *elem;
int length;
}SqList;
SqList L;
L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);//需要加头文件<stdlib.h>,且结尾要用free(L.elem)释放空间
C++用new 类型名T (初值列表)来动态分配存储空间
补充操作算法中用到的预定义常量和类型
// 函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
// Status 是函数的类型
//ElemType 是数据类型
顺序表基本操作和实现
线性表L的初始化的算法
Status InitList_Sq(SqList &L){
L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);
//exit(0)是正常退出,异常退出要给出信息
if(!L.elem) exit(OVERFLOW); //存储分配失败,Process returned -2 (0xFFFFFFFF)
L.length=0; //空表
L.size=MAXSIZE;
return OK;
}
销毁线性表L
void DestroyList_Sq(SqList &L){
if(L.elem) free(L.elem); // 释放存储空间
//free并没有释放指针变量的空间,也没有将指针变量置为空,C语言置空用0/NULL而不是null/Null
L.elem=0; //L.elem=NULL
}
清空线性表L
void ClearList_Sq(SqList &L){
L.length=0; //将L重置为空表
}
求线性表L的长度
int ListLength_Sq(SqList L){
return L.length;
}
判断线性表L是否为空
int ListEmpty_Sq(SqList L){
if(L.length==0) return 1;
else return 0;
}
取第i个数据元素的值,随机存取 O(1)
int GetElem_Sq(SqList L,int i,ElemType &e){
if(i<1||i>L.length) return ERROR;
else e=L.elem[i-1]; //第i-1个单元存储着第i个数据元素
return OK;
}
按值查找,平均时间复杂度O(n)
int LocateList_Sq(SqList L,ElemType e){
for(int i=0;i<L.length;i++)
if(L.elem[i]==e)
return i+1;
return 0;// 查找失败
}
//顺序表的删除
Status ListDelete_Sq(SqList &L,int i,ElemType &e){
if(i<1||i>L.length) return ERROR;
else e=L.elem[i-1];
for(int j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j];
L.length--; //表长减一
return OK;
}
// 插入
Status ListInsert_Sq(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1) return ERROR; //注意和删除操作的区别
if(L.length>=L.MAXSIZE){ //判断表是否满了
ElemType *newbase=(ElemType*)realloc(L.elem,(L.size+LISTINCREMENT)*sizeof(ElemType))
if(!newbase) exit(0);
L.elem=newbase;
L.size+=LISTINCREMENT;
}
for(int j=L.length-1;j>=i-1;j--)
L.elem[j+1]=L.elem[j];
L.elem[i-1]=e;
L.length++ //表长加一
return OK;
}
顺序实现代码
# include<stdio.h>
# include<stdlib.h>
# define SIZE 20
# define SIZEADD 5
# define OVERFLOW -1
typedef int ElemType;
//定义顺序表的元素类型
typedef struct{
ElemType *e;
int length;
int size;
}Sqlist;
void initList(Sqlist *p);
void listInsert(Sqlist *p);
void listDelete(Sqlist *p);
void listTraverse(Sqlist *p);
void listDestroy(Sqlist *p);
int main(){
/*
顺序实现:1.创建空表 2.实现插入操作 3.实现删除操作
*/
Sqlist L;
initList(&L);
listInsert(&L);
listTraverse(&L);
listDelete(&L);
listTraverse(&L);
listDestroy(&L);
return 0;
}
//创建空表
void initList(Sqlist *p){
printf("Initializing:\n");
p->e=(ElemType *)malloc(SIZE*sizeof(ElemType));
if(!p->e)
{
printf("内存分配失败");
exit(OVERFLOW);
}
p->length=0;
p->size=SIZE;
printf("Initialized successfully!\n");
}
//插入
void listInsert(Sqlist *p){
printf("inserting:\n");
//判断表是否满了
if(p->length>=p->size){
ElemType *new=(ElemType*)realloc(p->e,(p->size+SIZEADD)*sizeof(ElemType));
if(!new) exit(OVERFLOW);
p->e=new;
p->size+=SIZEADD;
}
int i;
printf("请输入插入的位置:");
scanf("%d",&i);
while(i<1||i>p->length+1){
printf("Error!");
printf("请输入重新插入的位置:");
scanf("%d",&i);
}
int v;
printf("请输入插入的值: ");
scanf("%d",&v);
for(int j=p->length-1;j>=i-1;j--){
p->e[j+1]=p->e[j];
}
p->e[i-1]=v;
p->length++;
}
//删除
void listDelete(Sqlist *p){
printf("deleting:\n");
int i;
printf("请输入删除的位置:");
scanf("%d",&i);
while(i<1||i>p->length){
printf("Error!");
printf("请输入删除的位置:");
scanf("%d",&i);
}
for(int j=i-1;j< p->length-1;j++){
p->e[j]=p->e[j+1];
}
p->length--;
}
//遍历
void listTraverse(Sqlist *p){
printf("traversing: ");
if(p->e==0) exit(-1); //表不存在
if(p->length==0){ printf("none!\n"); return;}
for(int i=0;i<p->length;i++)
printf("%d ",p->e[i]);
printf("\n");
}
void listDestroy(Sqlist *p){
printf("destroying:\n");
if(p->e==0) exit(-1); //表已经不存在
free(p->e);
p->e=NULL; //C语言指针置空用0/NULL而不是null/Null
printf("Destroyed successfully!\n");
}
链式表示和实现
链式存储结构
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻,链式存储结构又称为非顺序映像或链式映像,用一组物理位置任意的存储单元来存放线性表的数据元素
相关术语
结点:数据元素的存储映像,由两个域组成:数据域(存储元素数值数据),指针域(存储直接后继结点的存储位置)
链表:n个结点由指针链组成一个链表
单链表:结点只有一个指针域的链表称为单链表或线性链表,由头指针唯一确定,所有单链表可以用头指针的名字命名
双链表:结点有两个指针域的链表
循环链表:首尾相接的链表称为循环链表。循环单链表,循环双链表
头指针:是一个指向链表中的第一个结点的指针
首元结点:是指针域中存储第一个数据元素的结点
头结点:是在链表的首元结点之前附设的一个结点
链表的存储结构有两种形式:不带头结点,带头结点
两种形式的讨论:
1.表示空表:
无头结点时,头指针为空时表示空表
有头结点时,头结点的指针域为空时表示空表
2.设置头结点的好处:便于首元结点的处理(首元结点的操作和其他位置一致,无需进行特殊处理);
便于空表和非空表的统一处理(无论链表是否为空,头指针都是指向头结点的非空指针)
单链表的存储结构
单链表的结点的存储结构:
typedef struct Lnode{
ElemType data; //结点的数据域
struct Lnode *next;// 结点的指针域
}Lnode,*LinkList;
//定义链表L,实际上就是定义一个指向结点的指针作为头指针
LinkList L;
单链表基本操作的实现
单链表的初始化(带头结点)
//注意;!!!代码实际的参数应该是LinkList变量,返回值是该变量,不返回该指针变量的值话,主函数里的头指针L的值不会发生改变,则初始化失败
//以下只是类C的表述,与实际代码有一定不同
Status InitList_L(LinkList &L){
LinkList L=new Lnode; //或L=(LinkList)malloc(sizeof(Lnode));
L->next=NULL;
return OK;
}
判断链表是否为空(带头指针)
int ListEmpty_L(LinkList L){
//可以考虑初始化时,将表长存储在头结点的数据域里(数据类型符合的话)
if(L->next==NULL) return 1; //为空
else return 0;//或者是if(L->next) return 0;else return 1;
}
销毁单链表
//从头指针开始,依次释放所有结点
//实际代码应该返回NULL,让L接收,将L指针变量置空
Status DestroyList_L(LinkList &L){
LinkList p;
while(L){
p=L;L=L->next;
free(p);
}
return OK;
}
清空单链表:链表仍存在,但为空链表(头指针和头结点仍然在)
// 从首元结点开始依次释放后续结点,将头结点指针域置为空,得到空表
Status ClearList_L(LinkList &L){
LinkList p=L->next,q;
while(p){
q=p;
p=p->next;
free(q);
}
L->next=NULL;
return OK;
}
求单链表的表长
int ListLength_L(LinkList L){
LinkList p=L->next;int s=0;
while(p){ p=p->next; s++;}
return s;
}
//或者
int ListLength_L(LinkList L){
LinkList p=L;int s=0;
while(p->next){ p=p->next; s++;}
return s;
}
取值——取单链表中的第i个元素的内容
Status GetElem_L(LinkList &L,int i,ElemType &e){
LinkList p=L->next;s=1;
if(i<0||i>ListLength(L)) return ERROR; //或者while(p&&s<i) {p=p->next;s++;} if(!p||s>i) return ERROR;
while(s<i) {p=p->next;s++;}
e=p->data;
return OK;
}
查找——按值查找,根据指定数据获取该数据位置位序
int LocateElem_L(LinkLisit &L,ELmeType e){
LinkList p=L->next;int s=1;
while(p&&p->data!=e){ s++; p=p->next;}
if(p) return s;
else return 0;
}
查找——按值查找,返回地址
Lnode* LocateElem_L(LinkList L,ElemType){
//找到则返回L中值为e的数据元素的地址,查找失败返回NULL
LinkList p=L->next;
while(p&&p->data!=e) p=p->next;
return p;
}
插入——在第i个结点前插入值为e的新结点
Status ListInsert_L(LinkList &L,int i,ElemType e){
LinkList p=L;int j=0;//不用p=L->next;j=1;否则下面循环i=1是会出错
//找到第i-1个值
while(p&&j<i-1){
p=p->next;j++;
}
if(!p||j>i-1) return ERROR; //i大于表长+1或者小于1,插入位置非法
LinkList q=(Linklist)malloc(sizeof(Lnode));
if(!q) exit(OVERFLOW);
q->data=e;q->next=p->next;p->next=q;
return OK;
}
删除第i个结点的操作:
Status ListDelete(LinkLIst &L,int i,ElemType &e){
LinkList p=L;int j=0;
while(p->next&&j<i-1) {p=p->next;j++;} //注意和插入的区别
if(!(p->next)||j>i-1) return ERROR;
e=p->next->data;
LinkList q=p->next;
p->next=q->next;
free(q);
return OK;
}
链式实现代码
#include<stdio.h>
#include<stdlib.h>
#define OVERFLOW -2
typedef int ElemType;
//定义结构体类型
typedef struct Lnode{
ElemType e;
struct Lnode *next;
}Lnode,*LinkList;
LinkList initList();
LinkList listDestroy(LinkList L);
void listInsert(LinkList L);
ElemType listDelete(LinkList L);
void listTraverse(LinkList L);
int main(){
/*
链式实现:1.创建空表 2.实现插入操作 3.实现删除操作
*/
LinkList L=initList();
listInsert(L);
listInsert(L);
listInsert(L);
listTraverse(L);
listDelete(L);
listTraverse(L);
listInsert(L);
listTraverse(L);
listDelete(L);
listTraverse(L);
L=listDestroy(L);
return 0;
}
/*
初始化链表
注意函数的参数和返回值,不是传指针变量而无返回值,这种情况会导致初始化失败
*/
LinkList initList(){
printf("Initializing:\n");
//头指针和头结点
LinkList L=(LinkList)malloc(sizeof(Lnode));
if(!L) exit(OVERFLOW);
L->next=NULL;
L->e=0; //利用头结点的数据e存放有效结点个数
printf("Initialized successfully!\n");
return L;
}
/*
销毁链表
注意:将头指针置空
*/
LinkList listDestroy(LinkList L){
printf("deleting:\n");
if(L==NULL) exit(-1); //表不存在
LinkList p=L,q;
while(p){
q=p;
p=p->next;
free(q);
}
printf("Destroyed successfully!\n");
return NULL; //将头指针置空
}
/*
插入
*/
void listInsert(LinkList L){
printf("inserting:\n");
if (L==NULL) exit(-1); //表不存在
int j=0,i,v;
LinkList p=L;
printf("请输入插入的位置:");
scanf("%d",&i);
while(i<1||i>(p->e+1)){
printf("Error!");
printf("请输入重新插入的位置:");
scanf("%d",&i);
}
printf("请输入插入的值: ");
scanf("%d",&v);
//找到第i-1个位置
while(j<i-1&&p){
p=p->next;
j++;
}
//生成结点
LinkList q=(LinkList)malloc(sizeof(Lnode));
if(!q) exit(OVERFLOW);
q->e=v;
q->next=p->next;
p->next=q;
L->e++;
printf("Inserted successfully!\n");
}
/*
删除
*/
ElemType listDelete(LinkList L){
printf("deleting: \n");
LinkList p=L;
int i,j=0,v;
printf("请输入删除的位置:");
scanf("%d",&i);
while(i<1||i>L->e){
printf("Error!");
printf("请输入删除的位置:");
scanf("%d",&i);
}
//找到第i-1个位置,且其后的元素存在(其实前面已经多索引进行判断这里可以省去)
while(p->next&&j<i-1){
p=p->next;
j++;
}
LinkList q=p->next;
p->next=q->next;
v=q->e;
free(q);
L->e--;
printf("Deleted successfully!\n");
return v;
}
/*
遍历
*/
void listTraverse(LinkList L){
printf("count=%d,traversing: \n",L->e);
if(!L) exit(-1); //表不存在
LinkList p=L->next;
if(!p) {
printf("none!\n");
return;
}
while(p){
printf("%d ",p->e);
p=p->next;
}
printf("\n");
}
头插法
//C++
void CreateList_H(LinkList &L,int n){
L=new Lnode;
L->next=NULL; //先建一个带头结点的单链表
LinkList p;
for(int i=n;i>0;i--)
{
p=new Lnode;
cin>>p->data;
p->next=L->next;
L->next=p;
}
}
尾插法
//C++
void CreateList_R(LinkList &L,int n){
L=new Lnode;
LinkList r=L; //尾指针
LinkList p;
L->next=NULL;
for(int i=1;i<=n;i++){
p=new Lnode;
cin>>p->data;
r->next=p;
p->next=NULL;
r=p; //!!!r要指向新的尾结点
}
}
循环链表
循环链表是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环),这样从表中的任一结点出发都可以找到表中其他结点
注意:由于循环链表没有NULL,故涉及遍历操作时,其终止条件不再像非循环链表那样判断p/p->next是否为空,而是判断它们是否等于头指针
带尾指针的循环链表的合并(尾指针的名字命名链表)
LinkList Connect_L(LinkList Ta,LinkList Tb){
//假设Ta,Tb都是非空单循环链表
LinkList p=Ta->next; p指向Ta的表头结点
Ta->next=Tb->next->next; //将Ta的表尾和Tb的表头相连
delete Tb->next;
Tb->next=p;
return Tb;
}
双向链表(双链表)
单链表查找某结点的后继简单但是查找某结点的前驱结点麻烦,可以用双向链表克服这个缺点
双向链表:在单链表的每个结点中再增加一个指向其直接前驱的指针域prior,这样链表就形成了有两个方向的不同链,故称为双向链表
// 双向链表的结点结构定义
typedef struct DuLnode{
ElemType data;
Struct DuLnode *next,*prior;
}DuLnode,*DuLinkList;
双向链表结构的对称性:
p->prior->next=p=p->next->prior
双向链表的插入
void ListInsert_DuL(DuLinkList &L,int i,ElemType e){
DuLinkList p;
if(!(p=GetElemP_DuL(L,i))) return ERROR;
s=new DuLnode; s->data=e;
s->next=p;
s->prior=p->prior;
p->prior->next=s;
p->prior=s;
return OK;
}
双链表的删除:
void ListDelete_Dul(DuLink &L,int i,ElemType &e){
DuLinkList p;
if(!(p=GetElem(L,i))) return ERROR;
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}
双向循环链表
和单链表的循环链表类似,双向链表也有循环链表
让头结点的前驱指针指向链表的最后一个结点
让最后一个结点的后继指针指向头结点
顺序表和链表的比较
比较项目 | 顺序表 | 链表 |
---|---|---|
空间 | 预先分配,会导致空间闲置或者溢出现象;存储密度为1:不用为表示结点间的逻辑关系而增加额外的存储开销 | 动态分配存储空间,不会出现空间闲置或者溢出现象;存储密度小于1:需要借助指针来体现元素间的逻辑关系 |
时间 | 随机存取元素,按位置访问元素时间复杂度为O(1);插入和删除:平均时间复杂度O(n) | 顺序存取元素,按位访问元素平均时间复杂度为O(n);插入和删除:不需移动元素,在确定插入和删除位置后,时间复杂度为O(1) |
适用情况 | 表长变化不大,且事先确定变化范围;很少进行插入或删除操作,经常按位访问数据元素 | 长度变化大;频繁进行插入或删除操作 |
应用
有序表的合并
用顺序表实现
void MergeList_Sq(SqList L1,SqList L2,SqList &L3){
ElemType *p1,*p2,*p3,*r1,*r2;
p1=L1.elem;
p2=L2.elem;
L3.length=L1.length+L2.length;
L3.elem=new ElemType[L3.length+1];
p3=L3.elem;
r1=L1.elem+L1.length-1;
r2=L2.elem+L2.length-1;
while(p1<=r1&&p2<=r2){
if(*p1<=*p2) *p3++=*p1++;
else *p3++=*p2++;
}
while(p1<=r1) *p3++=*p1++;
while(p2<r2) *p3++=*p2++;
}
用链表实现
void MergeList_L(LinkList L1,LinkList L2,LinkList &L3)
{
L3=L1;
LinkList p1=L1->next,p2=L2->next,p3=L3;
while(p1&&p2){
if(p1->data<=p2->data)
{p3->next=p1;p3=p1; p1=p1->next;}
else
{p3->next=p2;p3=p2;p2=p2->next;}
p3->next=p1?p1:p2;
delete L2;
}
顺序表的有序合并和归并排序的思想差不多
☞归并排序
题库训练
单链表的建立
简单的链表操作1
简单的链表操作2
链表的基本操作—查找,删除,插入,取值
双向链表的应用
双向循环链表的基本操作