ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

c语言链表与结构体

c语言链表与结构体 摘要本文从结构体讲起介绍如何用 struct 定义并调用结构体、通过点号与 strcpy 完成成员赋值再引入结构体指针来灵活修改数据随后基于结构体指针引出链表讲解节点的创建、尾插、尾删与整体释放等核心操作并简要介绍双向链表、循环链表和快慢指针等进阶技巧帮助读者理解内存管理与指针应用。目录一、结构体详解二、链表核心操作三、总结一、结构体详解结构体是一类特殊的数据类型相当于一个存储多个数据的包裹里面包含各种我们所需的数据比如一个描述人的结构体里面可能包含年龄身高学历等数据每个人的数据都不相同但都是一样的数据类型。我们用 struct 来定义结构体这种数据类型。1.结构体的调用我们这里定义一个结构体struct person{int age;char name[20];};我们可以清晰地看出这个结构体所包含的信息是年龄和姓名怎么调用它呢struct person p1;这里我们看到 struct person 是我们所定义的一个结构体后面是我们结构体的一个对象可以类比为 int pstruct person 相当于 int 的作用p1 相当于 p也就是变量名。那么怎么给 p1 里面的内容赋值呢这里我们需要借助一个专用的运算符——点号.通过 p1.age14 即可完成赋值。需要注意的是结构体中的字符串不能直接用 赋值必须借助 strcpy 函数例如 strcpy(p1.name, 张三)将字符串复制到成员中。这里我给一个完整的代码示例#include stdio.h #include string.h // 定义结构体 struct person { int age; char name[20]; }; int main() { // 先声明结构体对象 struct person p1; // 再给 p1 里面的内容赋值 p1.age 14; strcpy(p1.name, 张三); // 打印结构体成员 printf(p1: 年龄%d, 姓名%s\n, p1.age, p1.name); return 0; }其实结构体就类似于一个多功能的数组可以存放各种数据。这里我们可以发现好像只有 p1 自己才能修改它自己的值其他的都不可以好像没有其他方式了这是不是太局限了所以这里我们引入结构体指针struct person *p2p1p2 可直接指向 p1 的内容只不过这里并不是用.了而是用-我们 p2-age18p1.age 的内容也就变成了 18我们可以使用指针来指向这个结构体从而来修改它我们甚至可以设置多个指针同时指向它只要我们需要这样就解决了结构体值的灵活性问题。二、链表核心操作链表是一个特别的数据结构是我们通过结构体来实现的。我们讲过结构体是一个可以存储多种数据的包裹但如果我在这个包裹里面存的是指针我们就可以通过这个指针找到它所指向的值。这里如果我们存的又是一个结构体指针我们就可以通过存一个指针来指向多个数据。我们可以初始化一个结构体让它一开始的数据类型就有一个结构体指针。这里的 next 就是一个结构体指针它所指向的地址就是下一个结构体的地址。这里我为方便理解把这个地址专门画出来了实际过程中这个框是不存在的。我们可以通过这个指针来设置多个结构体连接在一起的数据结构这个就是链表。具体代码实现部分#include stdio.h #include stdlib.h // 定义链表节点结构体 struct Node { int data; // 数据域 struct Node *next; // 指针域指向下一个节点 }; // 创建新节点 struct Node* createNode(int value) { struct Node *newNode (struct Node*)malloc(sizeof(struct Node)); newNode-data value; newNode-next NULL; return newNode; }我粗略地讲一下这个函数的实现这个函数是一个返回类型为结构体指针的函数我们传入我们给它的数据这里是 value然后我们创建一个名叫 newNode 的结构体指针因为它所指向的是一个地址但是这个地址现在是没有空间的没有空间就代表没有一块连续的地址就无法存放数据我们要给它创造一个空间就是 malloc空间的大小是 sizeof 这个结构体这个结构体多大我们开辟多少空间所以这个指针就有了空间才能进行后续的赋值操作后面我们把这个指向下一个节点的指针进行初始化让它等于 NULL这个我们后续有非常大用这样一个节点就顺利地创建成功了。这里为什么 newNode 一定要是结构体指针而不是结构体是因为 malloc 开辟的空间是堆空间里面的不会随函数的结束而释放掉结构体其实也就是一个特殊的数据类型也是存放在栈空间里面的栈与堆的区别可参考我上一篇文章这里我们确实创建了一个节点但是这个链表并没有连接起来这里我们还需要一个函数进行连接。我们可以通过修改上一个节点的 next 指针让它指向新创建的节点从而把两个节点串联起来。下面给出一个在链表尾部插入节点的函数// 在链表尾部插入节点 void insertAtTail(struct Node **head, int value) { struct Node *newNode createNode(value); // 如果链表为空新节点就是头节点 if (*head NULL) { *head newNode; return; } // 否则遍历到链表末尾把最后一个节点的 next 指向新节点 struct Node *current *head; while (current-next ! NULL) { current current-next; } current-next newNode; }这个函数我来逐行解读1.我们先创建一个结构体指针来存放我们所创建的结构体的地址。2.想要理解这个 if我们先讲一下这个传入的参数 head。我们知道一个数据有地址我们就可以用指针存放其地址但是指针也是一种数据类型它也有地址所以我们可以用二级指针**表示这个指针的地址例如int a5;int *pa;int **ppp;我们可以这样直观地理解在调用函数时我们传入的参数和函数的参数是简单的赋值你对这个函数的参数做什么都影响不了传入的参数。但是我们改一下把这个参数的地址传入到这个函数里面我们就可以通过 * 解引用来改变这个参数的值。二级指针就是我原本要传入的参数就是一个地址但是我同时也要改变这个参数我们就像上面一样就传入这个参数这个参数是一个地址的地址这个就是我们的 head。这个 if 里面的逻辑就是我们想改变的参数如果它的值是空的我们就给它赋值把我们创建的节点的地址给它。这里就是为什么要传入二级指针因为当节点是空的时候我们要把这个空节点给替换掉。我这里要改变这个一级指针所以传入的参数必须是二级指针。3.如果这个头节点不为空呢我们就要找到这个链表的尾节点在尾节点处让尾节点的 next 指向我们这个新创建的节点。我们这里发现后续我要更改的东西只是结构体里面的东西并不是这个结构体的地址了所以后续我们就用不着二级指针了为了方便我们就用一个一级指针指向这个 *head我们知道尾节点的 next 是 NULL这里就是为什么我们要初始化 next 为 NULL因为不是尾节点的其他节点的 next 已经指向下一节点了而非 NULL。while 循环里面如果 next 不是空我们就跳到下一个节点直到为空我们在空这里让 next 指向我们所创建的新节点。这里我们是用 while 循环找到的 NULL我们其实还可以创建一个指针专门指向尾部每次创建节点时直接让这个指针的 next 指向新节点然后更新这个指针这样时间复杂度就可以降到 O(1)。这样一个链表的创建函数就成功了如果熟练的话我们可以把这两个函数合并为一个。这就是我们常用的尾插还有头插这个自行了解原理很简单。删除与链表整体 free1.链表有增加也有链表的删除理解了链表的增加大家也猜得出来链表的删除是什么流程。大致是让它的前一个节点的 next 指向跳过这个节点指向这个节点的后方但要注意这个节点是否有前一个节点因为我这里是头节点存了值的所以要注意这个我们采用头节点单纯为空、不存值的时候就不用担心这个。但是这个节点只是简单地跳过可不行我们节点都是 malloc 创建的它存放在堆空间内我们要对它进行手动的释放不然就会出现内存泄漏这是一个和增加不同的重点区别。下面我给一个链表尾节点的删除示例// 删除链表尾节点 void deleteTail(struct Node **head) { // 链表为空直接返回 if (*head NULL) { return; } // 如果只有一个节点删除后链表为空 if ((*head)-next NULL) { free(*head); *head NULL; return; } // 找到倒数第二个节点 struct Node *current *head; while (current-next-next ! NULL) { current current-next; } // 释放尾节点并把倒数第二个节点的 next 置空 free(current-next); current-next NULL; }这个函数同样需要传入二级指针head因为当链表只有一个节点时删除后头指针本身要变成 NULL这需要修改一级指针本身。函数先判断链表是否为空为空直接返回再判断是否只有一个节点如果是就释放该节点并把头指针置空否则从头遍历找到倒数第二个节点即 next 的 next 为 NULL 的节点释放它的 next 指向的尾节点再把它的 next 置为 NULL这样尾节点就被正确删除并释放了。2.链表调用结束后我们需要手动对这个链表进行释放避免出现内存泄漏。这个模板差不多大多数链表都是大差不差的可根据自己的需求进行更改代码如下// 释放整个链表 void freeList(struct Node *head) { struct Node *current head; while (current ! NULL) { struct Node *next current-next; // 先保存下一个节点的地址 free(current); // 释放当前节点 current next; // 移动到下一个节点 } }这个函数只需要传入一级指针head即可因为我们只是逐个释放节点并不需要修改头指针本身。函数用一个current指针从头开始遍历每次先保存当前节点的下一个节点地址因为释放当前节点后它的 next 就不可用了再释放当前节点然后移动到下一个节点直到链表末尾。这样每个 malloc 创建的节点都被正确释放不会造成内存泄漏。这里我并没有加上 headNULL因为这里也许很多人的需求不同但要注意避免出现野指针双向链表与循环链表以及快慢指针1.双向链表我们发现这个创建的链表里面似乎可以存不止一个指针next 是存的下一个节点的地址我们是不是可以也设置一个名为 last 的指针存放上一个节点的地址这样这个链表就是双向的了具体的实现我就不写出来了需要具体了解的可以直接尝试写一下或者看 AI 给的代码。2.循环链表我们这个链表是线性的类似一根麻绳但我们可以将尾节点的 next 指向头节点这样就构成了首尾闭合的链表类似一个圈这样就是循环链表具体的实现也要看你链表的结构这里也不放代码了。3.快慢指针这里的快慢指针是我们在链表解决某些问题时用到的一个方法。具体是我们设置两个指针一个快一点一个慢一点。比如我们要输出这个链表倒数第二个数打个比方我们可以让快指针先提前比慢指针走两个节点然后开始一步一步走当快指针指向空时两个指针停下来这时慢指针指向的节点就是倒数第二个节点。还比如我们判断一个链表是否是循环链表时我们可以这样设计设置一个 while 循环当慢指针或快指针为空时就停止循环快指针每次循环走两步慢指针走一步。如果是线性的就会走到头就会自己退出循环如果是循环链表我们在这里面设计一个判断如果慢指针等于快指针的话就代表是循环链表因为每次快指针都比慢指针多走一步在循环足够多次时他们会重逢。这是我们的一个技巧代码也就不放出来了。三、总结这次讲解了结构体与链表这部分知识点实现时需要我们理解内存管理和深刻理解指针。实现很多功能时代码会比较长也很容易遗漏一些东西这是基本功的关键。尽量自己动手出现 bug 也别慌这是常态。这部分我推荐多练与理解记忆而非死记硬背。学完这个部分后我们可以实现一些管理系统类似图书管理系统、点餐系统等等可以用这些项目练手。
返回列表