C语言技能实践之链表的创建与操作

一、适用对象

       本博文主要针对对象是刚刚入门C的学生、刚刚接触C语言技能实践的同学、有一定链表知识的同学。


二、链表定义

       所谓链表,字面上理解就是用一条链将结点连接而组成的表。在数据结构层面上讲,链表是由数据域和指针域组成的线性表,具有线性结构和一对一的特性。无论是在学习C语言程序开发,还是日常工作系统的研发,一旦接触C,我们都离不开链表。因此,链表的深度理解与独立编程成为C程序员不可或缺的基础,必须牢牢掌握。


三、链表的构成

链表的物理结构:数据域 + 指针域

数据域:又称数据元素(由若干个数据项组成,数据的基本单位)

指针域:主要存储结点与结点之间的关系(即下一结点的物理地址)

一个基本结点的逻辑结构如下图所示:

       与单独由数组构成的顺序表不同的是,由于链表所具有的指针域,决定了链表可以实现结点之间的地址(所在位置)可以在结构上不相邻,只需在每个结点的数据域后添加地址信息即可通过指针将当前结点与下一结点连接起来,从而形成链表。链表的逻辑结构如下图所示:

       由于线性表是若干个数据元素组成的有序序列,因而在描述线性表(顺序表+链表),在本博文主题中的,即链表,它的逻辑结构描述可以表达如下:

        如上图,L = (A,B,C)

        清楚地认识了链表结点的构造以及链表的结构后,我们开始进入链表的C语言自定义程序编写,其中该模块用到的C语言自定义函数有Init_LinkList(初始化函数),为了养成良好的编程习惯,故作者先带领读者来实现C语言链表数据类型的定义。下面的代码可供读者参考:

#include <stdio.h>
#include <stdlib.h>
typedef int Elemtype; /*将int数据类型自定义为Elemtype类型*/
typedef struct LinkList{
Elemtype data;  /*数据域*/
struct LinkList *next; /*后继指针*/
}LinkList;   /*定义一个LinkList链表数据类型*/
LinkList *head; /*定义头结点*/
LinkList *Init_LinkList(){
LinkList *head = (LinkList*)malloc(sizeof(LinkList)); /*初始化链表,为头结点分配动态内存*/
head->next = NULL; /*初始化头结点后继指针指向*/
head->data = 0; /*头结点数据域用作计数器*/
return head;
}

四、链表的构造与操作

       在了解了上述关于链表的基本定义后,我们开始进一步挖掘链表的基本操作。言归正传,链表的基本操作可以包含有增加、删除、查找、修改,简称增删查改,这些是链表的最基本操作,其他运算都是在这几个基本操作的基础上去实现的。因此,清晰地了解链表基本操作的原理对于后续我们用C写项目或系统打下坚实基础。

1、链表的插入(增加)

       链表的增加和插入,顾名思义,就是在一个空链表上插入一个新的结点,或是在一个结点与另一个结点之间插入一个新结点,亦或是在链表的尾结点加入一个新结点。因此,链表的插入有三种情况。基于这三种情况,链表的插入则分成了头插法和尾插法。

(1)头插法

       一般地,对于我们初学者,作者建议在初始化链表(即创建空链表)应自动添加一个头结点,该头结点不存放如何数据元素,仅是为了方便增删查改的运算,这也是头插法的捷径。原因是,头插法的定义是在头结点后面添加新结点,并使新结点的后继指针指向首结点的过程。为了方便理解这段话中的关键名词的含义,作者会在下面具体阐述这几个名词的意义。 

(a)头结点:链表的第一个结点,该结点不存放任何实质性的数据,可以用于作计数器,也可以用作头插法的捷径,简化头插法的循环遍历运算,头插法时不需要在重新定位指针。

(b)首结点:链表的第一个数据元素,该结点才是链表的第一个元素,它存放链表的第一个数据元素。

(c)尾结点:链表的最后一个数据元素,通常在线性表中,尾结点也可以作链表的尾结点。尾结点在链队列里面起着重要作用,其他情况下,我们可以忽略尾结点在C程序中的定义,尤其是单链表下,我们在访问链表最后一个元素时,可通过while/for循环来改变指针定向来访问尾结点,但这个步骤的前提是要定义好头结点,从头结点出发去循环遍历。除此以外,尾结点也为尾插法作好铺垫。在本章博文中,作者仅采用循环遍历的方法来实现尾插法(C语言实操)。

       理解了上面的专业术语的含义后,我们可以进入到头插法的过程这一关键步骤。前面,我们粗略了解了头结点的作用,所以我们不难发现,头插法其实是基于头结点来对链表进行插入操作,即在链表的头结点的后继指针指向新结点,新结点指向首结点的过程。为了读者更好理解作者的意义,作者通过如下图来演示头插法的过程:

       值得注意的是,在执行头插法时,指针指向的调整顺序必须遵循以下原则:先将新结点的后继指针指向头结点的后继结点,最后再调整头结点的后继指针指向新结点。这样做的原因是,防止后续链表丢失。

        在理解了上述步骤的基础上,我们可以来编写C语言链表之头插法的函数。该函数可以命名为Insert_LinkList,其中类型可以为LinkList/void/int等,但在使用这些类型时要注意返回的类型。

