🤡博客主页:醉竺
🥰本文专栏:《数据结构与算法》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨
目录
一.线性表的链式表示
链表的介绍:
与顺序表相同,链表也是一种线性表,它的数据的逻辑组织形式是一维的。而与顺序表不同的是,链表的物理存储结构是用一组地址任意的存储单元存储数据的。也就是说,它不像顺序表那样占据一段连续的内存空间,而是将存储单元分散在内存的任意地址上。在链表结构中,每个数据元素记录都存放到链表的一个结点 (node)中,而每个结点之间由指针将其连接在一起,这样就形成了一条如同“链”的结构。
1.1 引言
在C程序中,链表的每个结点可以是一个结构体类型元素,当然也可以是其他的构造类型元素。在链表的每个结点中,都必须有一个专门用来存放指针(地址)的域,用这个指针域来存放后继结点的地址,这样就达到了连接后继结点的目的。此外,一条链表的最后一个结点的指针域要置空,表示该结点为链表的尾结点,因为它没有后继结点了。
单向链表(SinglyLinked List)是列表中最常用的一种,它就像火车,所有节点串成一列而且指针所指的方向一样。也就是链表表中每个数据除了要存储原本的数据,还必须存储下一个数据的存储地址。
1.2 定义
单链表是通过一组任意的存储单元(不要求连续)来存储线性表中的数据元素,对每个元素除了存放自身信息之外,还需要存放一个指向其后继的指针。其中data为数据域,存放本结点的数据元素;next为指针域,存放其后继结点的地址。
1.3 单链表的存储结构图示
单链表结点存储结构示意图
单链表存储结构逻辑图
单链表的存储结构物理图 (重要)
从上述图片可以看出单链表有以下特征:
1.每一个结点包括两部分:数据域和指针域。其中数据域用来存放数据元素本身的信息,指针域用来存放后继结点的地址。
2.单链表逻辑上是连续的,而物理上并不一定连续存储结点。
3.单链表是非随机存取的存储结构,查找某个结点时需要从表头遍历,只要获得链表的第一个结点,就可以通过头指针遍历整条链表。
4.解决了顺序表插删需要大量移动元素的缺点。
5.最后一个结点的next指向“空”(通常用NULL或“^”符号表示)。
二.单链表的基本操作 (代码)
2.1 单链表的准备工作
2.1.1 结点的存储结构
以C语言结构体类型来储存单链表结点。
typedef int SLTDateType;
//定义单链表结点的存储结构
typedef struct SListNode
{
SLTDateType data; //数据域
struct SListNode* next; //指针域
}SListNode;
2.1.2 开辟新结点
把“开辟新结点”方法封装起来,省去后续其它操作方法中再开辟新结点的重复冗余操作。
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc failed");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
2.1.3 遍历打印
// 单链表打印
void SListTPrint(SListNode* phead)
{
SListNode* cur = phead; //定义一个cur指针从头结点开始往后遍历
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next; //cur指针往后移动
}
printf("NULL\n");
}
2.2 单链表的插入
2.2.1 头插
分解过程:
原理:
1. 从一个空表开始,先生成一个新结点 ,将读取到的数据X存入新结点的数据域data中,并用指针newnode指向该新结点。
2. 将新结点的指针域next指向当前链表的表头,即把新结点插入到表头。
3. 再把头指针移动(指向)到新结点上即可。
注:采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点插入的时间为 O(1),设单链表长为n,则总时间复杂度为 O(n)。
头插法建立单链表:读入数据的顺序与生成的链表中元素的顺序是相反的。
// 单链表头插
void SListPushFront(SListNode** pphead, SLTDateType x)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
//assert(*pphead); //*pphead不能断言,链表为空也需要能插入
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
// 单链表头插时,链表是否为空不影响头插,操作是统一的
/*
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc failed");
return;
}
newnode->data = x;
newnode->next = NULL;
*/
}
函数形参是二级指针解释:
头插法的操作过程中是需要改变头指针head的,因为每插入一个新结点,head最后就会指向新结点。如果函数形参是一级指针phead的话,当头指针实参head传给形参phead后,形参phead只是实参上数值的一份拷贝,当形参头指针phead移动且指向的内容更改时,真正的头指针实参head的内容并没有改变。所以必须用二级指针pphead,实参应传入头指针head的地址即&head才符合要求;如果是要改变结构体的指向,即改变的是结构体的内容,只需要传入一级指针即可。后续操作同理,谨记!
理解:
如果需要在一个除主函数main外的函数内部改变一个变量的内容,就需要这个变量的地址。即,想要改变谁,就需要在该函数内传入谁的地址。
例如,要改变一个int型变量的内容,在函数内部就需要传入这个int变量的地址,然后再解引用来更改这个变量其中的内容。
同理,如果要改变的是一个指针变量的内容呢,就需要这个指针变量的地址,也就是二级指针。
通过获取一个变量的地址,再对其解引用,从而操控这个变量的内容,这也就是指针的魅力!指针的强大!
2.2.2 尾插
原理:
1.空链表情况:当前只有一个头指针无任何链表结点,生成一个新结点,头指针直接指向这个新结点。
2.非空链表:建立一个尾指针指向单链表最后一个结点,生成一个新结点,让尾结点的指针域next指向新结点,然后尾指针再移动(指向)到新结点上。
注:单链表规定尾结点的next域必须为空。尾插法的关键是找到尾结点,让尾结点的next指针域指向新的要插入的结点,然后尾指针再指向新结点(也是新的尾结点)。
尾插法建立单链表:读入数据的顺序与生成的链表中元素的顺序是相同的。
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
//assert(*pphead); //*pphead不能断言,链表为空也需要能插入
SListNode* newnode = BuySListNode(x);
//1.空链表情况:当前只有一个头指针无任何链表结点
if (*pphead == NULL)
{
*pphead = newnode;
}
else //2.非空链表
{
SListNode* tail = *pphead;
/*
tail->next != NULL : 使tail指针指向尾结点(即最后一个结点)的时候停下,目的是在尾结点后面插入新结点
因为尾插法的关键就是让尾结点的next指针指向新的要插入的结点,所以需要先遍历到尾结点
*/
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
2.3 单链表的删除
2.3.1 头删
1.创建一个del指针,用来记录当前链表要删除的第一个结点。
2.头指针head向后移动一位,指向后面一个结点(新的表头结点)。
3.释放free要删除的第一个结点(旧的表头结点)。
// 单链表头删
void SListPopFront(SListNode** pphead)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
assert(*pphead); //头指针为NULL,代表空链表,此时删除无意义
// if ((*pphead)->next == NULL) //一个结点
//{
// free(*pphead);
// *pphead = NULL;
//}
//else //多个结点
//{
// SListNode* del = *pphead;
// //*pphead = del->next;
// *pphead = (*pphead)->next;
// free(del);
// del = NULL;
//}
//统一写法
SListNode* del = *pphead; //记录要删除的第一个结点
//*pphead = del->next;
*pphead = (*pphead)->next;
free(del);
del = NULL;
}
2.3.2 尾删
原理:
1.当链表只有一个结点的时候:直接释放这个结点即可。
2.当链表有多个结点的时候:尾删法的关键是要找到尾结点的前一个结点,找到尾结点的前一个结点,也就相应的找到了尾结点(prve→next就找到了尾结点),然后删除尾结点。再把新的尾结点的next指针置空(因为单链表最后一个结点的指针域要指向空)。
方法一方法二本质上是一样的。
// 单链表尾删
void SListPopBack(SListNode** pphead)
{
assert(pphead); //链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); //头指针为NULL,代表空链表,此时删除无意义
if ((*pphead)->next == NULL) //一个结点
{
free(*pphead);
*pphead = NULL;
}
else //多个结点
{
//第一种方法
/*
SListNode* tail = *pphead;
SListNode* prev = NULL;
while (tail->next != NULL) //循环条件为要找到尾结点,即指针指向尾结点停止时
{
prev = tail; //记录尾结点的前一个结点
tail = tail->next;
}
free(tail);
prev->next = NULL;
*/
//第二种方法
SListNode* tail = *pphead;
// 找尾
while (tail->next->next) //找到单链表倒数第二个结点,即尾结点前一个结点
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
2.4 单链表结点的查找
遍历整个链表用cur指针记录当前指向的结点,当结点的数据域data与所要查找的数据一样时返回当前结点(指针)。
// 单链表结点的查找
SListNode* SListFind(SListNode* phead, SLTDateType x)
{
SListNode* cur = phead;
while (cur != NULL)
{
if (x == cur->data)
return cur;
else
cur = cur->next;
}
return NULL;
}
2.5 单链表结点的修改
// 单链表结点的修改
void SListModify(SListNode* phead, SLTDateType x, SLTDateType y)
{
SListNode* pos = SListFind(phead, x);
if (pos != NULL)
pos->data = y;
}
2.6 单链表任意位置的插入
2.6.1 结点在指定位置前插入
原理:
1.若是在第一个结点前一个位置插入新的结点,直接调用头插法即可。
2.在其它非第一个结点位置前插入新结点,新结点的指针域指向pos位置的结点,pos位置前一个结点的指针域指向新结点。
// 单链表结点在pos前一个位置插入
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next; //prev指向pos前一个位置的结点
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
2.6.2 结点在指定位置后插入
原理:
1.生成一个新结点,新结点的指针域指向pos位置后面的一个结点,处于pos位置的结点指针域指向新结点。
2.步骤1和步骤2的顺序不可逆。因为如果pos位置的结点指针域先指向新结点,(此时pos位置结点的指针域不在记录其原先后面一个结点的位置了),就没法找到pos位置后面的一个结点,新结点也没法插入了。
// 单链表结点在pos后一个位置插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
2.7 删除指定位置的结点
2.7.1 删除指定位置前的结点
原理:
1.移除头结点和移除其他节点的操作是不一样的,因为删除链表的其它节点都是通过找到它的前一个结点,然后其前一个结点再指向当前结点的后一个结点,从而来移除当前结点。
2.而头结点没有前一个节点。所以头结点如何移除呢?其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。 即头删法。
3.要删除pos位置的结点,让pos位置前一个结点的指针指向pos位置后一个结点,再释放pos位置的结点。
// 删除pos位置的结点
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
2.7.2 删除指定位置后的结点
// 删除pos位置后面的结点
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next); //如果pos后面没有结点(即pos位置是尾结点),就不能再删除pos后面的结点了
SListNode* after = pos->next;
pos->next = after->next;
free(after);
after = NULL;
}
2.8 单链表的销毁
原理:
依次遍历每一个结点 ,用cur指针记录当前结点,next指针记录当前结点的下一个结点。然后释放当前结点,cur再移动指向next指针指向的结点,直到遍历完整个链表。
// 单链表的销毁
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
单链表的内容学习结束,感兴趣的可以学习下一篇内容:
《带头结点的双向循环链表》https://blog.csdn.net/weixin_43382136/article/details/130907108
三.归纳总结
把单链表的所有代码整合一块,可以运行跑起来,需要的直接复制即可;创建三个文件,SList.h,SList.c,Test.c .
1.SList.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDateType;
//定义单链表结点的存储结构
typedef struct SListNode
{
SLTDateType data; //数据域
struct SListNode* next; //指针域
}SListNode;
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListTPrint(SListNode* phead);
// 单链表头插
void SListPushFront(SListNode** phead, SLTDateType x);
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x);
// 单链表头删
void SListPopFront(SListNode** pphead);
// 单链表尾删
void SListPopBack(SListNode** pphead);
// 单链表结点的查找
SListNode* SListFind(SListNode* phead, SLTDateType x);
// 单链表结点的修改
void SListModify(SListNode* phead, SLTDateType x, SLTDateType y);
// 单链表结点在pos前一个位置插入
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLTDateType x);
// 单链表结点在pos后一个位置插入
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 删除pos位置的结点
void SListErase(SListNode** pphead, SListNode* pos);
// 删除pos位置后面的结点
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode** pphead);
2.SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
// 单链表打印
void SListTPrint(SListNode* phead)
{
SListNode* cur = phead; //定义一个cur指针从头结点开始往后遍历
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next; //cur指针往后移动
}
printf("NULL\n");
}
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc failed");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
// 单链表头插
void SListPushFront(SListNode** pphead, SLTDateType x)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
//assert(*pphead); //*pphead不能断言,链表为空也需要能插入
// 单链表头插时,链表是否为空不影响头插,操作是统一的
/*
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc failed");
return;
}
newnode->data = x;
newnode->next = NULL;
*/
SListNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
// 单链表尾插
void SListPushBack(SListNode** pphead, SLTDateType x)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
//assert(*pphead); //*pphead不能断言,链表为空也需要能插入
SListNode* newnode = BuySListNode(x);
//1.空链表情况:当前只有一个头指针无任何链表结点
if (*pphead == NULL)
{
*pphead = newnode;
}
else //2.非空链表
{
SListNode* tail = *pphead;
/*
tail->next != NULL : 使tail指针指向尾结点(即最后一个结点)的时候停下,目的是在尾结点后面插入新结点
因为尾插法的关键就是让尾结点的next指针指向新的要插入的结点,所以需要先遍历到尾结点
*/
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
// 单链表头删
void SListPopFront(SListNode** pphead)
{
assert(pphead); //链表为空,pphead也不能为空,因为它是头指针plist的地址
assert(*pphead); //头指针为NULL,代表空链表,此时删除无意义
// if ((*pphead)->next == NULL) //一个结点
//{
// free(*pphead);
// *pphead = NULL;
//}
//else //多个结点
//{
// SListNode* del = *pphead;
// //*pphead = del->next;
// *pphead = (*pphead)->next;
// free(del);
// del = NULL;
//}
//统一写法
SListNode* del = *pphead; //记录要删除的第一个结点
//*pphead = del->next;
*pphead = (*pphead)->next;
free(del);
del = NULL;
}
// 单链表尾删
void SListPopBack(SListNode** pphead)
{
assert(pphead); //链表为空,pphead也不为空,因为他是头指针plist的地址
assert(*pphead); //头指针为NULL,代表空链表,此时删除无意义
if ((*pphead)->next == NULL) //一个结点
{
free(*pphead);
*pphead = NULL;
}
else //多个结点
{
//第一种方法
/*
SListNode* tail = *pphead;
SListNode* prev = NULL;
while (tail->next != NULL) //循环条件为要找到尾结点,即指针指向尾结点停止时
{
prev = tail; //记录尾结点的前一个结点
tail = tail->next;
}
free(tail);
prev->next = NULL;
*/
//第二种方法
SListNode* tail = *pphead;
// 找尾
while (tail->next->next) //找到单链表倒数第二个结点,即尾结点前一个结点
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
// 单链表结点的查找
SListNode* SListFind(SListNode* phead, SLTDateType x)
{
SListNode* cur = phead;
while (cur != NULL)
{
if (x == cur->data)
return cur;
else
cur = cur->next;
}
return NULL;
}
// 单链表结点的修改
void SListModify(SListNode* phead, SLTDateType x, SLTDateType y)
{
SListNode* pos = SListFind(phead, x);
if (pos != NULL)
pos->data = y;
}
// 单链表结点在pos前一个位置插入
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLTDateType x)
{
assert(pphead);
assert(pos);
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next; //prev指向pos前一个位置的结点
}
SListNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// 单链表结点在pos后一个位置插入
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 删除pos位置的结点
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SListNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
// 删除pos位置后面的结点
void SListEraseAfter(SListNode* pos)
{
assert(pos);
assert(pos->next); //如果pos后面没有结点(即pos位置是尾结点),就不能再删除pos后面的结点了
SListNode* after = pos->next;
pos->next = after->next;
free(after);
after = NULL;
}
// 单链表的销毁
void SListDestroy(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur)
{
SListNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
3.Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
//测试头插和尾插法
void TestSListist1()
{
SListNode* plist = NULL;
printf("单链表头插4个元素后:\n");
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListTPrint(plist);
printf("单链表再尾插2个元素后:\n");
SListPushBack(&plist, 5);
SListPushBack(&plist, 6);
SListTPrint(plist);
/*printf("把data为4的结点修改成data为666后:\n");
SListModify(plist, 4, 666);
SListTPrint(plist);*/
printf("在data为3的结点前面插入data为888的结点后:\n");
SListNode* pos = SListFind(plist, 3);
if (pos != NULL)
{
SListInsertBefore(&plist, pos, 888);
}
SListTPrint(plist);
printf("在data为5的结点后面插入data为999的结点后:\n");
pos = SListFind(plist, 5);
if (pos)
{
SListInsertAfter(pos, 99);
}
SListTPrint(plist);
printf("单链表再尾删2个元素后:\n");
SListPopBack(&plist);
SListPopBack(&plist);
SListTPrint(plist);
printf("单链表再头删2个元素后:\n");
SListPopFront(&plist);
SListPopFront(&plist);
SListTPrint(plist);
//销毁链表
SListDestroy(&plist);
}
int main()
{
TestSListist1();
return 0;
}
创作不易,如果本文对您有点帮助,希望点个赞或者关注吧~