一.链表
链表也是线性表的一种,其结构特征是在逻辑结构上是连续的,但在物理结构上不是连续的。链表的组成是由一个一个的节点组成的。单个节点内有要储存的数据以及指向下一个结点的指针,即数据域与指针域。链表中每个节点都是独立申请的(即需要插入数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。如图所示
结合前面我们所学习的结构体的知识,给出每个节点所对应的结构体代码。假设当前保存的数据类型是SLDataType型
//首先我们先定义一个节点
typedef int SLDataType;
typedef struct SLNode {
SLDataType data;//数据域
struct SLNode* next;//指针域
}SLNode;
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。 当我们想要从第⼀个节点走到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下一个 节点的钥匙)就可以了。
二.详细实现链表的增删擦改等等功能
2.0 首先在测试文件中创建一个节点
//SLTest.c
SLNode* plist = NULL;
2.1 头文件
先给出头文件,列出我们即将实现的功能
//SList.h
typedef int SLDataType;
typedef struct SLNode {
SLDataType data;//数据域
struct SLNode* next;//指针域
}SLNode;
//链表的打印
void SLPrint(SLNode* phead);
//链表的头插和尾插
void SLPushFront(SLNode** phead,SLDataType x);
void SLPushBack(SLNode** phead,SLDataType x);
//链表的头删尾部删除
void SLPopFront(SLNode** pphead);
void SLPopBack(SLNode** pphead);
//查找
void SLFind(SLNode** phead, SLDataType x);
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x);
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);
//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x);
//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos);
//销毁链表
void SLDestory(SLNode** pphead);
2.2 实现
//SList.c
2.2.1 实现链表的打印
要想实现链表的打印,就要遍历整个链表,在传参是应该将节点的地址传过来,用一个循环即可
//链表的打印
void SLPrint(SLNode* phead)
{
SLNode* pcur = phead;
while (pcur)
{
printf("%d->",pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
我们一般不直接使用头节点地址,而是设置一个临时变量,让这个临时变量遍历链表
while(pcur)-->遍历整个链表 while(pcur->next)-->找到末尾节点
2.2.2 链表的头插和尾插
1.链表的头插
有一点需要我们特别是注意,我们把newnode赋值给了phead,相当于改变了头指针的指向,那我们这时只传结构体的地址是不够的,我们需要把指向结构体指针的地址也传过来(二级指针pphead),来改变结构体指针的指向
还有一点--》无论是头插还是尾插,我们都需要申请一个新节点,我们简洁性,我们给一些一个函数来申请一个新节点SLBuyNode
SLBuyNewNode函数
//创建新节点
SLNode* SLBuyNewNode(SLDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->data = x;
newnode->next = NULL;//不管链表为不为空,newnode的next一定时空指针
return newnode;
}
现在头插分为两种请况,链表为空或者链表不为空,两种结果是一样的
void SLPushFront(SLNode** pphead,SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNewNode(x);
//链表为空或不为空都是一样的,新节点的next为原本的头指针,再把next赋为新的头节点
newnode->next = *pphead;
*pphead = newnode;
}
若链表为空,也就是*pphead=NULL;这时把newnode->next = *pphead;也是正确的;
2.链表的尾插
void SLPushBack(SLNode** pphead,SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNewNode(x);
//当链表为空时,newnode成为头节点
if (*pphead == NULL)
{
*pphead = newnode;//改编身命运,让newnode指向的空间成为头节点
return;
}
//链表不为空时,找出尾部节点
//最好不要动头节点,防止待会要用而找不到位置
SLNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
2.2.3 链表的头部删除与尾部删除
由于删除后要改变指向同时要释放空间,还是传二级指针,并且链表不能为空
1.头部删除
a.链表不管有几个节点直接让下一个节点成为头节点就好
!!注意->的优先级高于*,注意括号!!
//链表的头删尾部删除
void SLPopFront(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
//注意 ->的优先级高于*
//不管有几个节点,只需让第二个节点成为新的头节点即可
SLNode* next = (*pphead)->next;//记录下第二个节点的位置,否则释放后将找不到第二个节点了
free(*pphead);
*pphead = next;//让第二个节点成为空指针
}
2.尾部删除
首先,肯定不能是空链表,分为只有一个节点或者不止一个节点
a.只有一个节点--即(*phead)->next == NULL,可直接释放头节点,在把头节点置为空即可
b.不止一个节点--先找到末尾节点和末尾节点的前一个结点(用快慢双指针)
void SLPopBack(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
SLNode* ptail = *pphead;
//找到末尾节点前的一个节点,释放malloc所得节点
//只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//while (pcur->next->next != NULL)//必须有两个以上节点
//{
// pcur = pcur->next;
//}
//另一种好阅读的写法\双指针(快慢指针)
SLNode* prev = NULL;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
//销毁尾部节点
free(ptail);
ptail = NULL;
}
2.2.3 链表的查找
链表的查找是通过data来确定其位置,所以我们可以遍历链表找到指向data数据的指针,在返回该指针即可
//查找(可以不穿二级指针,但是为了一致性,还是传了)
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
assert(pphead);
//遍历整个链表查找x,
SLNode* pcur = *pphead;
while (pcur)
{
if ((pcur)->data == x)
{
printf("链表中存在该数据\n");
return pcur;
}
pcur = pcur->next;
}
printf("查找的数据不存在\n");
}
2.2.4.在指定位置之前插入数据
//首先,不能是空指针,但我们只用判断pos位置是否为空即可,pos不为空-->链表不可能为空
由于是在指定位置之前插入数据,我们需要得到指定位置的前一个位置的指针(prev)
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead);
assert(pos);
SLNode* newnode = SLBuyNewNode(x);
//pos刚好是头节点
if (pos == *pphead)
{
newnode->next = pos;
pos->next = NULL;
return;
}
//先遍历链表找到前面一个数据的指针
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
2.2.5 在指定位置之后插入节点
//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x)
{
//pos存在说明链表不会为空并且链表一定存在,因为pos处有数据
assert(pos);
SLNode* newnode = SLBuyNewNode(x);
//由于设置的newnode->NULL 无论pos在哪里,下面代码都通用
newnode->next = pos->next;
pos->next = newnode;
}
2.2.6 删除pos节点
还是只用检查pos节点是否为空
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
//若是头节点,直接头删
if (pos == *pphead)
{
SLPopFront(pphead);
return;
}
//不是头节点,取得pos之前位置的节点
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
2.2.7 删除pos位置之后的节点
先把pos->next = pe=os->next->next;
再把pos->next 释放即可,注意要将指针置为空,避免出现野指针(结构体中定义过的next)
//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos)
{
//还是只要pos存在,链表就存在
assert(pos&&pos->next!=NULL);
pos->next = pos->next->next;
free(pos->next);
pos->next = NULL;//指针是存在的
}
2.2.8 销毁链表
即遍历链表,将链表中的所有空间全部释放
//销毁链表
void SLDestory(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
//需要遍历链表,一个一个释放,但又不能释放完上一个就找不到下一个了
//需要一个辅助指针
SLNode* pcur = *pphead;
while (*pphead )
{
pcur = *pphead;
(*pphead) = (*pphead)->next;
free(pcur);
}
pcur = NULL;
}
以上就是全部的实现啦!
三.全部代码
以下是全部的代码
//头文件SList.h
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
//思考,都已经有顺序表来储存数据了,为什么还要用单链表呢?
//1.顺序表扩大倍数时可能会浪费内存,有没有另一种数据结构能够避免呢?
//2.顺序表的删除和插入需要挪动后方/前方的所有数据,太麻烦
//所以就请出我们的单链表
//单链表也是线性表的一种,在逻辑上是连续的,在物理结构上是非连续的
//链表右节点构成,每个节点分为数据域和指针域,数据域储存数据,指针域储存指向下一个节点的地址
//这样做不需要扩大内存,按需申请节点即可
//下面我将用单链表实现数据的增删查改
//首先我们先定义一个节点
typedef int SLDataType;
typedef struct SLNode {
SLDataType data;//数据域
struct SLNode* next;//指针域
}SLNode;
//链表的打印
void SLPrint(SLNode* phead);
//链表的头插和尾插
void SLPushFront(SLNode** phead,SLDataType x);
void SLPushBack(SLNode** phead,SLDataType x);
//链表的头删尾部删除
void SLPopFront(SLNode** pphead);
void SLPopBack(SLNode** pphead);
//查找
void SLFind(SLNode** phead, SLDataType x);
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x);
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);
//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x);
//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos);
//销毁链表
void SLDestory(SLNode** pphead);
//实现文件SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SLlist.h"
//单链表这里指的是单项不循环无头链表,不需初始化
// node -- 我啥也改变不了,我只是一个结构体
// &node也就是phead(一级指针) -- 可以改变node中的next和data
// &phead(二级指针) --可以改变自身指向的空间,决定自己的命运
//链表的打印
void SLPrint(SLNode* phead)
{
SLNode* pcur = phead;
while (pcur)
{
printf("%d->",pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
//创建新节点
SLNode* SLBuyNewNode(SLDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
newnode->data = x;
newnode->next = NULL;//不管链表为不为空,newnode的next一定时空指针
return newnode;
}
//链表的头插和尾插
void SLPushFront(SLNode** pphead,SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNewNode(x);
//链表为空或不为空都是一样的,新节点的next为原本的头指针,再把next赋为新的头节点
newnode->next = *pphead;
*pphead = newnode;
}
void SLPushBack(SLNode** pphead,SLDataType x)
{
assert(pphead);
SLNode* newnode = SLBuyNewNode(x);
//当链表为空时,newnode成为头节点
if (*pphead == NULL)
{
*pphead = newnode;//改编身命运,让newnode指向的空间成为头节点
return;
}
//链表不为空时,找出尾部节点
//最好不要动头节点,防止待会要用而找不到位置
SLNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
//链表的头删尾部删除
void SLPopFront(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
//注意 ->的优先级高于*
//不管有几个节点,只需让第二个节点成为新的头节点即可
SLNode* next = (*pphead)->next;//记录下第二个节点的位置,否则释放后将找不到第二个节点了
free(*pphead);
*pphead = next;//让第二个节点成为空指针
}
void SLPopBack(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
SLNode* ptail = *pphead;
//找到末尾节点前的一个节点,释放malloc所得节点
//只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//while (pcur->next->next != NULL)//必须有两个以上节点
//{
// pcur = pcur->next;
//}
//另一种好阅读的写法\双指针(快慢指针)
SLNode* prev = NULL;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
//销毁尾部节点
free(ptail);
ptail = NULL;
}
//查找(可以不穿二级指针,但是为了一致性,还是传了)
void SLFind(SLNode** pphead, SLDataType x)
{
assert(pphead);
//遍历整个链表查找x,
SLNode* pcur = *pphead;
while (pcur)
{
if ((pcur)->data == x)
{
printf("链表中存在该数据\n");
return;
}
pcur = pcur->next;
}
printf("查找的数据不存在\n");
}
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead);
assert(pos);
SLNode* newnode = SLBuyNewNode(x);
//pos刚好是头节点
if (pos == *pphead)
{
newnode->next = pos;
pos->next = NULL;
return;
}
//先遍历链表找到前面一个数据的指针
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x)
{
//pos存在说明链表不会为空并且链表一定存在,因为pos处有数据
assert(pos);
SLNode* newnode = SLBuyNewNode(x);
//由于设置的newnode->NULL 无论pos在哪里,下面代码都通用
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
//若是头节点,直接头删
if (pos == *pphead)
{
SLPopFront(pphead);
return;
}
//不是头节点,取得pos之前位置的节点
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos)
{
//还是只要pos存在,链表就存在
assert(pos&&pos->next!=NULL);
pos->next = pos->next->next;
free(pos->next);
pos->next = NULL;//指针是存在的
}
//销毁链表
void SLDestory(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
//需要遍历链表,一个一个释放,但又不能释放完上一个就找不到下一个了
//需要一个辅助指针
SLNode* pcur = *pphead;
while (*pphead )
{
pcur = *pphead;
(*pphead) = (*pphead)->next;
free(pcur);
}
pcur = NULL;
}
//测试文件SLTest.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SLlist.h"
void SLTest()
{
/*SLNode* plist1 = (SLNode*)malloc(sizeof(SLNode));
SLNode* plist2 = (SLNode*)malloc(sizeof(SLNode));
SLNode* plist3 = (SLNode*)malloc(sizeof(SLNode));
SLNode* plist4 = (SLNode*)malloc(sizeof(SLNode));
plist1->next = plist2;
plist2->next = plist3;
plist3->next = plist4;
plist4->next = NULL;
plist1->data = 1;
plist2->data = 2;
plist3->data = 3;
plist4->data = 4;*/
SLNode* plist = NULL;
SLPushBack(&plist, 1);
SLPushBack(&plist, 2);
SLPushBack(&plist, 3);
SLPushBack(&plist, 4);
/*SLPushBack(NULL, 4);*/
SLPushFront(&plist, 5);
SLPrint(plist);
/*SLPopBack(&plist);
SLPopBack(&plist);
SLPopBack(&plist);*/
SLPopFront(&plist);
SLPopFront(&plist);
SLPrint(plist);
SLFind(&plist, 5);
SLFind(&plist, 3);
SLDestory(&plist);
SLPrint(plist);
}
int main()
{
SLTest();
return 0;
}