序言:
本篇博客主要介绍单链表的基本概念,包括如何定义和初始化单链表,以及如何进行数据的插入,删除和销毁等操作。
1.单链表
1.1 概念与结构
概念:链表是一种非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。物理存储结构上非连续,
举例来说,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加⼏节。只需要将火车里的某节车厢去掉/ 加上,不会影响其他车厢,每节车厢都是独立存在的。
类比于链表,每节“车厢”是什么样的呢?
1.1.1 结点
与顺序表不同的是,链表里的每节“车厢”都是独立申请下来的空间,我们称之为“结点”。
结点的组成主要是有两个部分:当前结点要保存的数据和保存下一个结点的地址(指针变量)。
图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望 plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针 变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
1.1.2 链表的性质
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合学到的结构体知识,我们可以给出每个结点对应的结构体代码:
假设当前保存的结点为整型:
struct SListNode
{
int data; //结点数据
struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也要保存下一个结点的地址(当下一个结点为空时保存的地址为空)。
当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下一个结点的地址就可以了。
1.1.3 链表的打印
给定的链表结构,如何实现结点从头到尾的打印?
1.2 实现单链表
1.2.1 头文件的包含
#pragma once
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
//定义链表的结构
//定义节点的结构
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;//指向下一个节点的指针
}SLTNode;
//typedef struct SListNode SLTNode;
//phead:头(首)节点
void SLTPrint(SLTNode* phead);
//尾插
void SLTPushBack(SLTNode** phead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** phead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** phead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);
1.2.2 打印单链表
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
打印单链表逻辑:
这段 C 语言代码定义了一个名为`SLTPrint`的函数,用于打印一个链表的内容。
函数的参数是一个指向链表头节点的指针`phead`。在函数内部,定义了一个指针`pcur`并初始化为`phead`,然后通过一个循环遍历链表。在循环中,使用`printf`函数输出当前节点的数据,并将指针`pcur`指向下一个节点,直到`pcur`为`NULL`,表示链表遍历结束。最后,输出`NULL`表示链表结束的标志。
1.2.3 向操作系统申请一个新结点
//向操作系统申请一个新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTBuyNode
的函数,用于向操作系统申请一个新的链表节点。函数的参数是一个
SLTDataType
类型的变量x
,用于指定新节点的数据。在函数内部,使用malloc
函数分配一个SLTNode
类型大小的内存空间,并将其地址赋给指针newnode
。然后,通过判断newnode
是否为NULL
来检查内存分配是否成功。如果分配失败,使用perror
函数输出错误信息,并使用exit
函数终止程序运行。如果分配成功,将参数x
赋值给新节点的data
成员,并将新节点的next
成员初始化为NULL
。最后,函数返回新节点的指针。
1.2.4 尾插
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = SLTBuyNode(x);
//链表为空,phead直接指向newnode节点
if (*pphead == NULL)
{
*pphead = newnode;
}
else {
//链表不为空,找尾节点,将尾节点和新节点连接起来
SLTNode* ptail = *pphead;
while (ptail->next)//等价于ptail->next != NULL
{
ptail = ptail->next;
}
//ptail newnode
ptail->next = newnode;
}
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTPushBack
的函数,用于在链表的尾部插入一个新节点。函数的参数是一个指向链表头节点指针的指针
pphead
和一个SLTDataType
类型的变量x
。在函数内部,首先调用SLTBuyNode
函数创建一个新节点newnode
,并将x
作为其数据。然后,通过判断
*pphead
是否为NULL
来确定链表是否为空。如果链表为空,将*pphead
直接指向newnode
。如果链表不为空,则通过一个循环找到链表的尾节点
ptail
。在循环中,只要ptail->next
不为NULL
,就将ptail
指向下一个节点。当循环结束时,ptail
就指向了尾节点。最后,将ptail
的next
指针指向新节点newnode
,完成尾部插入操作。
1.2.5 头插
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//newnode *pphead
newnode->next = *pphead;
*pphead = newnode;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTPushFront
的函数,用于在链表的头部插入一个新节点。函数的参数是一个指向链表头节点指针的指针
pphead
和一个SLTDataType
类型的变量x
。在函数内部,首先使用assert
函数检查pphead
是否有效。然后,调用SLTBuyNode
函数创建一个新节点newnode
,并将x
作为其数据。接下来,将新节点的
next
指针指向当前的头节点(即*pphead
),然后将*pphead
更新为新节点newnode
,从而实现了在链表头部插入新节点的操作。
1.2.6 尾删
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//只有一个结点的时候
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* ptail = &pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
//prev tail
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTPopBack
的函数,用于删除链表的尾节点。函数的参数是一个指向链表头节点指针的指针
pphead
。在函数内部,首先使用assert
函数检查pphead
是否有效以及链表是否不为空。如果链表只有一个节点,那么直接释放该节点的内存,并将
*pphead
置为NULL
。如果链表有多个节点,那么通过一个循环找到尾节点
ptail
和其前一个节点prev
。在循环中,只要ptail->next
不为NULL
,就将prev
更新为ptail
,并将ptail
指向下一个节点。当循环结束时,ptail
就指向了尾节点,prev
指向尾节点的前一个节点。然后,将prev->next
置为NULL
,断开与尾节点的连接,释放尾节点的内存,并将ptail
置为NULL
。
1.2.7 头删
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTPopFront
的函数,用于删除链表的头节点。函数的参数是一个指向链表头节点指针的指针
pphead
。在函数内部,首先使用assert
函数检查pphead
是否有效以及链表是否不为空。然后,通过
(*pphead)->next
获取头节点的下一个节点,并将其赋给next
指针。接着,释放头节点的内存空间,最后将*pphead
指向next
,完成头节点的删除操作。
1.2.8 查找
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTFind
的函数,用于在链表中查找指定值的节点。函数的参数是一个指向链表头节点的指针
phead
和一个SLTDataType
类型的变量x
,表示要查找的值。在函数内部,定义一个指针pcur
并初始化为phead
,然后通过一个循环遍历链表。在循环中,检查当前节点的data
值是否等于要查找的值x
。如果相等,就返回当前节点的指针;如果不相等,就将pcur
指向下一个节点,继续循环。如果循环结束后都没有找到匹配的节点,就返回NULL
,表示未找到。
1.2.9 在指定位置之前插入数据
//在指定位置之前插入数据
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
//pos就是头结点
if(pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
else {
SLTNode* newnode = SLTBuyNode(x);
//pos在头结点之后--->找pos前驱节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev newnode pos
newnode->next = pos;
prev->next = newnode;
}
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLInsert
的函数,用于在链表的指定位置之前插入一个新节点。函数的参数是一个指向链表头节点指针的指针
pphead
、一个指向要插入位置的节点指针pos
以及一个SLTDataType
类型的变量x
,表示要插入的数据。在函数内部,首先使用
assert
函数检查pphead
和pos
是否有效。如果
pos
就是头节点,那么就调用SLTPushFront
函数进行头插操作。如果
pos
在头节点之后,那么就调用SLTBuyNode
函数创建一个新节点newnode
,并通过一个循环找到pos
的前驱节点prev
。在循环中,只要prev->next
不等于pos
,就将prev
指向下一个节点。当循环结束时,prev
就指向了pos
的前驱节点。然后,将新节点的next
指针指向pos
,将prev
的next
指针指向新节点,完成在指定位置之前插入新节点的操作。
1.2.10 在指定位置之后插入数据
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
pos->next = newnode;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTInsertAfter
的函数,用于在链表的指定位置之后插入一个新节点。函数的参数是一个指向指定位置的节点指针
pos
和一个SLTDataType
类型的变量x
,表示要插入的数据。在函数内部,首先使用
assert
函数检查pos
是否有效。然后,调用SLTBuyNode
函数创建一个新节点newnode
,并将x
作为其数据。接下来,将新节点的
next
指针指向pos
的下一个节点,然后将pos
的next
指针指向新节点,完成在指定位置之后插入新节点的操作。
1.2.11 删除pos结点
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//要删除的结点刚好就是头结点---头删
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else {
//prev
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTErase
的函数,用于删除链表中的指定节点。函数的参数是一个指向链表头节点指针的指针
pphead
和一个指向要删除节点的指针pos
。在函数内部,首先使用
assert
函数检查pphead
和pos
是否有效。如果要删除的节点恰好是头节点,那么就调用
SLTPopFront
函数进行头删操作。否则,通过一个循环找到要删除节点的前驱节点
prev
。在循环中,只要prev->next
不等于pos
,就将prev
指向下一个节点。当循环结束时,prev
就指向了要删除节点的前驱节点。然后,将prev->next
指向pos
的下一个节点,释放pos
所占用的内存空间,并将pos
置为NULL
,完成删除节点的操作。
1.2.11 删除pos结点之后的数据
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
//pos del del->next
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
逻辑概述:
这段 C 语言代码定义了一个名为
SLTEraseAfter
的函数,用于删除链表中指定节点pos
之后的节点。函数的参数是一个指向指定节点的指针
pos
。在函数内部,首先使用assert
函数检查pos
以及pos->next
是否有效。然后,定义一个指针
del
并将其指向pos
的下一个节点。接着,将pos
的next
指针指向del
的下一个节点。之后,释放del
所占用的内存空间,并将del
置为NULL
,完成删除pos
之后节点的操作。
1.2.12 销毁链表
//销毁链表
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
1.3 源码
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//向操作系统申请一个新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = SLTBuyNode(x);
//链表为空,phead直接指向newnode节点
if (*pphead == NULL)
{
*pphead = newnode;
}
else {
//链表不为空,找尾节点,将尾节点和新节点连接起来
SLTNode* ptail = *pphead;
while (ptail->next)//等价于ptail->next != NULL
{
ptail = ptail->next;
}
//ptail newnode
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//newnode *pphead
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//只有一个结点的时候
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* ptail = &pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
//prev tail
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//未找到
return NULL;
}
//在指定位置之前插入数据
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
//pos就是头结点
if(pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
else {
SLTNode* newnode = SLTBuyNode(x);
//pos在头结点之后--->找pos前驱节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev newnode pos
newnode->next = pos;
prev->next = newnode;
}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//pos newnode pos->next
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//要删除的结点刚好就是头结点---头删
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else {
//prev
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//prev pos pos->next
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);
//pos del del->next
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
2. 主函数
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
//手动构造一个链表
//并且打印链表
void test01()
{
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
node2->data = 2;
node3->data = 3;
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
//打印链表
SLTNode* plist = node1;
SLTPrint(plist);
}
void test02()
{
SLTNode* plist = NULL;
//往空链表中插入节点
SLTPushBack(&plist, 1);
SLTPrint(plist);
SLTPushBack(&plist, 2);
SLTPrint(plist);
SLTPushBack(&plist, 3);
SLTPrint(plist);
SLTPushBack(&plist, 4);
SLTPrint(plist);
}
int main()
{
//test01();
test02();
return 0;
}
3. 小结
以上便是本篇博客的所有内容,主要是关于单链表的增,删,查,改的操作,如果这篇博客对诸君有所帮助,还请点点赞。