单链表
单链表概念与结构
概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
用生活实例比喻就好比我们日常做的高铁火车:
每个列车有n节车厢,每个车厢在组装前都是独立的,当我们需要时,会用一个一个的钩锁将每节车厢链接,每个钩锁就好比指针链接,他将两个不相干的车厢链接。
那链表的车厢(结构)时怎样的?
本质上就是通过结构体来存放数据与下一个结构体的地址,每个结构体被称为节点。
节点
图中plist存放的时第一个节点的地址(就是列车的车头),我们可以通过地址来找寻下一个节点,通过下一个节点里存放的地址就可以找到第二个节点,这就是节点的作用。
链表的性质
- 链式机构在逻辑上是连续的,在物理结构上不⼀定连续
- 结点⼀般是从堆上申请的
- 从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
节点(结构体)的编写:
c
typedef int SLTDataType//如果要修改数据类型就只用在这里改一下,
//不用在整个程序里面替换数据类型,方面后续修改数据类型的需要
typedef struct SLTNode
{
SLTDataType data; //结点数据
struct SListNode* next; //指针保存下⼀个结点的地址
}SLTNode;
单链表的打印
前面我们说了可以通过每个节点存放的地址来找到下一个节点,我们打印链表就可以借助这个特点来打印。
c
void SLTPrint(SLTNode* phead)
{
assert(phead);
SLTNode* pcur=phead;//保存链表地址,利用这个临时变量来读取内存
while(pcur)
{
printf("%d->",pcur->data);
pcur=pcur->next;
}
printf("NULL\n");
}
while循环判定条件的选择:
前面的结构让我们了解到最后一个节点里存放的指针是NULL;
当我们pcur为NULL时,代表我们通过循环访问了每一个节点,每访问一个节点就打印了它的数据,这样我们就打印出了整个链表的数据内容。
实现单链表
创建一个头文件SList.h
c
#ifndef __SList_H_
#define __SList_H_
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义链表(结点)的结构
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
//打印
void SLTPrint(SLTNode* phead);
//插入
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//删除
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
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);
#endif
接下来在SList.c文件内一个一个实现头文件里面的函数。
创建新的节点
因为要插入数据,就必须创建节点,为了代码的可读性与简洁,我们将创建节点封装为一个函数,写在SList.c文件内部。
c
SL_BuyNode(SLTDataType x)//x为要写入的数据
{
SLTNode* newnode=(SLTNode*)malloc(sizeof(SLTNode));//采用动态内存管理,方便后续的删除数据等操作
assert(newnode);//防止空间开辟失败
//初始化
newnode->data=x;//保存数据
newnode->next=NULL;//让它指向的下一个节点暂时为空
retuen newnode;//返回新节点的地址
}
在末尾插入数据
思路:
观察结构图,我们要在尾部插入节点,首先要创建一个新的节点,将新节点的地址赋给原来的末尾节点内的指针;
分析一下执行此操作需要的数据:
原末尾节点的地址
新节点的地址
代码实现:
c
void SLTPushBack(SLTNode** pphead, SLTDataType x)//这里注意下我们的形参时二级指针
//原因:因为我们尾插时可能会遇到首地址phead为空,也就是一个节点都没有的情况
//所以我们会将节点的地址存放的首地址内部,这样我们就需要改变首地址的内容
//因为形参是实参的临时拷贝,形参的改变不会影响实参,想要形参改变的同时影响到实参,就要使用地址传参,这里接收数据就要使用二级指针
{
assert(pphead);
SLTNode* newnode=SL_BuyNode(x);//创建新节点
if(*pphead==NULL)//链表里面一个节点都没有
{
*pphead=newnode;
}
else
{
SLTNode* pcur=*pphead;//这里链表里面以及有节点了,防止改变首地址,我们借用临时变量来操作
while(pcur->next)//当节点内的next指针指向空,我们就找到了原末尾节点地址
{
pcur=pcur->next;
}
pcur->next=newnode;
}
}
在头部插入数据
思路:
- 创建一个新节点
- 将原首节点的地址存放的新节点内部的指针里
- 将首地址plist内存放的地址改为新头节点的地址
需要的数据:新节点地址,原头节点地址(plist存放的地址)
代码实现:
c
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode=SL_BuyNode(x);
newnode->next=*pphead;
*pphead=newnode;
}
删除尾部数据
思路:
- 找到倒数第二个节点
- 利用free函数释放最后一个节点的空间(最后一个节点的空间在倒数第二个节点内部存放)
- 让倒数第二节点内的指针指向NULL;
需要的数据:倒数第二个节点地址
代码实现:
c
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
if((*pphead)->next==NULL)
{
free(*pphead);
*pphead=NULL;
}
else
{
SLTNode* prv=*NULL;
SLTNode* pcur=*pphead;
while(pcur->next==NULL)
{
prv=pcur;
pcur=pcur->next;
}
free(pcur);
prv->next=NULL;
}
}
删除第一个节点
思路:
- 将第二个节点的地址存放到plist
- 释放第一个节点空间
需要的数据:第二个节点的地址
代码实现:
c
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
SLTNode* div = (*pphead)->next;//暂存第二个节点的地址
free(*pphead);
*pphead = div;
}
在链表中寻找目标数据
思路:
- 通过循环遍历每个节点
- 每个节点的数据与目标数据对比
- 找到了就返回该节点地址,没找到就返回NULL
代码实现:
c
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;;
}
在指定位置之前插入数据
思路:
- 创建新的节点
- 找到指定位置之前的节点地址
- 将新节点的地址存放的指定位置之前的节点中
- 将指定位置的节点地址存放到新节点里
数据需求:指定位置,新的节点,指定位置前的节点地址
指定位置由函数SLTFind提供。
c
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode=SL_BuyNode(x);
if (pos == NULL)//当指定位置为NULL,说明该链表没有节点或者没有我们我要的数据,就将新节点作为头节点
{
SLTPushFront(pphead,x);
}
else
{
SLTNode* prv = *pphead;
while (prv->next!=pos)
{
prv =prv->next;
}
newnode->next = pos;
prv->next = newnode;
}
}
在指定位置之后插⼊数据
思路:
- 创建新节点
- 找到指定位置
- 将指定位置后节点地址放到newnode里面
- 将newnode的地址放到指定位置节点里
c
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode=SL_BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
删除pos结点
思路:
- 找到该位置下一个节点地址并赋值给该位置前一个节点
- 释放该位置空间
代码:
c
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead&&*pphead);//当pphead为NULL,说明没有节点,防止free释放NULL
assert(pos);
if(pos==*pphead)//只有一个节点
{
SLTPopFront(pphead);
}
else//两个及两个以上
{
SLTNode* prv = *pphead;
while (prv->next != pos)
{
prv = prv->next;
}
prv->next = pos->next;
free(pos);
pos = NULL;
}
}
删除pos之后的结点
思路:
- 找到pos的下下一个节点地址赋值给pos
- 释放pos下一个节点
代码:
c
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
SLTNode* div = pos->next;
pos->next = div->next;//div->next等价于pos->next->next
free(div);
div = NULL;
}
销毁链表
思路:
- 将plist(*pphead)重新赋值为NULL
- 利用free释放后续节点
c
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* div = pcur->next;
free(pcur);
pcur = div;
}
*pphead = NULL;
}
单链表测试
c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void test()
{
SLTNode* plist = NULL;
//尾插1,2,3,4
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
//结果:1->2->3->4->NULL
SLTPrint(plist);
//头插
SLTPushFront(&plist,0);
//结果:0->1->2->3->4->NULL
SLTPrint(plist);
//尾删
SLTPopBack(&plist);
//结果:0->1->2->3->NULL
SLTPrint(plist);
//头删
SLTPopFront(&plist);
//结果:1->2->3->NULL
SLTPrint(plist);
//查找数据
SLTNode* div=SLTFind(plist, 3);
if (div)
{
printf("YES\n");
}
else
{
printf("NO!\n");
}
//在指定位置前插入
SLTInsert(&plist,div,11);
//结果:1->2->->11->3->NULL
SLTPrint(plist);
//在指定位置后插入
SLTInsertAfter(div,12);
//结果:1->2->->11->3->12->NULL
SLTPrint(plist);
//删除指定位置
SLTErase(&plist, div);
//结果:1->2->->11->12->NULL
SLTPrint(plist);
SListDestroy(&plist);
SLTPrint(plist);
}
int main()
{
test();
return 0;
}
源码地址如下
-------------------------------------------------分隔符
单链表的基本结构与性质就介绍完了,出了我列举的几个函数外还有其他功能的函数,有兴趣的可以自行尝试,原理与上述函数差不多。
有错请在评论区指正,谢谢。