头插法的C代码如下,可供读者参考:

void Insert_LinkList(LinkList *head,Elemtype data){
LinkList *p = (LinkList*)malloc(sizeof(LinkList)); //为新结点分配动态内存空间
p->next = NULL;  //新结点后继指向NULL
p->data = data;  //赋值
p->next = head->next;  //连接第一步
head->next = p;  //连接第二步
head->data++;  //修改计数器值
}

(2)尾插法

       在头插法的实训过程中,我们发现头插法是利用头结点对链表进行插入操作,那么举一反三,尾插法的思想跟头插法就大相径庭了。尾插法,就是利用尾结点对链表进行插入运算。当然,尾插法既可以通过自定义尾结点来实现插入操作,同时也可以省掉一些步骤,直接通过循环来找到链表的尾结点,再用临时变量存储尾结点的地址,最后在尾结点的后继指针调整指向,指向新结点即可完成尾插法。

与头插法阐述一样,为了方便读者进一步理解,作者特地通过以下几个图来使过程更容易看懂。

了解了上述步骤,我们开始进入C语言尾插法函数的编写,下面代码示例,可供读者参考:

void TailInsert_LinkList(LinkList *head,Elemtype data){
LinkList *s = head; //用s来存储头结点位置
LinkList *p = (LinkList*)malloc(sizeof(LinkList));  //为新结点开辟空间
p->data = data;  //赋值
p->next = NULL;  //后继指针指向NULL
while(s->next){
  s = s->next;  //遍历查找尾结点
}
s->next = p; //在尾结点后面插入新结点
head->data++;  //计数器累加
}

2、链表的删除

       在已经掌握了插入运算后,恭喜你已经大致了解了链表的基本操作。我们不难发现,链表的增删查改都是基于指针的修改,因此,指针的知识需要我们十分熟悉。在讲到链表的删除操作,我们也可以顾名思义,链表的删除就是将上一个结点与下一个结点用后继指针连接起来,从而将当前结点与下一结点断链,最终释放当前结点的内存空间,这就是删除运算的基本思想。

与插入算法的描述一样,我们先通过简单的图解来加深对删除运算的理解。

下面,我们通过C代码的形式来进一步理解:

void Delete_LinkList(LinkList *head,Elemtype data){
LinkList *pre = head;  //存储前驱结点
LinkList *cur = head->next;  //存储要删除的结点
while(cur){
  if(cur->data == data){
     pre->next = cur->next;  //若找到匹配值,则删除
  }
  pre = cur;
  cur = cur->next; //否则调整指针位置
}
free(cur);  //删除运算结束,释放内存
}

3、链表的查找

对于学习过数据结构的小伙伴们都知道,查找有许多办法,譬如二分查找、折半查找、顺序查找等。对于刚入门的小伙伴,读者推荐先使用顺序查找的办法来实现链表的查找运算。

       所谓顺序查找,就是从头结点开始遍历,不断地将目标值与每个结点数据域进行一一比对,对上了就进行返回指针或是返回位置。综合而言,查找和删除的思想大致相同,都是用了遍历的方法来查找。本博文以返回指针来实现。示例代码如下所示,可供读者参考:

LinkList *SequenSearch_LinkList(LinkList *head,Elemtype target){
LinkList *p = head->next;
while(p){
  if(p->data == target){
     return p;
  }
  p = p->next;
}
return NULL;
}

4、链表的修改

       谈到链表的修改,其过程亦是万变不离其宗,均是利用循环遍历方式来实现,分别有通过值、通过下标(通常是数组为结构的顺序表)方法来对链表进行修改。

由于其逻辑思想与上述差不多,故不再通过图解法来展开赘述,有兴趣的读者可以依据上述的插入、删除来自行画图,进一步加深理解。下面,我们将进行C语言代码呈现,可供读者参考:

int Edit_LinkList(LinkList *head,Elemtype key,Elemtype result){
LinkList *p = head->next;
while(p){
  if(p->data == key){
    p->data = result;
    return 1;
  }
  p = p->next;
}
return 0;
}

上述代码中,key是目标值,result是修改后的值。


以上是作者的个人心得与体会,希望可以对诸位学习C语言链表的同学有一些帮助,如文章有模糊或不正之处,敬请广大资深博主或程序代码爱好者雅正。在阅览的同时,麻烦您转发、点赞加支持,您的小小爱心举动会让我更有信心,十分感谢!

  • 36
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值