文章目录
🐮前言
-
单链表是一种常见的数据结构,用于存储一系列的数据元素,每个节点包含数据和指向下一个节点的指针。
-
单链表通常用于实现某些算法或数据结构,如链式前向星、哈希表、链式栈、队列等等。
-
单链表在程序设计中的作用不可忽略,是很多基础算法的核心数据结构之一。
-
学习单链表有助于提高算法和数据结构的基本能力并增强编程的实践经验。
-
本篇博客将介绍单链表的基本操作及其算法应用,旨在帮助读者掌握单链表数据结构及相关算法的设计和实现,进一步提高编程的能力和水平。
总之,单链表是一种常见的基础数据结构,与编程和算法设计密切相关,了解和掌握单链表的基本概念和操作,对于提高编程能力和设计程序算法都有帮助。
🍌单链表与顺序表的对比
单链表的优点:
- 动态性:单链表可以在任意位置进行插入和删除操作,不必像顺序表那样需要移动元素,操作比较灵活。
- 空间利用效率高:单链表的结构只需要记录一个指针域和一个数据域,不像顺序表需要预留一定的存储空间,因此空间利用率较高。
单链表的缺点:
- 存储空间分配不灵活:由于单链表的结构决定了只有通过指针才能访问相邻元素,因此不能随机访问。
- 存储密度较低:由于每个结点只包含一个指针域和一个数据域,因此存储密度比较低,会占用更多的空间。
顺序表的优点:
- 存储密度较高:由于所有元素在连续的存储空间中,因此存储密度比较高,可以节省空间。
- 存取效率高:由于所有元素在连续的存储空间中,因此可以通过元素的下标进行随机存取,存取效率比较高。
顺序表的缺点:
- 插入和删除操作较慢:由于需要移动元素,因此插入和删除操作比较慢。
- 维护困难:当顺序表达到一定的长度后,插入和删除操作会变得更加困难,在需要频繁进行插入和删除操作时,顺序表的效率会比较低。
总的来说,单链表和顺序表各有优点和缺点,需要根据具体的应用场景进行选择。如果需要频繁进行插入和删除操作,单链表的效率更高;如果需要频繁进行随机存取操作,顺序表更为适合。
🍊单链表的初始操作及结构体
- 单链表的实现也是需要搭建3个文件,一个是SList.h(头文件),SList.c(各函数的实现文件),Test.c(测试文件)。
- 搭建好之后我们只需要在SList.c和Test.c文件开头包含#include"SList.h"即可将三个文件链接起来。
- 写单链表的结构体时,需要定义包含两个成员的结构体类型,一个成员用于存储结点的数据,另一个成员用于存储下一个结点的指针。一般而言,结构体类型的名称和结构体变量的名称都需要有意义,以便于程序的阅读和理解。
相关功能代码:
#pragma once
// 输入输出所需头文件
#include<stdio.h>
// malloc 所需头文件
#include<stdlib.h>
// assert 所需头文件
#include<assert.h>
// 每个节点的数据的类型
typedef int SLTDataType;
// 每个节点的结构体
typedef struct SListNode
{
// 存放该结点的数据
SLTDataType data;
// 存放下一个节点的地址,将整个结构串联成一个单链表
struct SListNode* next;
}SLTNode; // 用 typedef 将结构体重命名为 SLTNode 便于后续写程序
🍉申请一个节点
-
单链表的节点是单链表中最基本的单位。每个节点由两部分组成,分别是数据域和指针域。数据域存储节点的数据或者信息,而指针域指向下一个节点的地址。因此,单链表是由一连串的节点构成的数据结构。具体来说,单链表的节点通常包含以下信息:
-
1.数据域:存储节点的数据或信息,可以是任何数据类型,例如整数、字符、字符串、结构体等。
-
2.指针域:指向下一个节点的地址。在C语言中可以使用结构体来表示一个节点,节点结构体至少包含一个数据项和一个指向下一个节点的指针。
单链表的节点是非常灵活和可扩展的,因为每个节点都只需要存储自身的信息和指向下一个节点的指针就可以了,这就使得单链表更易于被实现和扩展。当需要插入或删除节点时,只需更新相应节点的指针域即可。
同时,这种设计也带来了一定的缺点,即单链表不能直接访问任何一个节点的前一个节点。
- 节点应至少包括一个数据项(例如整数或字符串)和一个指向下一个节点的指针。
- 申请内存要创建一个新的节点,你需要使用C语言的stdlib.h库中的malloc函数来为其分配内存。将分配一个大小为SListNode结构体的内存块,并将其指针指向newnode。在这个节点被使用完毕之后,应该使用free函数释放它的内存。
- 经过内存分配后,你需要用正确的值初始化节点。在这种情况下,节点的值可以作为函数的参数进行传递。初始化一个新的节点并返回这个节点的指针。
- 我们首先为节点分配内存,然后将value赋值给节点的data字段,并把next设为NULL(因为这个节点尚未链接到另一个节点上)。
这样,我们就成功地创建了一个新的节点,并初始化了它的数据元素和指向下一个节点的指针。
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
🥕打印
- 单链表的打印操作是指遍历单链表并输出其中所有节点的数据或信息,这是单链表中常用的一种操作,也是单链表中尤为基础的一部分。
- 定义一个指针变量,指向单链表的头节点,命名为cur。其中,head为单链表头节点的指针。
- 使用while循环遍历整个单链表,直到当前的指针变量为空。
- 在循环中,使用p指针变量访问当前节点(即p所指向的节点),并打印其数据或信息。在实际操作中,可以根据需要定制输出样式。
- 将指针变量p更新为下一个节点的地址,即p = p->next,继续访问下一个节点。直到循环结束,所有的节点都被遍历,并输出了其中所有的数据或信息。
相关功能代码:
void SLPrint(SLTNode* phead)
{
struct Node* cur = phead; // 定义指针变量,指向头节点
while(cur != NULL){ // 循环条件
printf("%d ", cur->val); // 输出节点数据或信息
cur = cur->next; // 修改指针变量,指向下一个节点
}
}
需要注意的是,单链表的打印操作在多种算法或数据结构中都有应用。
同时,其中包含单个强迫离散节点间转移到下一个节点的元素。其执行步骤、方式及相关内容会因场景而异,需要结合实际情况进行设计。
🍓销毁
- 单链表的销毁是指将单链表中的所有节点都删除,并将链表中的所有内存空间释放。
- 具体步骤:
- 1.定义一个指针变量pos来保存待删除节点的下一个节点。
- 2.使用while循环遍历整个单链表,直到当前指针plist为空。
- 3.在循环中,保存当前节点的下一个节点的指针,即pos = pos->next。
- 4.使用free函数释放当前节点的内存空间,即free(head),释放节点所占用的内存空间。
- 5.将指针head指向下一个节点。即plist = pos。
重复步骤3-5,直到整个单链表中的所有节点都被删除并释放了内存空间。最后在返回。 - 需要注意的是,有指针就别忘了要assert。
- 需要注意的是,在进行单链表的尾插操作时,需要保证单链表不为空。可以在函数外创建一个头节点,排除空链表情况,并将其传入SLPushBack函数中进行操作。另外,为新节点分配内存空间之后,也需要对内存申请情况进行检查,以避免发生内存泄漏等问题。
相关程序代码:
void SLDestory(SLTNode* plist,SLTNode* pos)
{
assert(plist);
while (pos)
{
plist = pos;
pos = pos->next;
free(plist);
plist->next = NULL;
}
return;
}
还需要注意的是,在使用上述函数时,需要首先保证单链表中所有节点都已经被访问或使用,否则可能会造成内存泄漏。
🍎尾插
- 单链表尾插是单链表中的一种常用操作,而且非常实用,它可以在单链表的末尾插入一个新节点。
- 找到单链表的尾部节点,即包含指针域值为NULL的节点。可以用一个循环来找到它。
- 创建一个新节点,将其数据放入其中。需要调用BuyLTNode函数为新节点分配内存空间。
- 将新节点连接到单链表的尾部节点上,即将尾部节点的指针域指向新节点。至此,新的节点已经成功插入到了单链表的末尾。
- 为新节点分配内存空间之后,也需要对内存申请情况进行检查(assert),以避免发生内存泄漏等问题。
相关程序代码:
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyLTNode(x);
assert(*pphead);
//1,空链表
//2,非空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
🍐尾删
- 单链表尾删是指删除单链表中的尾节点,使其前一个节点成为新的尾节点。
- 找到单链表的倒数第二个节点。可以使用一个循环遍历直到找到该节点。注意,需要判断单链表中是否有节点,否则删除操作无法进行
- 保存单链表尾节点的指针,以便后续释放它所占用的内存空间。
- 将尾节点从单链表中删除,即将p节点的指针域设置为NULL。
- 释放单链表尾节点占用的内存空间。
相关程序代码:
void SLPopBack(SLTNode** pphead)
{
//assert(pphead); //链表为空,pphead也不能为空,因为他是头指针plist的地址
assert(*pphead); //链表为空,不能头删
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
//找尾
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
需要注意的是,在进行单链表的尾删操作时,需要先对单链表进行判空处理,避免出现访问空指针的错误。
🍇头插
- 单链表头插是指在单链表的头部插入一个新的节点,使其成为新的头节点。
- 创建一个新的节点,并将新节点的指针域指向原来的头节点。需要调用BuyLTNode函数为新节点分配内存空间。
- 将头指针指向新节点,即将头指针修改为新节点的地址。
相关程序代码:
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
需要注意的是,在进行单链表的头插操作时,需要先对头指针进行检查,避免出现空指针的错误。此外,在新建节点并为其分配内存空间时,也需要进行内存申请情况的检查,以避免出现内存泄漏等问题。
🍑头删
- 单链表头删是指删除单链表中的头节点,使其后一个节点成为新的头节点。
- 保存单链表头节点的指针,以便后续释放它所占用的内存空间。
- 释放原来的头节点所占用的内存空间。
相关程序代码:
void SLPopFront(SLTNode** pphead)
{
//assert(pphead); //链表为空,pphead也不能为空,因为他是头指针plist的地址
assert(*pphead); //链表为空,不能头删。
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
}
需要注意的是,在进行单链表的头删操作时,需要先对头指针进行检查,避免出现空指针的错误。此外,在释放原来的头节点所占用的内存空间时,也需要进行内存申请情况的检查,以避免出现内存泄漏等问题。
🍍数据的查找
- 在单链表中查找数据通常需要遍历整个链表,找到匹配的节点并返回其位置或指针。
- 使用一个指针指向链表的头结点,依次遍历链表中的每一个节点。
- 在每个节点中,检查该节点中存储的元素是否与待查找元素相同。如果相同,则返回该节点的位置或指针。
- 如果遍历整个链表都找不到相匹配的节点,则返回NULL表示查找失败。
相关程序代码:
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
需要注意的是,在进行单链表中数据查找操作时,需要先对单链表进行判空处理,避免出现访问空指针的错误。
此外,还需注意的是,本算法只能查找链表中第一个匹配的元素,无法查找所有匹配的元素。
🍋数据的修改
- 在单链表中修改指定节点的数据,通常需要先通过遍历找到该节点,之后直接修改其存储的数据。
- 使用一个指针指向链表的头结点,依次遍历链表中的每一个节点。
- 在每个节点中,检查该节点中存储的元素是否与待修改元素相同。如果相同,则直接修改该节点中存储的数据。
- 如果遍历整个链表都找不到匹配的节点,则返回NULL表示操作失败。
相关程序代码:
SLTNode* SLModify(SLTNode* phead, int target, int newdata)
{
SLTNode* cur = phead;
while (cur != NULL) {
if (cur->data == target) {
cur->data = newdata;
return cur;
}
cur = cur->next;
}
return NULL;
}
需要注意的是,在进行单链表中数据修改操作时,需要先对单链表进行判空处理,避免出现访问空指针的错误。此外,本算法只能修改链表中第一个匹配的元素,无法修改所有匹配的元素。
🍅在pos位置之后插入节点
- 首先对pos进行判空操作。
- 创建一个新节点 newnode,调用BuyLTNode函数。
- 用newnode的next指向pos位置的next,在用pos的next指向newnode。
相关程序代码:
//在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
🥔在pos位置之前插入节点
- 在pos位置之前插入,因为单链表具有单向性,因此这里需要遍历单链表找到pos位置的前一个节点,才能进行插入。
- 但是你会发现,如果是pos刚好指向头结点就可以直接用头插。
- 因为这里调用了头插,所以这里也要使用二级指针来操作。
相关程序代码:
//在pos之前插入
void SLInsertFront(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pphead);
assert(pos);
//如果pos为第一个节点,直接调用前面的头插
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
🐱删除pos位置之后的节点
- 找到要删除节点的位置。
- 将要删除节点的 next 指向NULL。
- 找到前一个节点,将其next指向NULL。
- 释放要删除节点及其后继节点内存空间。
相关程序代码:
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
🐶删除pos位置之前的节点
- 首先断言(assert),然后再调用头删(SLPopFront).
- 之后再将 prev 的 next 指向要删除节点的next。
- 释放要删除节点的内存空间。
- 释放要删除及其前面的节点。
相关程序代码:
void SLEraseFront(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
🐒完整代码
SList.h
#pragma once
// 输入输出所需头文件
#include<stdio.h>
// malloc 所需头文件
#include<stdlib.h>
// assert 所需头文件
#include<assert.h>
// 每个节点的数据的类型
typedef int SLTDataType;
// 每个节点的结构体
typedef struct SListNode
{
// 存放该结点的数据
SLTDataType data;
// 存放下一个节点的地址,将整个结构串联成一个单链表
struct SListNode* next;
}SLTNode; // 用 typedef 将结构体重命名为 SLTNode 便于后续写程序
//申请一个节点(包含初始化)
SLTNode* BuyLTNode(SLTDataType x);
//单链表的打印
void SLTPrint(SLTNode* phead);
//头插节点
void SLPushFront(SLTNode** pphead, SLTDataType x);
//尾插节点
void SLPushBack(SLTNode** pphead, SLTDataType x);
//头删节点
void SLPopFront(SLTNode** pphead);
//尾删节点
void SLPopBack(SLTNode** pphead);
//节点的查找
SLTNode* SLFind(SLTNode* phead, SLTDataType x);
//节点的修改
SLTNode* SLModify(SLTNode* phead, int target, int newdata);
//pos位置之前插入节点
void SLInsertFront(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//pos位置之后插入节点
void SLInsertAfter(SLTNode* pos, SLTDataType x);
//pos位置之前删除节点
void SLEraseFront(SLTNode** pphead, SLTNode* pos);
//pos位置之后删除节点
void SLEraseAfter(SLTNode* pos);
//节点的销毁
void SLDestory(SLTNode* plist, SLTNode* pos);
SList.c
#include"SList.h"
//单链表的打印
void SLPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//申请一个节点(包含初始化)
SLTNode* BuyLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//头插节点
void SLPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾插节点
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyLTNode(x);
assert(*pphead);
//1,空链表
//2,非空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头删节点
void SLPopFront(SLTNode** pphead)
{
//assert(pphead); //链表为空,pphead也不能为空,因为他是头指针plist的地址
assert(*pphead); //链表为空,不能头删。
SLTNode* del = *pphead;
*pphead = (*pphead)->next;
free(del);
}
//尾删节点
void SLPopBack(SLTNode** pphead)
{
//assert(pphead); //链表为空,pphead也不能为空,因为他是头指针plist的地址
assert(*pphead); //链表为空,不能头删
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
//找尾
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//查找单链表的节点
SLTNode* SLFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//修改单链表的节点
SLTNode* SLModify(SLTNode* phead, int target, int newdata)
{
SLTNode* cur = phead;
while (cur != NULL) {
if (cur->data == target) {
cur->data = newdata;
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos之前插入节点
void SLInsertFront(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//在pos之后插入节点
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos位置之前的节点
void SLEraseFront(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
//删除pos位置之后的节点
void SLEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
//单链表的销毁
void SLDestory(SLTNode* plist,SLTNode* pos)
{
assert(plist);
while (pos)
{
plist = pos;
pos = pos->next;
free(plist);
plist->next = NULL;
}
return;
}
Test,c
#include"SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLPushFront(&plist, 5);
SLPrint(plist);
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
SLPrint(plist);
SLPushBack(&plist, 5);
SLPrint(plist);
SLPopBack(&plist, 1);
SLPopBack(&plist, 2);
SLPopBack(&plist, 3);
SLPopBack(&plist, 4);
SLPopBack(&plist, 5);
SLPrint(plist);
SLPopFront(&plist, 1);
SLPopFront(&plist, 2);
SLPopFront(&plist, 3);
SLPopFront(&plist, 4);
SLPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLPushFront(&plist, 5);
SLPushFront(&plist, 6);
SLPushFront(&plist, 7);
SLPrint(plist);
SLModify(plist, 1, 999);
SLPrint(plist);
SLModify(plist, 2, 999);
SLPrint(plist);
SLModify(plist, 3, 999);
SLPrint(plist);
SLModify(plist, 4, 999);
SLPrint(plist);
SLModify(plist, 5, 999);
SLPrint(plist);
SLModify(plist, 6, 999);
SLPrint(plist);
SLModify(plist, 7, 999);
SLPrint(plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SLPushFront(&plist, 1);
SLPushFront(&plist, 2);
SLPushFront(&plist, 3);
SLPushFront(&plist, 4);
SLPushFront(&plist, 5);
SLPushFront(&plist, 6);
SLPushFront(&plist, 7);
SLPrint(plist);
SLFind(&plist, 3);
SLPrint(plist);
}
int main()
{
//TestSList1();
//TestSList2();
TestSList3();
return 0;
}
🐘写在最后
单链表是一种与数组相对的数据结构,在实际的开发中很常见。它虽然不能像数组那样随机访问,但它具有动态的特点,可以高效地进行插入和删除操作。了解单链表的实现方式和使用方法,有助于我们在实际开发中更加灵活地运用它。
在本篇博客中,我们介绍了单链表的基本知识,包括如何在单链表中插入节点和删除节点。
需要注意的是,在单链表操作的过程中,对指针的操作十分关键,需要谨慎地操作。
感谢各位阅读本小白的博客,希望能帮助到大家!也请大家严厉指出并纠正我在文章中的错误。😄