逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表那样也相互紧挨着。恰恰相反,数据随机分布在内存中的各个位置,这种存储结构称为线性表的链式存储。
由于分散存储,为了能够体现出数据元素之间的逻辑关系,每个数据元素在存储的同时,要配备一个指针,用于指向它的直接后继元素,即每一个数据元素都指向下一个数据元素(最后一个指向NULL(空))。
图1 链式存储存放数据
如图1所示,当每一个数据元素都和它下一个数据元素用指针链接在一起时,就形成了一个链,这个链子的头就位于第一个数据元素,这样的存储方式就是链式存储。
线性表的链式存储结构生成的表,称作“链表”。
链表中数据元素的构成
每个元素本身由两部分组成:- 本身的信息,称为“数据域”;
- 指向直接后继的指针,称为“指针域”。
图2 结点的构成 这两部分信息组成数据元素的存储结构,称之为“结点”。n个结点通过指针域相互链接,组成一个链表。
图3 含有n个结点的链表
图 3 中,由于每个结点中只包含一个指针域,生成的链表又被称为 线性链表 或 单链表。
链表中存放的不是基本数据类型,需要用结构体实现自定义:
- typedef struct Link{
- char elem;//代表数据域
- struct Link * next;//代表指针域,指向直接后继元素
- }link;
头结点、头指针和首元结点
头结点:有时,在链表的第一个结点之前会额外增设一个结点,结点的数据域一般不存放数据(有些情况下也可以存放链表的长度等信息),此结点被称为头结点。首元结点:链表中第一个元素所在的结点,它是头结点后边的第一个结点。若头结点的指针域为空(NULL),表明链表是空表。头结点对于链表来说,不是必须的,在处理某些问题时,给链表添加头结点会使问题变得简单。
头指针:永远指向链表中第一个结点的位置(如果链表有头结点,头指针指向头结点;否则,头指针指向首元结点)。
头结点和头指针的区别:头指针是一个指针,头指针指向链表的头结点或者首元结点;头结点是一个实际存在的结点,它包含有数据域和指针域。两者在程序中的直接体现就是:头指针只声明而没有分配存储空间,头结点进行了声明并分配了一个结点的实际物理内存。
图 4 头结点、头指针和首元结点
单链表中可以没有头结点,但是不能没有头指针!
链表的创建和遍历
万事开头难,初始化链表首先要做的就是创建链表的头结点或者首元结点。创建的同时,要保证有一个指针永远指向的是链表的表头,这样做不至于丢失链表。例如创建一个链表(1,2,3,4):
- link * initLink(){
- link * p=(link*)malloc(sizeof(link));//创建一个头结点
- link * temp=p;//声明一个指针指向头结点,用于遍历链表
- //生成链表
- for (int i=1; i<5; i++) {
- link *a=(link*)malloc(sizeof(link));
- a->elem=i;
- a->next=NULL;
- temp->next=a;
- temp=temp->next;
- }
- return p;
- }
链表中查找某结点
一般情况下,链表只能通过头结点或者头指针进行访问,所以实现查找某结点最常用的方法就是对链表中的结点进行逐个遍历。实现代码:
- int selectElem(link * p,int elem){
- link * t=p;
- int i=1;
- while (t->next) {
- t=t->next;
- if (t->elem==elem) {
- return i;
- }
- i++;
- }
- return -1;
- }
链表中更改某结点的数据域
链表中修改结点的数据域,通过遍历的方法找到该结点,然后直接更改数据域的值。实现代码:
- //更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
- link *amendElem(link * p,int add,int newElem){
- link * temp=p;
- temp=temp->next;//在遍历之前,temp指向首元结点
- //遍历到被删除结点
- for (int i=1; i<add; i++) {
- temp=temp->next;
- }
- temp->elem=newElem;
- return p;
- }
向链表中插入结点
链表中插入头结点,根据插入位置的不同,分为3种:- 插入到链表的首部,也就是头结点和首元结点中间;
- 插入到链表中间的某个位置;
- 插入到链表最末端;
图 5 链表中插入结点5
虽然插入位置有区别,都使用相同的插入手法。分为两步,如图 5 所示:
- 将新结点的next指针指向插入位置后的结点;
- 将插入位置前的结点的next指针指向插入结点;
提示:在做插入操作时,首先要找到插入位置的上一个结点,图4中,也就是找到结点 1,相应的结点 2 可通过结点 1 的 next 指针表示,这样,先进行步骤 1,后进行步骤 2,实现过程中不需要添加其他辅助指针。
实现代码:
- link * insertElem(link * p,int elem,int add){
- link * temp=p;//创建临时结点temp
- //首先找到要插入位置的上一个结点
- for (int i=1; i<add; i++) {
- if (temp==NULL) {
- printf("插入位置无效\n");
- return p;
- }
- temp=temp->next;
- }
- //创建插入结点c
- link * c=(link*)malloc(sizeof(link));
- c->elem=elem;
- //向链表中插入结点
- c->next=temp->next;
- temp->next=c;
- return p;
- }
从链表中删除节点
当需要从链表中删除某个结点时,需要进行两步操作:- 将结点从链表中摘下来;
- 手动释放掉结点,回收被结点占用的内存空间;
使用malloc函数申请的空间,一定要注意手动free掉。否则在程序运行的整个过程中,申请的内存空间不会自己释放(只有当整个程序运行完了以后,这块内存才会被回收),造成内存泄漏,别把它当成是小问题。
实现代码:
- link * delElem(link * p,int add){
- link * temp=p;
- //temp指向被删除结点的上一个结点
- for (int i=1; i<add; i++) {
- temp=temp->next;
- }
- link * del=temp->next;//单独设置一个指针指向被删除结点,以防丢失
- temp->next=temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
- free(del);//手动释放该结点,防止内存泄漏
- return p;
- }
完整代码
- #include <stdio.h>
- #include <stdlib.h>
- typedef struct Link{
- int elem;
- struct Link *next;
- }link;
- link * initLink();
- //链表插入的函数,p是链表,elem是插入的结点的数据域,add是插入的位置
- link * insertElem(link * p,int elem,int add);
- //删除结点的函数,p代表操作链表,add代表删除节点的位置
- link * delElem(link * p,int add);
- //查找结点的函数,elem为目标结点的数据域的值
- int selectElem(link * p,int elem);
- //更新结点的函数,newElem为新的数据域的值
- link *amendElem(link * p,int add,int newElem);
- void display(link *p);
- int main() {
- //初始化链表(1,2,3,4)
- printf("初始化链表为:\n");
- link *p=initLink();
- display(p);
- printf("在第4的位置插入元素5:\n");
- p=insertElem(p, 5, 4);
- display(p);
- printf("删除元素3:\n");
- p=delElem(p, 3);
- display(p);
- printf("查找元素2的位置为:\n");
- int address=selectElem(p, 2);
- if (address==-1) {
- printf("没有该元素");
- }else{
- printf("元素2的位置为:%d\n",address);
- }
- printf("更改第3的位置的数据为7:\n");
- p=amendElem(p, 3, 7);
- display(p);
- return 0;
- }
- link * initLink(){
- link * p=(link*)malloc(sizeof(link));//创建一个头结点
- link * temp=p;//声明一个指针指向头结点,用于遍历链表
- //生成链表
- for (int i=1; i<5; i++) {
- link *a=(link*)malloc(sizeof(link));
- a->elem=i;
- a->next=NULL;
- temp->next=a;
- temp=temp->next;
- }
- return p;
- }
- link * insertElem(link * p,int elem,int add){
- link * temp=p;//创建临时结点temp
- //首先找到要插入位置的上一个结点
- for (int i=1; i<add; i++) {
- if (temp==NULL) {
- printf("插入位置无效\n");
- return p;
- }
- temp=temp->next;
- }
- //创建插入结点c
- link * c=(link*)malloc(sizeof(link));
- c->elem=elem;
- //向链表中插入结点
- c->next=temp->next;
- temp->next=c;
- return p;
- }
- link * delElem(link * p,int add){
- link * temp=p;
- //遍历到被删除结点的上一个结点
- for (int i=1; i<add; i++) {
- temp=temp->next;
- }
- link * del=temp->next;//单独设置一个指针指向被删除结点,以防丢失
- temp->next=temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
- free(del);//手动释放该结点,防止内存泄漏
- return p;
- }
- int selectElem(link * p,int elem){
- link * t=p;
- int i=1;
- while (t->next) {
- t=t->next;
- if (t->elem==elem) {
- return i;
- }
- i++;
- }
- return -1;
- }
- link *amendElem(link * p,int add,int newElem){
- link * temp=p;
- temp=temp->next;//tamp指向首元结点
- //temp指向被删除结点
- for (int i=1; i<add; i++) {
- temp=temp->next;
- }
- temp->elem=newElem;
- return p;
- }
- void display(link *p){
- link* temp=p;//将temp指针重新指向头结点
- //只要temp指针指向的结点的next不是Null,就执行输出语句。
- while (temp->next) {
- temp=temp->next;
- printf("%d",temp->elem);
- }
- printf("\n");
- }
初始化链表为: 1234 在第4的位置插入元素5: 12354 删除元素3: 1254 查找元素2的位置为: 元素2的位置为:2 更改第3的位置的数据为7: 1274
总结
线性表的链式存储相比于顺序存储,有两大优势:- 链式存储的数据元素在物理结构没有限制,当内存空间中没有足够大的连续的内存空间供顺序表使用时,可能使用链表能解决问题。(链表每次申请的都是单个数据元素的存储空间,可以利用上一些内存碎片)
- 链表中结点之间采用指针进行链接,当对链表中的数据元素实行插入或者删除操作时,只需要改变指针的指向,无需像顺序表那样移动插入或删除位置的后续元素,简单快捷。
链表和顺序表相比,不足之处在于,当做遍历操作时,由于链表中结点的物理位置不相邻,使得计算机查找起来相比较顺序表,速度要慢。
基本操作
学会创建链表之后,本节继续讲解链表的一些基本操作,包括向链表中添加数据、删除链表中的数据、查找和更改链表中的数据。
首先,创建一个带头结点的链表,链表中存储着 {1,2,3,4}:
- //链表中节点的结构
- typedef struct link {
- int elem;
- struct link* next;
- }Link;
- Link* initLink() {
- int i;
- //1、创建头指针
- Link* p = NULL;
- //2、创建头结点
- Link* temp = (Link*)malloc(sizeof(Link));
- temp->elem = 0;
- temp->next = NULL;
- //头指针指向头结点
- p = temp;
- //3、每创建一个结点,都令其直接前驱结点的指针指向它
- for (i = 1; i < 5; i++) {
- //创建一个结点
- Link* a = (Link*)malloc(sizeof(Link));
- a->elem = i;
- a->next = NULL;
- //每次 temp 指向的结点就是 a 的直接前驱结点
- temp->next = a;
- //temp指向下一个结点(也就是a),为下次添加结点做准备
- temp = temp->next;
- }
- return p;
- }
链表插入元素
同顺序表一样,向链表中增添元素,根据添加位置不同,可分为以下 3 种情况:
- 插入到链表的头部,作为首元节点;
- 插入到链表中间的某个位置;
- 插入到链表的最末端,作为链表中最后一个结点;
对于有头结点的链表,3 种插入元素的实现思想是相同的,具体步骤是:
- 将新结点的 next 指针指向插入位置后的结点;
- 将插入位置前结点的 next 指针指向插入结点;
例如,在链表 {1,2,3,4}
的基础上分别实现在头部、中间、尾部插入新元素 5,其实现过程如图 1 所示:
图 1 带头结点链表插入元素的 3 种情况
从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1 ,再执行步骤 2。实现代码如下:
- void insertElem(Link* p, int elem, int add) {
- int i;
- Link* c = NULL;
- Link* temp = p;//创建临时结点temp
- //首先找到要插入位置的上一个结点
- for (i = 1; i < add; i++) {
- temp = temp->next;
- if (temp == NULL) {
- printf("插入位置无效\n");
- return;
- }
- }
- //创建插入结点c
- c = (Link*)malloc(sizeof(Link));
- c->elem = elem;
- //① 将新结点的 next 指针指向插入位置后的结点
- c->next = temp->next;
- //② 将插入位置前结点的 next 指针指向插入结点;
- temp->next = c;
- }
注意:链表插入元素的操作必须是先步骤 1,再步骤 2;反之,若先执行步骤 2,除非再添加一个指针,作为插入位置后续链表的头指针,否则会导致插入位置后的这部分链表丢失,无法再实现步骤 1。
对于没有头结点的链表,在头部插入结点比较特殊,需要单独实现。
图 2 不带头结点链表插入元素的 3 种情况
和 2)、3) 种情况相比,由于链表没有头结点,在头部插入新结点,此结点之前没有任何结点,实现的步骤如下:
- 将新结点的指针指向首元结点;
- 将头指针指向新结点。
实现代码如下:
- Link* insertElem(Link* p, int elem, int add) {
- if (add == 1) {
- //创建插入结点c
- Link* c = (Link*)malloc(sizeof(Link));
- c->elem = elem;
- c->next = p;
- p = c;
- return p;
- }
- else {
- int i;
- Link* c = NULL;
- Link* temp = p;//创建临时结点temp
- //首先找到要插入位置的上一个结点
- for (i = 1; i < add-1; i++) {
- temp = temp->next;
- if (temp == NULL) {
- printf("插入位置无效\n");
- return p;
- }
- }
- //创建插入结点c
- c = (Link*)malloc(sizeof(Link));
- c->elem = elem;
- //向链表中插入结点
- c->next = temp->next;
- temp->next = c;
- return p;
- }
- }
注意当 add==1 成立时,形参指针 p 的值会发生变化,因此需要它的新值作为函数的返回值返回。
链表删除元素
从链表中删除指定数据元素时,实则就是将存有该数据元素的节点从链表中摘除。
对于有头结点的链表来说,无论删除头部(首元结点)、中部、尾部的结点,实现方式都一样,执行以下三步操作:
- 找到目标元素所在结点的直接前驱结点;
- 将目标结点从链表中摘下来;
- 手动释放结点占用的内存空间;
从链表上摘除目标节点,只需找到该节点的直接前驱节点 temp,执行如下操作:
- temp->next=temp->next->next;
例如,从存有 {1,2,3,4}
的链表中删除存储元素 3 的结点,则此代码的执行效果如图 3 所示:
图 3 带头结点链表删除元素
实现代码如下:
- //p为原链表,elem 为要删除的目标元素
- int delElem(Link* p, int elem) {
- Link* del = NULL, *temp = p;
- int find = 0;
- //1、找到目标元素的直接前驱结点
- while (temp->next) {
- if (temp->next->elem == elem) {
- find = 1;
- break;
- }
- temp = temp->next;
- }
- if (find == 0) {
- return -1;//删除失败
- }
- else
- {
- //标记要删除的结点
- del = temp->next;
- //2、将目标结点从链表上摘除
- temp->next = temp->next->next;
- //3、释放目标结点
- free(del);
- return 1;
- }
- }
对于不带头结点的链表,需要单独考虑删除首元结点的情况,删除其它结点的方式和图 3 完全相同,如下图所示:
图 4 不带头结点链表删除结点
实现代码如下:
- //p为原链表,elem 为要删除的目标元素
- int delElem(Link** p, int elem) {
- Link* del = NULL, *temp = *p;
- //删除首元结点需要单独考虑
- if (temp->elem == elem) {
- (*p) = (*p)->next;
- free(temp);
- return 1;
- }
- else
- {
- int find = 0;
- //1、找到目标元素的直接前驱结点
- while (temp->next) {
- if (temp->next->elem == elem) {
- find = 1;
- break;
- }
- temp = temp->next;
- }
- if (find == 0) {
- return -1;//删除失败
- }
- else
- {
- //标记要删除的结点
- del = temp->next;
- //2、将目标结点从链表上摘除
- temp->next = temp->next->next;
- //3、释放目标结点
- free(del);
- return 1;
- }
- }
- }
函数返回 1 时,表示删除成功;返回 -1,表示删除失败。注意,该函数的形参 p 为二级指针,调用时需要传递链表头指针的地址。
链表查找元素
在链表中查找指定数据元素,最常用的方法是:从首元结点开始依次遍历所有节点,直至找到存储目标元素的结点。如果遍历至最后一个结点仍未找到,表明链表中没有存储该元素。
因此,链表中查找特定数据元素的 C 语言实现代码为:
- //p为原链表,elem表示被查找元素
- int selectElem(Link* p, int elem) {
- int i = 1;
- //带头结点,p 指向首元结点
- p = p->next;
- while (p) {
- if (p->elem == elem) {
- return i;
- }
- p = p->next;
- i++;
- }
- return -1;//返回-1,表示未找到
- }
注意第 5 行代码,对于有结点的链表,需要先将 p 指针指向首元结点;反之,对于不带头结点的链表,注释掉第 5 行代码即可。
链表更新元素
更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作即可。
直接给出链表中更新数据元素的 C 语言实现代码:
- //p 为有头结点的链表,oldElem 为旧元素,newElem 为新元素
- int amendElem(Link* p, int oldElem, int newElem) {
- p = p->next;
- while (p) {
- if (p->elem == oldElem) {
- p->elem = newElem;
- return 1;
- }
- p = p->next;
- }
- return -1;
- }
函数返回 1,表示更改成功;返回数字 -1,表示更改失败。如果是没有头结点的链表,直接删除第 3 行代码即可。
总结
以上内容详细介绍了对链表中数据元素做"增删查改"的实现过程及 C 语言代码,最后给大家一段完整的代码,实现对有头结点链表的“增删查改”:
- #include <stdio.h>
- #include <stdlib.h>
- //链表中节点的结构
- typedef struct link {
- int elem;
- struct link* next;
- }Link;
- Link* initLink() {
- int i;
- //1、创建头指针
- Link* p = NULL;
- //2、创建头结点
- Link* temp = (Link*)malloc(sizeof(Link));
- temp->elem = 0;
- temp->next = NULL;
- //头指针指向头结点
- p = temp;
- //3、每创建一个结点,都令其直接前驱结点的指针指向它
- for (i = 1; i < 5; i++) {
- //创建一个结点
- Link* a = (Link*)malloc(sizeof(Link));
- a->elem = i;
- a->next = NULL;
- //每次 temp 指向的结点就是 a 的直接前驱结点
- temp->next = a;
- //temp指向下一个结点(也就是a),为下次添加结点做准备
- temp = temp->next;
- }
- return p;
- }
- //p为链表,elem为目标元素,add为要插入的位置
- void insertElem(Link* p, int elem, int add) {
- int i;
- Link* c = NULL;
- Link* temp = p;//创建临时结点temp
- //首先找到要插入位置的上一个结点
- for (i = 1; i < add; i++) {
- temp = temp->next;
- if (temp == NULL) {
- printf("插入位置无效\n");
- return;
- }
- }
- //创建插入结点c
- c = (Link*)malloc(sizeof(Link));
- c->elem = elem;
- //① 将新结点的 next 指针指向插入位置后的结点
- c->next = temp->next;
- //② 将插入位置前结点的 next 指针指向插入结点;
- temp->next = c;
- }
- //p为原链表,elem 为要删除的目标元素
- int delElem(Link* p, int elem) {
- Link* del = NULL, *temp = p;
- int find = 0;
- //1、找到目标元素的直接前驱结点
- while (temp->next) {
- if (temp->next->elem == elem) {
- find = 1;
- break;
- }
- temp = temp->next;
- }
- if (find == 0) {
- return -1;//删除失败
- }
- else
- {
- //标记要删除的结点
- del = temp->next;
- //2、将目标结点从链表上摘除
- temp->next = temp->next->next;
- //3、释放目标结点
- free(del);
- return 1;
- }
- }
- //p为原链表,elem表示被查找元素
- int selectElem(Link* p, int elem) {
- int i = 1;
- //带头结点,p 指向首元结点
- p = p->next;
- while (p) {
- if (p->elem == elem) {
- return i;
- }
- p = p->next;
- i++;
- }
- return -1;//返回-1,表示未找到
- }
- //p 为有头结点的链表,oldElem 为旧元素,newElem 为新元素
- int amendElem(Link* p, int oldElem, int newElem) {
- p = p->next;
- while (p) {
- if (p->elem == oldElem) {
- p->elem = newElem;
- return 1;
- }
- p = p->next;
- }
- return -1;
- }
- //输出链表中各个结点的元素
- void display(Link* p) {
- p = p->next;
- while (p) {
- printf("%d ", p->elem);
- p = p->next;
- }
- printf("\n");
- }
- //释放链表
- void Link_free(Link* p) {
- Link* fr = NULL;
- while (p->next)
- {
- fr = p->next;
- p->next = p->next->next;
- free(fr);
- }
- free(p);
- }
- int main() {
- Link* p = initLink();
- printf("初始化链表为:\n");
- display(p);
- printf("在第 3 的位置上添加元素 6:\n");
- insertElem(p, 6, 3);
- display(p);
- printf("删除元素4:\n");
- delElem(p, 4);
- display(p);
- printf("查找元素 2:\n");
- printf("元素 2 的位置为:%d\n", selectElem(p, 2));
- printf("更改元素 1 的值为 6:\n");
- amendElem(p, 1, 6);
- display(p);
- Link_free(p);
- return 0;
- }
执行结果为:
初始化链表为:
1 2 3 4
在第 3 的位置上添加元素 6:
1 2 6 3 4
删除元素4:
1 2 6 3
查找元素 2:
元素 2 的位置为:2
更改元素 1 的值为 6:
6 2 6 3
标签:结点,单链,temp,int,及其,elem,next,链表,基本操作 From: https://www.cnblogs.com/suishou/p/16808551.html