目录
前言
链表的概念与理解
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
此图为单向链表的物理图(箭头便于理解,实际上并不存在直接链接)
- 链式结构在逻辑上是连续的,但是在物理上不一定连续
- 现实中的结点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
链表的分类
- 单向或者双向
- 带头或不带头
- 循环或非循环
最常用的链表有两种:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
我们要实现的就是无头单向非循环链表
单链表的实现
头文件
- 头文件我们进行头文件的引用以及类型定义和函数声明。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//把类型命名为SLNDateType,当改变链表数据类型时改变此句int即可
typedef int SLNDateType;
//结构体 - 节点
typedef struct SListNode
{
SLNDateType data;
struct SListNode* next;
}SLN;//将结构体名为SLN方便使用
//打印
void SListPrint(SLN* phead);
//创建节点
SLN* CreatListNode(SLNDateType x);
//尾插
void SListPushBack(SLN** pphead, SLNDateType x);
//头插
void SListPushFront(SLN** pphead, SLNDateType x);
//尾插
void SListPopBack(SLN** pphead);
//头删
void SListPopFront(SLN** pphead);
//查找
SLN* SListFind(SLN* phead, SLNDateType x);
//特定下标插入(向前插入)
void SListInsert(SLN** phead, SLN* pos, SLNDateType x);
//特定下标插入(向后插入)
void SListInsertAfter(SLN** phead, SLN* pos, SLNDateType x);
//特定节点删除
void SListErase(SLN** phead, SLN* pos);
//循环
void SListDestory(SLN* phead);
链表打印
- 我们从首位开始读取,将phead地址给cur,判断cur若不为空则没有到达尾部,继续打印,把cur置为
cur->next
void SListPrint(SLN* phead)
{
SLN* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");//结尾+换行
}
开辟空间/创建节点
- 在进行插入相关的函数时需要先创建一个节点(开辟一个空间)
SLN* CreatListNode(SLNDateType x)
{
SLN* newnode = (SLN*)malloc(sizeof(SLN));
if (newnode == NULL)//判断malloc是否申请成功
{
perror("malloc fail");//报错
exit(-1);
}
newnode->data = x;//开辟的空间数据的值置为x
newnode->next = NULL;//开辟空间的next置空,根据需要改变
return newnode;
}
尾插
- 需要注意的是因为我们传过来的值是指针,而尾插需要改变改变传过来的值(plist),形参使用二级指针。
void SListPushBack(SLN** pphead, SLNDateType x)
{
SLN* newnode = CreatListNode(x); //先开辟一个空间
if (*pphead == NULL)//如果链表为空
{
*pphead = newnode;//直接把头放到开辟出的空间
}
else
{
//找到尾节点
SLN* tail = *pphead;
//找尾部
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;//把newnode变为尾部
}
}
头插
- 开辟空间后其
next=*pphead
变成首,再将*pphead重新放回首位 - 头插不必考虑链表此时是否只有一个节点
void SListPushFront(SLN** pphead, SLNDateType x)
{
SLN* newnode = CreatListNode(x);//开辟空间
newnode->next = *pphead;
*pphead = newnode;
}
尾删
- 尾删需要考虑只有一个节点或两个或以上节点的情况
- 两个及以上节点时可以采用两种方法
法一
法二
void SListPopBack(SLN** pphead)
{
//用assert或if语句均可
assert(*pphead != NULL);
/*if (*pphead == NULL)
return;*/
if ((*pphead)->next == NULL)//一个节点
{
free(*pphead);
*pphead = NULL;
}
else//两个及以上节点
{
//法一
SLN* prev = NULL;
SLN* tail = *pphead;
while (tail->next != NULL)//先找尾
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
//法二
/*SLN* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;*/
}
}
while(tail->next != NULL)
和while(tail->next)
同等效应,意义不完全一样
头删
- 前面的理解后相信接下来的就没有过于难理解了‘’
- 头删先把原本第一个节点的next给到新创建的next中,然后释放掉第一个节点,最后把首位的指针指向此时的next(原本第二个节点)
- 过程中next作临时储存
void SListPopFront(SLN** pphead)
{
assert(*pphead != NULL);//断言链表不能为空
SLN* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找
- 对于需要对传入的参数进行改变的使用二级指针,而查找不对形参改变,使用一级指针即可
- 我们使用循环,在cur逐渐向下一位移动的过程中如果找到即返回该类值。找不到返回空
SLN* SListFind(SLN* phead, SLNDateType x)
{
SLN* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;//走到下一位
}
}
//找不到返回空
return NULL;
}
任意位置插入(前插和后插)
插入到该位置前面
- 插入需要考虑只有一个节点或两个或以上节点的情况
void SListInsert(SLN** pphead, SLN* pos, SLNDateType x)
{
//最好加上断言
assert(pos);
SLN* newnode = CreatListNode(x);
//判断是否只有一个节点
if (*pphead == pos)
{
//(可直接调用头插函数)
newnode->next = *pphead;
*pphead = newnode;
}
else
{
//找到pos前一位
SLN* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
//改变链接
posPrev->next = newnode;
newnode->next = pos;
}
}
插入到该位置后面
- 单链表更倾向于使用插入位置后面,效率更高
- 创建新节点,让其指向该位置的下一位,再将该位置指向创建的新节点
void SListInsertAfter(SLN* pos, SLNDateType x)
{
assert(pos);
SLN* newnode = CreatListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
删除任意位置(该位置或后删)
删除该位置
- 首位和剩下的其余位置分两种情况讨论
void SListErase(SLN** pphead, SLN* pos)
{
//断言不能为空
assert(*pphead);
assert(pos);
//头位
if (*pphead == pos)
{
*pphead = pos->next;
free(pos);
//或调用头删函数
//SListPopFront(pphead);
}
else//非头位
{
SLN* prev = *pphead;
while (prev->next != pos)//向后走到pos位的前一位
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;//非必须,可加可不加
//基于编程习惯,通常会置空
}
}
删除该位置后面的
- 此函数与尾删的法二比较类似
void SListEraseAfter(SLN* pos)
{
//该位置后面不能为空
assert(pos->next);
SLN* next = pos->next;
pos->next = next->next;
free(next);
//next = NULL;//非必要
//出了函数,next也就销毁了
}
销毁/释放链表
- 链表不同于顺序表,需要逐步释放完所有节点
- 我们使
next
存储cur
的下一位,释放cur
后再给cur
,实现逐步向后释放 - 最后把
plist
(*pphead)置空
void SListDestory(SLN** pphead)
{
SLN* cur = *pphead;
while (cur)
{
SLN* next = cur->next;//临时存储cur的下一位
free(cur);
cur = next;
}
*pphead = NULL;
}