使用一组任意的存储单元存储线性表的数据元素(这些存储单元可以是连续也可以是不连续的)。
常见的链式表:单链表、静态链表、循环链表、双向链表。
我们修改一下上一篇文章的例子:假如现在是新生入校,按照新生的先来后到编号1-6,先到的同学可以随意选择床铺,
但是需要记住下一位同学的床铺号(1舍友记住2舍友床铺号,2舍友记住3舍友床铺号,依次类推)。
此时无形的就把同学构成了一条链表。
链表的存储方式也于此类似,下面讨论一下链表的特点:
(1)数据元素的逻辑顺序和物理顺序不一定相同。
(2)在查找数据元素时,必须从头指针开始依次查找,表尾的指针域指向NULL。
(3)在特定的数据元素之后插入或删除元素,不需要移动数据元素,因此时间复杂度为 O(1)。
(4) 存储空间不连续,数据元素之间使用指针相连,每个数据元素只能访问周围的一个元素。
(5)长度不固定,可以任意增删。
在来看下链表中的结点:结点是数据元素信息和指示存储位置信息这两部分的组成,这两部分信息也称作数据元素的存储映像。
结点包括了两个域:存储数据元素data是数据域,用来存储位置信息next是指针域。
注意:为了增强程序可读性,通常在链表第一个结点之前我们会加一个头结点,
头结点的数据域可以不存储任何信息,也可以存储线性表的长度等附加信息。
另外,有头结点的好处是便于在第一个结点做插入和删除时不需要做特殊的处理。
下面来看单链表
//----线性表的单链表存储结构(课本28页)---
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
基本操作包括了教材19页的12个操作,其中的操作也实现了教材中的2.8,2.9,2.10算法。
C语言代码:
在给出基本操作代码:
//----单链表的存储结构(课本28页)---
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//-----线性表的链式 之单链表的基本操作(12个) */
//1,
Status InitList(LinkList *L)
{ /* 操作结果:构造一个空的线性表L(默认带头结点) */
*L =(LinkList)malloc(sizeof(LNode)); //产生头结点,并初始化给头指针。
if(!*L) //分配空间失败
exit(OVERFLOW);
(*L)->next =NULL;//头结点的指针域为空。
return OK;
}
//2
Status DestroyList(LinkList *L)
{ /* 初始条件:线性表L已存在。操作结果:销毁线性表L */
LinkList p;
while(*L){
p=(*L)->next;
free(*L);//从头结点释放资源
*L=p;
}
return OK;
}
//3,
Status ClearList(LinkList L) /* 注意L不改变,不使用&L */
{ /* 初始条件:线性表L已存在。操作结果:将L重置为空表 */
LinkList p,q;
p=L->next; // p指向首元结点
while(p){ //释放第一个结点及以后的结点资源
q =p->next;
free(p);
p =q;
}
L->next =NULL; //头指针指向的头结点next域为空
return OK;
}
//4,
Status ListEmpty(LinkList L)
{ /* 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
if(L->next) // 不为空
return FALSE;
else
return TRUE;
}
//5,
int ListLength(LinkList L)
{ /* 初始条件:线性表L已存在。操作结果:返回L中数据元素个数 */
int i=0; //计数器
LinkList p=L->next; // 使p指向第一个结点
while(p){ //遍历链表
i++;
p=p->next; //使p指向下一个结点
}
return i;
}
//6,书中算法2.8(29页)
Status GetElem(LinkList L,int i,ElemType *e)
{ /* L为带头结点的单链表的头指针。当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR */
int j=1; // 计数器
LinkList p=L->next; // 使p指向第一个结点
while(p && jnext;
j++;
}
//判断一下
if(!p || j>i) // 第i个元素不存在(前者判断i的值大于表长,后者判断i的值小于1)
return ERROR;
*e=p->data; // 取第i个元素
return OK;
}
//7,
int LocateElem(LinkList L,ElemType e,Status(*compare)(ElemType,ElemType))
{ /* 初始条件: 线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0) */
/* 操作结果: 返回L中第1个与e满足关系compare()的数据元素的位序。 */
/* 若这样的数据元素不存在,则返回值为0 */
int i=0;
LinkList p=L->next; // 使p指向第一个结点
while(p)
{
i++;
if(compare(p->data,e)) // 满足关系的数据元素,返回位序
return i;
p=p->next;
}
return 0;
}
//8,
Status PriorElem(LinkList L,ElemType cur_e,ElemType *pre_e)
{ // 初始条件: 线性表L已存在
// 操作结果: 若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,返回OK;否则操作失败,pre_e无定义,返回OVERFLOW。
LinkList p =L->next; //p指向第一个结点,当前结点
LinkList q;
while(p->next){ //从第二个结点遍历判断,保证了当前结点是别的结点前驱【在顺序表操作中 j =2】
q =p->next; //q指向当前p的后继【第一个结点没有前驱,不用判断了,从第二个结点】
if(q->data ==cur_e){ //当前结点指针p的后继q的data域 与cur_e 相同
*pre_e =p->data; //p的data域必定是cur_e的前驱
return OK;
}
p =q; //向后移动p指针
}
return ERROR;
}
//9,
Status NextElem(LinkList L,ElemType cur_e,ElemType *next_e)
{ // 初始条件:线性表L已存在 */
// 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,返回OK;否则操作失败,next_e无定义,返回OVERFLOW
LinkList p=L->next;
while(p->next) //从第二个结点开始遍历,保证了当前结点有后继的【也排除了最后一个结点没有后继】
{
if(p->data ==cur_e) //与上一个操作不同的是,要判断第一个结点(当前结点)。
{
*next_e=p->next->data; //while已经保证了当前结点有后继
return OK;
}
p=p->next; //向后移动
}
return ERROR;
}
//10,书中算法2.9(29页)
/*
自然语言描述:
1,从头结点遍历寻找第i-1个结点位置
2,判断i值的合法性,即不能小于1,不能大于表长加1
3,生成一个由s指向的结点空间,在数据域中存储e
4,根据插入操作的逻辑定义,修改相关结点的指针域
*/
Status ListInsert(LinkList L,int i,ElemType e) /* L不改变 */
{ // 在带头结点的单链线性表L中第i个位置之前插入元素e
int j =0; //带头结点 从0开始【有头结点的好处:避免了在表头做操作时的复杂处理】
LinkList p=L, s;
//第一步 从头结点开始寻找第i-1个位置的结点
while(p && jnext; // 此时p指向第i-1个位置的结点
j++;
}
if(j>i-1 || !p) // 判断i不能小于1 或者 不能超过表长加1(此时表尾做i-1个位置结点)
return ERROR;
s=(LinkList)malloc(sizeof(struct LNode)); //生成新结点
//关键代码【此时的p是i-1个位置的结点】
s->data=e;
s->next=p->next; //把i-1个结点的指针域next 赋值 给新结点的指针域next
p->next=s; //把s赋值给 第i-1个结点指针域next
return OK;
}
//11,书中算法2.10
/*
自然语言描述:
1,从第一个结点开始遍历需找第i-1个结点,赋值给p,也就是待删除结点的前驱
2,判断i的合法性
3,根据删除的逻辑定义,修改相关结点指针域
4,释放资源
*/
Status ListDelete(LinkList L,int i,ElemType *e) /* 不改变L */
{ // 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
int j=0;
LinkList p=L,q;
// 从第一个结点开始遍历寻找第i-1个结点
while(p->next && jnext还保证了表尾 不能做 i-1位置的结点
{
p=p->next; //但此时p的指向是i-1的结点,也就是i结点的前驱
j++;
}
if(!p->next||j>i) // i值不合法(p是待删除结点的前驱,要保证p不是表尾哦)
return ERROR;
//关键代码
q=p->next; //q指向第i个结点(待删除)
p->next=q->next; //把i个结点的指针域next 赋值 给i-1个结点的指针域next
*e=q->data;
free(q); //释放q
return OK;
}
Status ListTraverse(LinkList L,void(*visit)(ElemType)) //注意visit的参数与顺序表的不同
{ /* 初始条件:线性表L已存在 */
/* 操作结果:依次对L的每个数据元素调用函数visit()。一旦visit()失败,则操作失败 */
LinkList p=L->next;
while(p)
{
(*visit)(p->data);
p=p->next;
}
printf("\n");
return OK;
}
然后给出测试上述操作的C代码:
测试:
#include"ch2.h"
typedef int ElemType;
#include"Single_linked_list.c"
Status equal(ElemType c1,ElemType c2)
{
if(c1==c2)
return TRUE;
else
return FALSE;
}
void visit(ElemType c) /* 与顺序表不同 */
{
printf("%d ",c);
}
void main()
{
LinkList L;
Status i;
int j,j1;
ElemType e,e1;
//1, 测试第一个基本操作(构造单链表L)
printf("1,测试第一个基本操作(构造单链表表L)\n");
i=InitList(&L);
if(i==OK)
printf(" 成功构造单链表\n");
printf("\n");
//2, 测试第十个基本操作(插入数据元素)
printf("2,测试第十个基本操作(插入数据元素)\n");
for(j=1;j<=5;j++){
ListInsert(L,1,j); //与顺序表不同参数
}
printf(" 在L的表头依次插入1~5后:L=");
ListTraverse(L,visit);
printf("\n");
//3, 测试第三、四个操作
printf("3, 测试第三、四个操作\n");
i=ListEmpty(L);
printf(" L是否空:i=%d(1:是 0:否)\n",i);
i=ClearList(L);
printf(" 清空L后:L="); ListTraverse(L,visit);
i=ListEmpty(L);
printf(" L是否空:i=%d(1:是 0:否)\n",i);
//再次插入数据1-11
for(j=1;j<=11;j++){//注意如果j=0时,将会在插入操作中触发第一步i值不合法。
ListInsert(L,j,j);
}
printf(" 有插入了11条数据\n");
ListTraverse(L,visit);
printf("\n");
//4,测试第五个操作
printf("4,测试第五个操作(L的数据元素个数)\n");
printf(" L表的长度=%d\n",ListLength(L));
printf("\n");
//5, 测试第六个操作
printf("5, 测试第六个操作(取得第5个位置的数据元素)\n");
i=GetElem(L,5,&e);
printf(" 第5个位置的数据元素:%d\n",e);
printf("\n");
//6,测试第七个操作
printf("6,测试第七个操作(LocateElem)\n");
j=LocateElem(L,11,equal);
if(i>0)
printf(" 在L表中存在于11相同的元素,位序是:%d\n",j);
else
printf(" 在L表中不存在与11相同的数据元素\n");
printf("\n");
//7,测试第八、九个操作
printf("7,测试第八、九个操作\n");
for(j=1;j<=2;j++){ // 测试头两个数据
GetElem(L,j,&e1); // 把第j个数据赋给e1
i=PriorElem(L,e1,&e); // 求e1数据元素的前驱
if(i==ERROR)
printf(" 元素%d无前驱\n",e1);
else
printf(" 元素< %d的前驱为:%d >\n",e1,e);
}
for(j=ListLength(L)-1;j<=ListLength(L);j++){ // 最后两个数据
GetElem(L,j,&e1); // 把第j个数据赋给e1
i=NextElem(L,e1,&e); // 求e1的后继
if(i==ERROR)
printf(" 元素%d无后继\n",e1);
else
printf(" 元素< %d的后继为:%d >\n",e1,e);
}
printf("\n");
//8,测试第十个操作(删除操作)
printf("8,测试第十一个操作(删除操作)\n");
j1=ListLength(L); //注意要把表L的长度先保存到一个变量中,因为在删除时,表的长度会自减一的。
for(j=j1+1;j>=j1;j--){
i=ListDelete(L,j,&e); // 删除第j个数据
if(i==ERROR)
printf(" 删除第%d个数据失败\n",j);
else
printf(" 删除的元素值为:%d\n",e);
}
printf("\n");
//9,测试第十二个操作
printf("9,测试第十二个操作\n");
ListTraverse(L,visit);
printf("\n");
//10,测试第二个操作
printf("10,测试第二个操作\n");
DestroyList(&L);
printf(" 销毁L=%d\n",L);
}
总结:
上述操作中需要重点掌握的算法是,插入、删除、获取数据元素等等。
比较重点的基本操作在代码中都有自然语言算法描述和释。如需详细了解,请见教材。
下面,我们简单分析一下比较重要操作的问题:
书中算法2.8(29页)
基本操作是比较i和j并后移指针,while循环体中的语句频度和被查元素在表中的位置有关,最多是表为n,所以
时间复杂度为O(n).
书中算法2.9插入操作和书中算法2.10删除操作:
插入和删除的时间复杂度也都是O(n),因为在第i个结点之前插入或删除一个新的结点,都必须找到第i-1个结点。
好,举个考试题:对于一个具有n个结点的单链表,在已知的结点p后插入一个新结点的时间复杂度为_.在给定值
为x的结点后插入一个新结点的时间复杂度为_。第一个空填O(1) 第二个填 O(n)。既然给定了结点就不用在寻找结点
了,这也正体现了单链表的特点,插入删除时不用移动数据元素。
如果死记算法的代码(对于个别自己经常模糊的除外),很容易忘。
下面以插入算法为例,分析一下我的理解。
(1)首先在做题的时候,应该准备好纸和笔。
(2)比如我做的是线性表,可以画一条横坐标,其实线性表就是一条线嘛。
(3)然后你会想:我要做的是插入操作,要先找到i的前一个位置的结点吧,即找i-1个位置的结点。
(4)那么,现在又会先想:我在那个范围找到i-1位置的结点?现在就要想插入操作的逻辑定义了,对与单链表可以在头结点之后插入还可以在表尾之后插入。
那么就在坐标上把0-5画出闭区间(假设表的长度为5,0表示头结点),那么0-5就是需找i-1位置结点的范围。
(5)我们知道了范围,就有了while循环的两个依据条件,同时也知道了不合法的i值范围。
但是在实际问题中i的值是不确定的,要想while循环执行正确,我们就要结合不合法的i值范围,
所以我们假定j=0:
1)当i<=0的时候i值不合法:第一个条件j表长+1的时候i值不合法:这里我们要结合链表在存取元素必须从头指针进行,最后一个结点的指针为NULL的特点,
既然是和表长有关系,我们可以使用单链表的依次访问的特点,来进行判断i的值是否超过表尾+1,即移动p判断
3)然后这两个条件一起就保证可以定位合法的i-1个结点,也就是while循环的条件了。
(6)判断不合法的i。
(7)根据插入操作的逻辑定义,修改指针域。
3,最后在说下不带头结点时,插入的特殊操作:
因为没有头结点,所以在表头插入的时候,需要特殊处理一下,
if(i=1){
s->next=L;
L=s;
}else{
}
书中只给出了算法2.11(头插法)。在给出尾插法代码。
#include"ch2.h"
typedef int ElemType;
//头插法,逆序
void CreateList_tou(LinkList *L,int n){
int i;
LinkList p=NULL;
*L =(LinkList)malloc(sizeof(LNode)); //创建头结点
(*L)->next=NULL;
(*L)->data =n; //头结点记住表长
printf("请输入%d个数据\n",n);
for(i=n;i>0;--i){
p =(LinkList)malloc(sizeof(LNode)); //生成新结点
scanf("%d",&(p->data)); //输入元素值
p->next = (*L)->next; //把头结点的指针赋值给新结点的next域
(*L)->next =p; //新结点总在头结点后插入
}
}
//尾插法,正序
void CreateList_wei(LinkList *L,int n){
int i;
LinkList p,q=NULL;
*L =(LinkList)malloc(sizeof(LNode)); //创建头结点
(*L)->next=NULL;
(*L)->data =n;
q =(*L); //指向头指针
printf("请输入%d个数据\n",n);
for(i=0;imalloc(sizeof(LNode)); //生成新结点
scanf("%d",&(p->data)); //输入元素值
q->next =p;
q =q->next; //移动q指针,为下一个结点插入做准备
}
p->next =NULL;
}
void visit(ElemType c)
{
printf("%d ",c);
}
void main()
{
int n=5;
LinkList La,Lb;
printf("按非递增顺序, ");
CreateList_tou(&La,n); // 逆位序输入n个元素的值
printf("La="); // 输出链表La的内容
ListTraverse(La,visit);
printf("按非递减顺序, ");
CreateList_wei(&Lb,n); / 正位序输入n个元素的值
printf("Lb="); // 输出链表Lb的内容
ListTraverse(Lb,visit);
}
总结:
创建用户输出数据的单链表方法常用有两种:尾插法和头插法。
尾插法是指:每次是将新结点插入到链表的尾部,
采用这种方法建立单链表,读入数据元素的顺序与生成链表元素的顺序一致。
头插法是指:每次是将新结点插入到链表的头部(头结点的后面),
采用这种方法建立的单链表,读入数据元素的顺序与生成链表元素的顺序相反。
教材中的算法2.12也是合并表的操作,请见教材31页。
对于此算法,我们给出两种不同的方法,以及对2.12算法的改进
(去除重复的数据元素)。
1,C代码:
#include"ch2.h"
typedef int ElemType;
#include"Single_linked_list.c"
/*
自然语言算法描述:
1,设立3个指针pa,pb,pc,其中pa,pb指向各自的首元结点,pc、Lc指向La的头结点
2,利用while循环单链表,判断取的表La和Lb相同位序的结点数据域(pa->data和pb->data),比较之在把结点链接到Lc后,
使之产生前后位序。
3,判断表La和Lb中是否还有剩余的结点,在插入表Lc的尾部。
*/
/*
书中算法2.12 (31页)
*/
void MergeList(LinkList La,LinkList *Lb,LinkList *Lc){//参数La是不变的是为了Lc使用La的空间,Lb的空间是需要释放。
LinkList pa,pb,pc =NULL;
pa =La->next;
pb =(*Lb)->next;
(*Lc) =pc =La; //表La的头结点作为表Lc的头结点,是为了空间的复用。
while(pa && pb){ //pc现在指向的是La的头结点
if(pa->data <= pb->data){
pc->next =pa; //若pa较小,就插在pc后
pc =pa; //移动pc
pa =pa->next; //关键 移动pa,此时的pa记住了La表下一个待比较的指针。
}else{
pc->next =pb; //若pb较大,就插在pc后,现在La表会有变化,但是pa已经记住了需要下一个要比较的指针,
//前面的结点就无需关心了。
pc =pb;
pb =pb->next; //移动pb,记住Lb表下一个待比较的指针。
}
}
//插入剩余段
pc->next =pa? pa:pb;
free(*Lb);
}
/*
算法2.12的另一种实现,使用原来的La表存储组装后的链表
*/
void MergeList_2(LinkList *La,LinkList *Lb){
LinkList pa,pb =NULL; //La和Lb的第一个结点指针
LinkList a,b =NULL; //
pa =(*La)->next;
pb =(*Lb)->next;
while(pa && pb){ //pc现在指向的是头结点
if(pa->data <= pb->data){
a =pa;
pa =pa->next;
}else{
b =pb;
pb =pb->next;
//组合a , b
b->next =pa; //Lb表有结点符合条件时,仍和la表建立关系
a->next =b; //把Lb表符合条件的结点,串在a上
a =b; //移动指针a
//[a和b总在pa,pb的后面]
}
}
if(pb){
a->next =pb;
}
free(*Lb);
}
/*
算法2.12的改进 即去掉重复的数据元素
*/
int com(ElemType c1,ElemType c2){
int i;
if(c1 == c2)
i =0;
else if(c1c2)
i =-1;
return i;
}
void MergeList_3(LinkList La,LinkList *Lb,LinkList *Lc){
LinkList pa,pb,pc =NULL;
pa =La->next;
pb =(*Lb)->next;
(*Lc) =pc =La;
while(pa && pb){
switch(com(pa->data,pb->data)){
case 0:
pb =pb->next; //移动Lb表
case 1:
pc->next =pa;
pc =pa;
pa =pa->next;
break;
case -1:
pc->next =pb;
pc =pb;
pb =pb->next;
}
}
//插入剩余段
pc->next =pa? pa:pb;
free(*Lb);
}
//-------------------------测试---------------------------------
void print(ElemType c)
{
printf("%d ",c);
}
//初始化方法--代码复用
void Init(LinkList *La,LinkList *Lb){
int i;
//初始化La和Lb
printf(" ----初始化两个表La、Lb,并且是非递减的有序序列\n");
if(!InitList(&(*La)))
printf(" 表La初始化失败");
if(!InitList(&(*Lb)))
printf(" 表Lb初始化失败");
for(i=1;i<=3;i++){
ListInsert((*La),i,i);
}
for(i=1;i<=11;i++){
ListInsert((*Lb),i,i);//注意i要从1开始,因为表此时的长度是0
}
printf(" 表La中的数据元素:");
ListTraverse((*La),print);
printf(" 表Lb中的数据元素:");
ListTraverse((*Lb),print);
}
//测试
void main(){
LinkList La,Lb,Lc=NULL;
//调用MergeList方法,合并两个表
printf("1,--------------调用MergeList方法(算法2.12)-------\n");
//关键代码
Init(&La,&Lb);//初始化
MergeList(La,&Lb,&Lc);
printf(" 合并两个表到Lc表中:\n");
ListTraverse(Lc,print);
printf("\n");
//调用MergeList_2方法,合并两个表
printf("2,---------------调用MergeList_2方法(算法2.12另一种实现)----------\n");
//关键代码
DestroyList(&Lc);//初始化
Init(&La,&Lb);
MergeList_2(&La,&Lb);
printf(" La表中的数据元素:\n");
ListTraverse(La,print);
printf("\n");
//调用MergeList_3方法,合并两个表
printf("3,---------------调用MergeList_3方法(算法2.7改进)------------\n");
//关键代码
DestroyList(&La); //销毁表La
Init(&La,&Lb);
MergeList_3(La,&Lb,&Lc);
printf(" Lc表中的数据元素:\n");
ListTraverse(Lc,print);
printf("\n");
}
3,总结:
从时间复杂度来分析:
单链表的合并操作和顺序表的合并操作时间复杂度相同。但是空间复杂度不同,前者不需要另建新表的结点空间。
在归并链表时,不需要另建新表结点空间,只需把原来两个链表结点之间的关系解除(从第一个结点开始依次解除关系),在重新组成一个链表。
对于算法2.12:/*通俗点讲*/
用pc把两个表中的已经解除关系(符合条件的)的结点串起来。假设表La为 1 3 5 7,Lb为2 4 6,
那么就先按123456串起来,然后在把剩余段串到表尾。有因pc和Lc都指向La的头指针,
pa、pb指向的是第一个结点的指针,谁符合条件pc就跟在谁屁股后面,
pc就在俩表之间跳来跳去【无论在那一个表pc都不会再pa或pb的前面,以表尾为正方向】。
注意:书中的算法也可以传两个参数,但是La必须要是可变的(&La)。这样在测试的时候输出La就行了。
即使传的是三个参数,在测试的时候也可以输出La,Lc和La是共享的。
对于算法2.12另一中实现的方式:
与书中算法的主要不同:
是在组合两个表结点之间的关系时,上一个算法是:一条线pc把两个表中的结点串了起来,
此算法是:先把Lb表中符合条件结点的next域和La建立关系,在把该结点粘在a的后面,这样使用a就把两个表串起来了,最后输出La就行了。
在单链表上查找结点时,不具有随机存取,必须从头结点开始。