了解线性表
什么是线性表
线性表是具有相同数据类型的数据元素(数据元素由数据项组成)的有限序列
线性表是一种逻辑结构,表示元素间一对一的相邻关系
线性表具有顺序存储和链式存储两种存储方式,因此有顺序表及链表两种存储结构
线性表的特点:
- 表中元素个数有限
- 表中元素具有逻辑上的顺序性,元素具有先后次序
- 表中元素都是数据元素,每个元素都是单个元素
- 表中元素的数据类型都相同,即每个元素占有相同大小的存储空间
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系而不考虑元素究竟表示什么内容
线性表的基本操作
- 初始化:InitList(&L)
- 求表长:Length(L)
- 按值查找:LocateElem(L,e)
- 按位查找:GetElem(L,i)
- 插入操作:ListInsert(&L,i,e)
- 删除操作:ListDelete(&L,i,&e)
- 输出操作:PrintList(L)
- 判空操作:Empty(L)
- 销毁操作:DestroyList(&L)
线性表的顺序表示
什么是顺序表
线性表的顺序存储即顺序表,利用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻
顺序表的特点
- 随机访问-通过首地址和元素序号可在时间O(1)内找到指定的元素
- 存储密度高(每个结点只存储数据元素)
- 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除需要移动大量元素
顺序表的存储类型
假设线性表的元素类型位ElemType,则线性表的顺序存储类型描述为:
#define MAXSIZE 50 //定义线性表的最大长度
typedef struct{
ElemType data[MAXSIZE]; //顺序表的元素
int length; //顺序表的当前长度
}SQList; //顺序表的类型定义
优化:动态分配
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int length; //数组的当前个数
int MaxSize; //数组的最大容量
}SeqList; //动态分配数组顺序表的类型定义
C的初始化动态分配语句
L.data=(ElemType *)malloc(sizeof(ElemType)*InitSize);
C++的初始化动态分配语句
L.data=new ElemType[InitSize];
顺序表基本操作的实现
-
初始化InitList(&L)
void InitList(SQList &L){ L.length=0; }
-
求表长:Length(L)
int Length(SQList L){ return L.length; }
-
按值查找:LocateElem(L,e)
int LocateElem(SQList L,ElemType e){ for(int i=0;i<L.length;i++){ if(L.data[i]==e){ return i+1; } } printf("输入值有误!"); return -1; }
-
按位查找:GetElem(L,i)
ElemType GetElem(SQList L,int i){ return L.data[i]; }
-
插入操作:ListInsert(&L,i,e)
bool ListInsert(SQList &L,int i,ElemType e){ if(i<1||i>L.length+1){ return false; } if(L.length>=MAXSIZE){ return false; } for(int j=L.length;j>=i;j--){ L.data[j]=L.data[j-1]; } L.data[i-1]=e; L.length++; return true; }
注意数组下标从0开始,而顺序表的下标从1开始
-
删除操作:ListDelete(&L,i,&e)
bool ListDelete(SQList &L,int i,ElemType &e){ if(i<1||i>L.length){ return false; } e=L.data[i-1]; for(i;i<L.length;i++){ data[i-1]=data[i]; } L.length--; return true; }
线性表的链式表示
什么是单链表
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
为了建立数据元素之间的线性关系,对每个链表结点,除了存放自身的信息外,还需要存放一个指向其后继的指针。
单链表中的结点类型的描述如下:
typedef struct LNode{
ElemType data;
struct LNode *next; //指针指向下一个节点
}LNode,*LinkList;
单链表的特点
- 不需要大量连续存储单元
- 附加指针域,存在浪费存储空间的缺点
- **非随机存取的存储结构-**即不能直接找到表中某个特定的结点,在查找某个特定的结点时,需要从表头开始遍历,依次查找
通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。
为了操作方便,在单链表的第一个结点之前附加一个结点,称之为头结点,头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点
区分头结点和头指针:
不管带不带头结点,头指针始终指向链表的第一个结点,结点内通常不存储信息。
引入头结点后可以带来两个优点:
- 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中的头结点的指针域为空),因此空表和非空表的处理也得到了统一
单链表的基本操作
1.利用头插法建立单链表
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点,如下图所示:
LinkList List_HeadInsert(LinkList &L){
LNode *s;int x;
L=(LNode *)malloc(sizeof(LNode));
L->next=NULL;
scanf("%d",&x);
while(x!=9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。
每个结点的插入时间为O(1),设单链表长为n,则总时间的复杂度为O(n)
2.用尾插法建立单链表
头插法建立单链表虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。
若希望两者次序一致,则可以尝试尾插法,将新节点插入到当前链表的表尾,因此需要增加一个尾指针r,使其始终指向当前链表的尾结点
尾插法建立单链表的算法如下:
LinkList List_TailInsert(LinkList &L){
int x;
L=(LNode *)malloc(sizeof(LNode));
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //表尾结点置空
return L;
}
3.按序号查找结点
由于链表结构存储的特点,因此不能实现随机存取
LNode *GetELem(LinkList ,int i){
int j=1;
LNode *p=L->next;
if(i==0) return L;
if(i<1) return NULL;
while(p&&j<i){
p=p->next;
j++;
}
return p;
}
时间复杂度为O(n)
4.按值查找表结点
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&p->data!=e){
p=p->next;
}
return p;
}
时间复杂度为O(n)
5.插入结点操作
1.后插操作
- 先检查插入位置的合法性
- 找到待插入位置的前驱结点,即第i-1个结点
- 插入新节点,先令新节点 *s的指针域指向 *p的的后继节点,在令结点 *p的指针域指向新插入的结点 *s
p=GetElem(L,i-1);
s->next=p->next;
p->next=s;
时间开销主要在于查找第i-1个元素,时间复杂度为O(n)
若在给定结点后面插入新结点,则时间复杂度仅为O(1)
2.前插操作
前插操作是指在某结点的前面插入一个新的结点,后插操作的定义与之刚好相反。在单链表插入操作中,通常采用后插操作
以上面的算法为例
- 首先调用GetElem()找到第i-1个结点,即插入结点的前驱结点
- 对插入结点的前驱结点执行后插操作
此外,还可以采用另一种方式转化为后插操作(若给定插入结点*p,则时间复杂度为O(1))
- 先调用后插操作将 *s插入到 *p后面
- 然后将p->data与s->data交换
s->next=p->next;
p->next=s;
temp=s->data;
s->data=p->data;
p->data=temp;
6.删除结点操作
- 同样,先检查删除位置的合法性
- 查找表中第i-1个结点,即被删除结点的前驱结点
- 将要删除的结点删除
p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);
和插入操作一样,时间开销主要在于查找第i-1个元素,时间复杂度为O(n)
注:若给出要删除的结点*p
可采用删除结点*p的后继结点操作实现,实际上就是将其后继结点的值赋予自身,然后删除后继结点,则时间复杂度为O(1)
q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
7.求表长操作
求表长操作就是计算单链表中数据结点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中每个结点,因此需要设置一个计数器变量,每访问一个结点,计数器加1,直到访问到空姐点为止,算法的时间复杂度为O(n)
注:单链表的长度不包括头结点,因此不带头结点和带头结点的单链表求表长操作会略有不同,对不带头结点的单链表,当表为空时,需要单独处理
什么是双链表
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历,要访问某个结点地前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点地时间复杂度为O(1),访问前驱结点地时间复杂度为O(n)
为了克服单链表地上述缺点,引入双链表,双链表结点中有两个指针prior和next,分别指向前驱结点和后继结点,如下图所示:
双链表中结点类型地描述如下:
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
双链表的特点
双链表在插入和删除操作的实现上,与单链表有较大的不同,这是因为“链”变化时也需要对prior指针做出修改,其关键是保证在修改的过程中不断链。以外,双链表可以很方便的找到其前驱结点,因此,插入、删除操作的时间复杂度仅为O(1)
双链表的基本操作
1.双链表的插入操作
在双链表中p所指的结点之后插入结点*s:
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
注:为了保证*p的后继结点的指针不能丢掉,因此第一步与第二部必须在第四步之前
2.双链表的删除操作
删除双链表中结点 *p的后继结点 *q:
p->next=q->next;
q->next->prior=p;
free(q);
什么是循环链表
1.循环单链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环
-
循环单链表的判空操作
在循环单链表中,表尾的结点*r的next指向L,故表中没有指针域为NULL的结点,因此循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针
-
循环单链表的插入、删除操作
循环单链表的插入、删除操作与单链表的几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持着循环的性质
因为循环单链表是一个”环“,因此在任何一个位置上的插入和删除操作都是等价的,无需判断是否是表尾
在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意一个结点开始遍历整个链表。
有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而设尾指针,从而使得操作效率更高。其原因是,若设的是头指针,对表尾进行操作需要O(n)的时间复杂度,而若设的是尾指针r,r->next即为头指针,对表头与表尾进行操作都只需要O(1)的时间复杂度
2.循环双链表
循环单链表的定义不难推出循环双链表,不同的是在循环双链表中,头结点的prior指针还要指向表尾结点
在循环双链表L中,某结点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior域和next域都等于L
什么是静态链表
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间
静态链表结构类型描述如下:
#define MaxSize 50
typedef struct{
ElemType data;
int next;
}SLinkList[MaxSize];
静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言中,这是一种非常巧妙地设计方法。
顺序表与链表的比较
1.存取(读写)方式
顺序表可以顺序存取,也可以随机存取,
链表只能从表头顺序存取元素
2.逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。
采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的
3.查找、插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n)
对于按序号查找,顺序表支持随机访问,时间复杂度为O(1),而链表的平均时间复杂度为O(n),顺序表的插入,删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需要修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大