(不说了,又是呕心沥血的一篇o(╥﹏╥)o,全篇就俩字:干货^_^
)
- 链表的概念
链表是计算机中一种常用和重要的数据结构,它采用动态存储的方式,依据需要动态开辟存储空间存储批量数据。当第一次需要内存时,可以由malloc()函数或calloc()函数分配一块连续的内存,第二次需要时,再分配一块连续的内存空间,但是两次分配的内存块之间并不是连续的,以此类推。由于存储批量数据,如何能有效地处理动态开辟的这些区域,简单的方式是使依次开辟的空间采用一种链式的方式“组织”在一起。这种组织方式通常通过临近的两块区域相互或单向记录地址来实现的,这样就构成了一个类似“链条”的数据形式,被称为链表。
链表的作用就是提高内存的存储效率上,通过占用零散存储空间的方式来存储批量数据
- 结点
链表有一个头指针变量,通常以head表示,它存放一个地址,该地址指向一个区域(动态开辟出来的空间)的首地址。链表中的每一个这样的区域被称为一个“结点”。
链表的种类
每个结点包括两个部分:一部分是用于存放用户实际需要的数据(数据域),另一块用于记录后面或前面结点的地址(地址域),它们都是指针变量。按照存储地址的方式不同,分为:
单链表:前面的结点在地址域中存放后面结点的首地址;
双链表:结点的地址域需要两个指针变量,分别存放前后两个结点的首地址。不要求掌握双链表。
单链表需要一个头结点来代表一个链表。根据头结点数据域中是否存储数据,分为:
带头结点:头结点数据域不存放数据,头结点的作用就是代表整个链表起始结点的,并通过它可以遍历后续所有结点。
不带头结点:头结点数据域与其他结点一样存放用户数据。它同带头结点一样也是链表起始,通过它才能访问整个链表的后续。
每一个结点都用一个结构体变量表示,这个结构体变量除包括用户实际需要的数据成员外,还应包括一个结构体指针。访问链表时,必须从头指针开始,依次寻找。
带头结点和不带头结点链表结构分别见图1和图2
图1 带头结点
图2 不带头结点
- 链表的创建及遍历
(1)简单链表的创建
见下面程序
#include <stdio.h>
#include <stdlib.h>
#define NULL 0
struct student //定义结构体类型student
{
long num; float score;
struct student *next;
};
int main()
{ struct student a,b,c,*head,*p;
//定义结构体变量a,b,c和结构体指针变量head,p,在内存中开辟出同样大小的空间
a. num=99101; a.score=89.5; //为结构体变量a的数据域赋值
b. num=99103; b.score=90; //为结构体变量b的数据域赋值
c. num=99107; c.score=85; //为结构体变量c的数据域赋值
head=&a; //为头结点赋值
a.next=&b; //给a的地址域赋值
b.next=&c; //给b的地址域赋值
c.next=NULL; //给链表最后一个结点的地址域赋空值,此值可以用于判断链表结尾
p=head; //下面用结构体指针变量遍历了这三个结点的链表
do
{ printf("%ld %5.1f\n",p->num,p->score); //输出当前p指向的结点的数据域
p=p->next; //访问链表中下一个结点,这是p在移动
} while(p!=NULL); //判断链表是否到尾。
return 0;
}
2)动态链表的创建
所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。不带头结点的动态链表的N-S流程图,见图1:
主要过程如下:
1)首结点的创建。利用p1开辟空间,存入相应的数据域数据(我们约定数据域学号为0即停止创建);并让p1,head同时指向它,见图2;
2)完成动态开辟的新结点的连接。此过程分三步:
①再去开辟新的结点空间,同时让p1指向它,为地址域赋值,见图3(a)
②让p2的地址域指向p1,完成连接,见图3(b);
③修改新链表的结尾,见图3(c);
图2 头结点
图3 连接新结点
3)连续完成2)步。结果见图4
图4 连接结点
4)在重复做2)的过程中,如果在①过程中p1的数值的学号若赋值为0,则为链表中最后一个结点的地址域赋值为NULL,见图5结束创建。
图5 连接结点并结束
创建不带头结点的动态链表的完整函数如下:
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define NULL 0 //令NULL代表0,用它表示“空地址
#define LEN sizeof(struct student) //令LEN代表struct student类型数据的长度
struct student
{ long num;
float score;
struct student *next;
};
int n; //n为全局变量代表创建结点的个数,本文件模块中各函数均可使用它
struct student *creat()
{
struct student *head,*p1,*p2;
n=0;
p1=p2=( struct student*) malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
head=NULL;
while(p1->num!=0)
{
n=n+1;
if(n==1) head=p1;
else p2->next=p1;
p2=p1;
p1=(struct student*)malloc(LEN);
scanf("%ld,%f",&p1->num,&p1->score);
}
p2->next=NULL;
return(head);
}
(3)动态链表的遍历
动态链表的遍历可以实现对目标结点的查询、链表结点的输出等相应操作,具体遍历过程同(1)中简单链表创建中的结点输出。基本程序段如下:
p=head; //下面用结构体指针变量遍历了这三个结点的链表
do
{
printf("%ld %5.1f\n",p->num,p->score);
//输出当前p指向的结点的数据域
p=p->next;
//访问链表中下一个结点,这是p在移动
} while(p!=NULL); //判断链表是否到尾。
链表结点的删除与插入
1)基本概念
从一个动态链表中删去一个结点,并不是真正从内存中把它抹掉,而是把它从链表中分离开来,即撤销原来的链接关系。见图1
图1 链表结点的删除
图1中结点C被孤立了,就是从链表这个集体中删除了.
思路分析
①根据链表删除的概念,结点的删除就是找到这个结点然后把它从链表中孤立出去。孤立的方法就是让它的前导结点的地址域不在存储删除结点的地址,而是存储它后一个结点的地址。由此可以看出要删除目标结点,首先要找到它,其次要将其前一个结点的地址修改。这样在删除过程中除了链表头结点,还应该有记录删除结点位置的变量,以及删除结点的前导结点。
②由链表的结构可以看出,单链表中链表的头结点可以代表整个链表,因此头结点特殊且重要。在删除结点时,就要分为链表中被删除的是头结点还是中间结点。
如果是头结点应该怎么办?中间结点怎么办?尾结点又有特殊性吗?根据这三种情况加以分析,确定删除结点的具体过程。
删除结点的步骤
删除结点的过程见图1的(a)~(d)
图1 链表结点删除示意图
图1中,head为链表头结点,p1负责实现查找要删除结点的,p1在移动过程中遍历链表结点,直到找到目标结点;p2为目标结点的前导结点,它在p1移动判断前赋值。删除流程图见图2。
程序如下:
struct student *del(struct student *head,long num)
{ struct student *p1,*p2;
if (head==NULL)
printf("\nlist null!\n");
else
{
p1=head;
while(num!=p1->num && p1->next!=NULL)
{
p2=p1;p1=p1->next;
}
if(num==p1->num)
{
if(p1==head)
head=p1->next;
else
p2->next=p1->next;
printf("delete:%ld\n",num);
free(p1);
n=n-1;
}
else printf("%ld not been found!\n",num);
}
return(head);
}
链表结点的插入
1)基本概念
对链表结点的插入就是指将一个结点插入到一个已有的链表中。
2)思路分析
如果将一个结点插入到一个链表中,首先要知道的是插入的位置,其次是插入的过程。
插入位置的确定可以根据①在指定位置插入②按照某种顺序规则插入 两种情形分别实现。两种中每一种都是明确要插入结点应被连接的位置。
具体插入过程的实现如果是被插结点要插入到链表中某结点之前,那么就将被插结点的地址域置为这个结点的地址,同时让这个结点的前导结点的地址域记为被插结点的地址即可完成。
实际插入时要结合被插位置是链表的头、中间还是链表尾来具体实现,
见图3。
图3 结点插入示意图
其中图(d)为插入在链表头,a),b),c)完成结点在中间的插入;e)在链表尾的插入。
实现流程图见图4.
完结撒花!!!(*^▽^*)