目录
在C语言中,链表是一种非常重要且常用的数据结构,它可以动态地存储和管理数据。链表由一系列节点组成,每个节点包含数据和一个指向下一个节点的指针。相比于数组,链表的大小可以动态地增长或缩小,这使得链表在很多场景下更加灵活和有效。
链表的基本概念
首先我们来看一张图
我们看到这个小火车,每一节车厢都有自己的内容和车厢号,我们在一个车厢里面可以访问到这个车厢里的内容,同时也可以知道下一个车箱子号。例如当我们想要去在这里面插入车厢的时候也只需要让前一个车厢记住这个车厢号,同时让这个车厢记住下一个车厢号就可以了。链表也是这种道理。
链表由节点组成,每个节点包含两部分:数据和指向下一个节点的指针。在C语言中,可以通过结构体来定义链表节点,例如:
struct Node {
int data;//这个车厢里的内容
struct Node* next;//下一个车厢的车厢号
};
接下来我们来看具体的链表的实现
链表各功能的实现
链表的基本操作包括插入、删除、查找等。下面分别介绍这些操作:
链表的尾插:![](https://img-blog.csdnimg.cn/direct/47db91067528456ba876316765b50bcb.png)
代码:
void SLTpushback(SLTNode** pphead, SLData x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
//非空链表
else
{
//找尾
SLTNode* ptail = *pphead;
while (ptail->next!=NULL)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
在尾插的代码中,因为我们尾插的节点可能作为首节点,也就是首节点phead这个指针的指向可能发生改变因此改变这个指针的指向我们就穿这个指针的地址&phead,因为我们在主函数定义的头节点phead本身就是一个指针所以&phead取出了一级指针的地址我们就使用二级指针来接收。并且后面涉及对pphead的解引用因此pphead这个二级指针不能传空,使用·assert断言。
接下来我们创建一个新的节点newnode来作为我们要插入的节点,因为在后面的函数中我们需要多次使用要插入的节点,因此我们把创建节点的功能定义为一个函数以方便我们后续的使用。因为节点是用作尾插因此我们把创建的节点next指针置为空。
SLTNode* SLTBuyNode(SLData x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail\n");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
接下来在创建节点成功后,我们需要判断这个链表是否为空链表,如果为空链表我们把创建的要尾插的节点作为首节点。如果不为空我们就需要找到尾部节点,为了不改变首节点的指向我们定义一个pucr指针来进行找尾操作。通过判断节点的下一个节点是否为NULL的方式我们就可以找到最后一个节点。但找到最后一个节点时候,我们让最后这个节点的next指针指向newnode这样newnode就变成了最后的那个节点。
头插:
代码:
void SLTpushfront(SLTNode** pphead, SLData x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头删:
因为我们要让plist指向新的头节点,所以我们先要通过原本的头节点访问到下一个节点并让plist指向新的头节点后才能释放掉原本的头节点 。
代码:
void SLTPopfront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
要删除所以这个链表不能为空。但链表中只有一个元素时,新节点为NULL即链表为空可以实现。
尾删:
再判断以一下如果只有一个节点是什么情况呢?
很显然plist没有next节点无法把next置空,那么特殊情况下(判断只有一个节点)我们直接释放掉这个节点然后返回plist为空。
代码:
void SLTPopback(SLTNode** pphead)
{
assert(*pphead && pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
assert(*pphead && pphead);
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
查找函数:
通过遍历判断节点是否为我们想要查找的值即可,如果是就找到了可以返回地址,如果遍历完都没有找到就是没找到返回NULL。
代码:
SLTNode* SLTFind(SLTNode* phead, SLData x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
指定位置前插入:
一样的特殊情况下当我们插入1节点之前时plist没有next,但是此时就相当于头插,判断是否是1节点之前并调用头插函数即可。
代码:
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLData x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//若pos == *pphead;说明是头插
if (pos == *pphead)
{
SLTpushfront(pphead, x);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev -> newnode -> pos
newnode->next = pos;
prev->next = newnode;
}
}
指定节点之后插入:
和指定位置之前插入差不多:
代码:
void STLInsterAfter(SLTNode** pphead, SLTNode* pos, SLData x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos -> newnode -> pos->next
newnode->next = pos->next;
pos->next = newnode;
}
指定节点删除:
一样的特殊情况下删除1节点plist没有next但就相当于头删所以调用头删即可。
代码:
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//pos是头结点/pos不是头结点
if (pos == *pphead)
{
//头删
SLTPopfront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
指定位置后删除:
代码:
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
//pos del del->next
pos->next = del->next;
free(del);
del = NULL;
}
链表的销毁:
通过头节点一边遍历一遍销毁节点,最后销毁头节点。再销毁一个节点时需要一个指针记住下一个节点地址防止找不到下有一个节点即可。
代码:
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
链表的优缺点
链表的优点包括:
- 动态性:链表的大小可以动态地增长或缩小,不像数组有固定的大小限制。
- 插入和删除效率高:在链表中插入或删除节点的效率很高,只需要调整指针指向即可。
链表的缺点包括:
- 随机访问效率低:链表中要访问某个特定位置的节点,需要从头开始遍历,效率较低。
- 需要额外的存储空间:每个节点都需要额外的指针空间,会占用一定的存储空间。
源代码:
SList.h
#pragma once
#define CAT_SECURE_NO_WARINGS
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLData;
typedef struct SLTNode
{
SLData data;
struct SLTNode* next;
}SLTNode;
void SLTPrint(SLTNode* phead);
void SLTpushback(SLTNode** pphead, SLData x);
void SLTpushfront(SLTNode** pphead, SLData x);
void SLTPopback(SLTNode** pphead);
void SLTPopfront(SLTNode** pphead);
SLTNode* SLTFind(SLTNode* phead, SLData x);
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLData x);
void STLInsterAfter(SLTNode** pphead, SLTNode* pos, SLData x);
void SLTPopfront(SLTNode** pphead);
void SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTEraseAfter(SLTNode* pos);
void SListDesTroy(SLTNode** pphead);
SList.c
#include "SList.h"
//*********************************************//
// 打印 //
//*********************************************//
void SLTPrint(SLTNode* phead)
{
SLTNode* pure = phead;
while (pure)
{
printf("%d->", pure->data);
pure = pure->next;
}
printf("NULL\n");
}
//*********************************************//
// 创建节点 //
//*********************************************//
SLTNode* SLTBuyNode(SLData x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail\n");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//*********************************************//
// 尾插 //
//*********************************************//
void SLTpushback(SLTNode** pphead, SLData x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//空链表和非空
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾
SLTNode* ptail = *pphead;
while (ptail->next!=NULL)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//*********************************************//
// 头插 //
//*********************************************//
void SLTpushfront(SLTNode** pphead, SLData x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//*********************************************//
// 尾删 //
//*********************************************//
void SLTPopback(SLTNode** pphead)
{
assert(*pphead && pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
assert(*pphead && pphead);
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
//*********************************************//
// 头删 //
//*********************************************//
void SLTPopfront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//*********************************************//
// 查找 //
//*********************************************//
SLTNode* SLTFind(SLTNode* phead, SLData x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//*********************************************//
// 指定位置前插入 //
//*********************************************//
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLData x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//若pos == *pphead;说明是头插
if (pos == *pphead)
{
SLTpushfront(pphead, x);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev -> newnode -> pos
newnode->next = pos;
prev->next = newnode;
}
}
//*********************************************//
// 指定位置后插入 //
//*********************************************//
void STLInsterAfter(SLTNode** pphead, SLTNode* pos, SLData x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos -> newnode -> pos->next
newnode->next = pos->next;
pos->next = newnode;
}
//*********************************************//
// 删指定节点 //
//*********************************************//
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
//pos是头结点/pos不是头结点
if (pos == *pphead)
{
//头删
SLTPopfront(pphead);
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//*********************************************//
// 删指定后节点 //
//*********************************************//
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
SLTNode* del = pos->next;
//pos del del->next
pos->next = del->next;
free(del);
del = NULL;
}
//*********************************************//
// 链表的销毁 //
//*********************************************//
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
test.c
#include "SList.h"
void SListTest02()
{
SLTNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTpushfront(&plist, 0);
printf("ago\n");
SLTPrint(plist);
SLTPopback(&plist);
SLTPopfront(&plist);
printf("now\n");
SLTPrint(plist);
printf("查找结果...");
SLTNode* p = SLTFind(plist,2);
if (p == NULL)
{
printf("没找到\n");
}
else printf("有%d\n", p->data);
printf("在2前插入4\n");
SLTInsert(&plist, p,4 );
SLTPrint(plist);
printf("在2后插入0\n");
STLInsterAfter(&plist, p, 0);
SLTPrint(plist);
printf("删2\n");
SLTErase(&plist, p);
SLTPrint(plist);
SListDesTroy(&plist);
}
int main()
{
SListTest02();
return 0;
}