前言:上次介绍了顺序表,这次我要分享对单链表的一些简单理解,主要框架与上次大致相同,内容主要是单链表的增删查改,适用于初学者,之后会继续更新一些更深入的内容。同时,这也仅仅是我个人对所学知识的一些总结,有错误还望各位能够指正,谢谢各位。
目录
一:链表的简单介绍
(1) 概述
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。同时要特别说明的是:链表与数组最本质的区别就是链表由两部分组成,即数据域和指针域,这样的结构使其既能够存储数据,又可以比较灵活的对数据进行插入删除。当然,其相较于数组而言不仅只有优点,还有一些不可避免的缺陷,后续会专门对二者进行一个比较。
(2) 图示
单链表的逻辑结构与现实生活中的火车十分相似,由车头与多节车厢组成。但是我们要清楚,单链表实际是以物理结构存在的,没有所谓的连接关系,只是一个一个节点之间可以依靠着各个节点的指针域进行链接,而形成一种链式结构。
二:单链表实现简单的增删查改
(1) 大致框架
1. 程序的基本框架
Test.c——用于各个接口函数功能的测试
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void Test1()//尾插尾删的测试样例
{
SLNode* phead = NULL;//定义一个指向链表起始位置的指针phead
//尾插需要改变phead的指向,所以传参时要传递二级指针!!!!!
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPushBack(&phead, 5);
SListPrint(phead);
SListPopBack(&phead);
SListPopBack(&phead);
SListPopBack(&phead);
SListPrint(phead);
SListDestroy(&phead);
}
//完成接口函数功能的测试
int main()
{
Test1();//测试尾插尾删
//Test2();//测试头插头删
//Test3();//测试锁定位置的插入与删除
return 0;
}
SList.h——用于各个头文件的包含,单链表结构的定义以及各个接口函数的定义
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLNDataType;//单链表数据域数据类型的定义
//单链表结构的定义
typedef struct SListNode
{
struct SListNode* next;//指针域
SLNDataType data;//数据域
} SLNode;
//单链表增删查改的接口函数定义
void SListPrint(SLNode* phead);//打印
SLNode* BuySListNode(SLNDataType x);//开辟新节点
void SListDestroy(SLNode** pphead);//销毁单链表,防止内存泄漏问题
void SListPushBack(SLNode** pphead, SLNDataType x);//尾插
void SListPopBack(SLNode** pphead);//尾删
void SListPushFront(SLNode** pphead, SLNDataType x);//头插
void SListPopFront(SLNode** pphead);//头删
SLNode* SListFind(SLNode* phead,SLNDataType x);//查找某个节点
void SListInsertAfter(SLNode* pos, SLNDataType x);//在pos节点后进行节点的插入
void SListEraseAfter(SLNode* pos);//删除pos节点后的节点
void SListEraseNode(SLNode** pphead, SLNode* pos);//删除pos节点
SList.c——用于各个接口函数功能的实现
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void SListPrint(SLNode* phead)//打印
{
}
SLNode* BuySListNode(SLNDataType x)//开辟节点,并且需要返回开辟的节点
{
}
void SListPushBack(SLNode** pphead, SLNDataType x)//尾插的两种情况
{
}
void SListPopBack(SLNode** pphead)//尾删要考虑三种情况(要考虑周到)!!!
{
}
void SListPushFront(SLNode** pphead, SLNDataType x)//头插直接插入即可
{
}
void SListPopFront(SLNode** pphead)//头删
{
}
SLNode* SListFind(SLNode* phead,SLNDataType x)//查找
{
}
void SListInsertAfter(SLNode* pos, SLNDataType x)//pos后插入
{
}
void SListEraseAfter(SLNode* pos)//删除pos节点后的节点
{
}
//删除pos节点有两种情况:1.pos位于首节点————直接头删即可 2.pos不位于首节点————需要先找到pos的前一个结点
void SListEraseNode(SLNode** pphead, SLNode* pos)
{
}
//销毁链表
void SListDestroy(SLNode** pphead)
{
}
2. 三大基本功能函数的实现(打印,节点创建,链表销毁)
(i) 单链表的打印
void SListPrint(SLNode* phead)//打印
{
SLNode* cur = phead;
while (cur)//遍历单链表进行打印
{
printf("%d->",cur->data);
cur = cur->next;
}
if (cur == NULL)//将NULL也打印出来
{
printf("NULL\n");
}
}
(ii) 单链表节点的创建
单链表在插入的过程中,可以实现单个节点的增加,这也是其相较于数组的一项优势,即不存在空间的浪费,而节点的增加必然离不开节点的开辟,因此需要运用到动态开辟空间的知识,即利用malloc函数开辟节点。
SLNode* BuySListNode(SLNDataType x)//开辟节点,并且需要返回开辟的节点
{
SLNode* tmp = (SLNode*)malloc(sizeof(SLNode));
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);//直接退出程序
}
SLNode* newnode = tmp;
newnode->data = x;
newnode->next = NULL;
return newnode;
}
(iii) 单链表的销毁
在以前学习动态内存管理的知识后就知道,动态开辟的空间在使用完后都要进行释放,否则就会产生内存泄漏的严重后果,因此我们需要将单链表中动态开辟的节点进行释放:
void SListDestroy(SLNode** pphead)
{
assert(pphead);
SLNode* cur = *pphead;
while (cur)//遍历节点进行释放
{
SLNode* tmp = cur->next;//保存节点
free(cur);
cur = tmp;
}
*pphead = NULL;
}
(2) 尾插尾删的实现
1. 尾插
单链表的尾插过程会涉及两种情况:
(1) 链表初始无节点———开辟节点后直接插入即可
(2) 链表初始有节点———先找尾,再将开辟的节点插入尾部
void SListPushBack(SLNode** pphead, SLNDataType x)//两种情况
{
SLNode* newnode = BuySListNode(x);//插入数据要先开辟节点
if (*pphead == NULL)//1.初始无节点
{
*pphead = newnode;
}
else//2.初始有节点时:尾插先找尾
{
SLNode* tail = *pphead;
while (tail->next)//遍历找尾
{
tail = tail->next;
}
tail->next = newnode;
}
}
2. 尾删
单链表尾节点的删除比插入复杂些,一共要考虑三种情况:
(1) 链表为空——使用assert函数进行断言直接报错警告操作者即可
(2) 只有一个节点——将唯一的节点释放再置空(这里需要强调一点的是:要直接释放传递过来的参数所代表的首节点,不可只释放用其定义的变量!!!)
(3) 有多个节点——利用遍历链表找尾节点的思路即可
void SListPopBack(SLNode** pphead)//要考虑三种情况(要考虑周到)!!!
{
//1.链表为空————使用assert函数进行断言直接报错警告操作者
assert(*pphead);
SLNode* tail = *pphead;//定义一个用来找尾节点的变量
//2.只有一个节点————将唯一的节点释放,再置空
/*if (tail->next == NULL) (当只有一个节点时,要free掉(*pphead)才达到了释放的效果!!!)
{
free(tail); //err!!!
tail = NULL;
}*/
if ((*pphead)->next == NULL)//当只有一个节点时,要free掉(*pphead)才达到了释放的效果
{
free(*pphead);
*pphead = NULL;
}
//3.有多个节点——利用遍历链表找尾节点的思路(有两种解决方法)
//else //方法一
//{
// SLNode* tailPrev = NULL;//用来保存tail的前一个节点,以保证链表能够链接起来
// while (tail->next)
// {
// tailPrev = tail;
// tail = tail->next;
// }
// free(tail);
// tail = NULL;
// tailPrev->next = NULL;
//}
else //简易一点 (方法二)
{
while (tail->next->next)//这里找尾并未找到尾节点,找到的是其前一个节点
{
tail = tail->next;
}
free(tail->next);//此时的tail->next才是真正的尾节点
tail->next = NULL;
}
}
3. 测试样例结果展示
(3) 头插头删的实现
1. 头插
相较于尾插,头插简单了一些,不需要进行找尾操作,直接将创建的节点插入即可:
void SListPushFront(SLNode** pphead, SLNDataType x)//头插直接插入即可
{
assert(pphead);
SLNode* newnode = BuySListNode(x);//创建节点
newnode->next = *pphead;//开始链接
*pphead = newnode;//链表首节点的移动
}
2. 头删
相较于尾删,头删同样简单了一些,不需要进行找尾操作,并且只用考虑链表有无节点两种情况就行,直接将首节点删除,再改变首节点即可:
void SListPopFront(SLNode** pphead)//头删
{
assert(pphead);
assert(*pphead);//判空
SLNode* next = (*pphead)->next;//记录,以方便改变首节点位置
free(*pphead);
*pphead = next;
}
3. 测试样例结果展示
(4) 指定位置的插入与删除
1. 坐标的查找
要想进行某个位置的插入删除,那就必须先获取该位置的坐标位置,使用以下函数接口就可以实现单链表中某个坐标的查找:
SLNode* SListFind(SLNode* phead,SLNDataType x)
{
SLNode* cur = phead;
while (cur && cur->data != x)//停止寻找的两种情况:1.cur遍历了整个链表 2.找到了目标坐标
{
cur = cur->next;//遍历寻找数据域为x的节点
}
return cur;//找到了就返回该节点,否则会返回NULL
}
2. 指定节点的插入
使用函数SListFind锁定坐标后,可以对其进行后置插入或前置插入,下面展示后置插入:
void SListInsertAfter(SLNode* pos, SLNDataType x)//pos表示被锁定的节点
{
SLNode* newnode = BuySListNode(x);//创建要插入的新节点
SLNode* next = pos->next;//记录被锁定节点的后一节点
pos->next = newnode;
newnode->next = next;//两步进行链接
}
3. 锁定节点的删除
对锁定节点进行删除一共有两种情况:
(1) pos位于首节点———直接头删即可
(2) pos不位于首节点———需要先找到pos的前一个结点,再执行删除及链接
//删除pos节点有两种情况:1.pos位于首节点————直接头删即可
// 2.pos不位于首节点————需要先找到pos的前一个结点
void SListEraseNode(SLNode** pphead, SLNode* pos)
{
assert(pphead && *pphead);
SLNode* posPrev = *pphead;
if (pos == *pphead)//头节点,直接头删即可
{
SListPopFront(pphead);
}
else
{
while (posPrev->next != pos)//遍历找到pos的前一个节点
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
}
}
三:完整代码的展示及测试结果的呈现
(1) Test.c
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void Test1()//尾插尾删的测试
{
SLNode* phead = NULL;//定义一个指向链表起始位置的指针phead
//尾插需要改变phead的指向,所以传参时要传递二级指针!!!!!
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPushBack(&phead, 5);
SListPrint(phead);
SListPopBack(&phead);
SListPopBack(&phead);
SListPopBack(&phead);
SListPrint(phead);
SListDestroy(&phead);
}
void Test2()//头插头删的测试
{
SLNode* phead = NULL;
SListPushFront(&phead, 1);
SListPushFront(&phead, 2);
SListPushFront(&phead, 3);
SListPushFront(&phead, 4);
SListPushFront(&phead, 5);
SListPrint(phead);
SListPopFront(&phead);
SListPopFront(&phead);
SListPopFront(&phead);
SListPrint(phead);
SListDestroy(&phead);
}
void Test3()//锁定位置的插入与删除的测试
{
//先构建一小段单链表
SLNode* phead = NULL;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushFront(&phead, 3);
SListPushFront(&phead, 4);
SListPushFront(&phead, 5);
SListPrint(phead);
//测试在pos节点后进行节点的插入
SLNode* node = SListFind(phead,3);
if (node != NULL)
{
SListInsertAfter(node, 6);
}
SListPrint(phead);
node = SListFind(phead, 5);
if (node != NULL)
{
SListInsertAfter(node, 8);
}
SListPrint(phead);
//测试删除pos节点
node = SListFind(phead, 5);
if (node != NULL)
{
SListEraseNode(&phead,node);
}
SListPrint(phead);
node = SListFind(phead, 1);
if (node != NULL)
{
SListEraseNode(&phead, node);
}
SListPrint(phead);
SListDestroy(&phead);
}
//完成接口函数功能的测试
int main()
{
//Test1();//测试尾插尾删
//Test2();//测试头插头删
Test3();//测试锁定位置的插入与删除
return 0;
}
(2) SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLNDataType;//单链表数据域数据类型的定义
//单链表结构的定义
typedef struct SListNode
{
struct SListNode* next;//指针域
SLNDataType data;//数据域
} SLNode;
//单链表增删查改的接口函数定义
void SListPrint(SLNode* phead);//打印
SLNode* BuySListNode(SLNDataType x);//开辟新节点
void SListDestroy(SLNode** pphead);//销毁单链表,防止内存泄漏问题
void SListPushBack(SLNode** pphead, SLNDataType x);//尾插
void SListPopBack(SLNode** pphead);//尾删
void SListPushFront(SLNode** pphead, SLNDataType x);//头插
void SListPopFront(SLNode** pphead);//头删
SLNode* SListFind(SLNode* phead,SLNDataType x);//查找某个节点
void SListInsertAfter(SLNode* pos, SLNDataType x);//在pos节点后进行节点的插入
void SListEraseNode(SLNode** pphead, SLNode* pos);//删除pos节点
(3) SList.c
#define _CRT_SECURE_NO_WARNINGS
#include "SList.h"
void SListPrint(SLNode* phead)//打印
{
SLNode* cur = phead;
while (cur)//遍历单链表进行打印
{
printf("%d->",cur->data);
cur = cur->next;
}
if (cur == NULL)//将NULL也打印出来
{
printf("NULL\n");
}
}
SLNode* BuySListNode(SLNDataType x)//开辟节点,并且需要返回开辟的节点
{
SLNode* tmp = (SLNode*)malloc(sizeof(SLNode));
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);//直接退出程序
}
SLNode* newnode = tmp;
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLNode** pphead, SLNDataType x)//尾插的两种情况
{
SLNode* newnode = BuySListNode(x);//插入数据要先开辟节点
if (*pphead == NULL)//1.初始无节点
{
*pphead = newnode;
}
else//2.初始有节点时:尾插先找尾
{
SLNode* tail = *pphead;
while (tail->next)//遍历找尾
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPopBack(SLNode** pphead)//尾删要考虑三种情况(要考虑周到)!!!
{
//1.链表为空————使用assert函数进行断言直接报错警告操作者
assert(*pphead);
SLNode* tail = *pphead;//定义一个用来找尾节点的指针变量
//2.只有一个节点————将唯一的节点释放,再置空
//(当只有一个节点时,要free掉(*pphead)才达到了释放的效果!!!)
/*if (tail->next == NULL) err
{
free(tail);
tail = NULL;
}*/
if ((*pphead)->next == NULL)//当只有一个节点时,要free掉*pphead才达到了释放的效果
{
free(*pphead);
*pphead = NULL;
}
//3.有多个节点——利用遍历链表找尾节点的思路(有两种解决方法)
//else //方法一
//{
// SLNode* tailPrev = NULL;//用来保存tail的前一个节点,以保证链表能够链接起来
// while (tail->next)
// {
// tailPrev = tail;
// tail = tail->next;
// }
// free(tail);
// tail = NULL;
// tailPrev->next = NULL;
//}
else //简易一点 (方法二)
{
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SListPushFront(SLNode** pphead, SLNDataType x)//头插直接插入即可
{
assert(pphead);
SLNode* newnode = BuySListNode(x);//创建节点
newnode->next = *pphead;//开始链接
*pphead = newnode;//链表首节点的移动
}
void SListPopFront(SLNode** pphead)//头删
{
assert(pphead);
assert(*pphead);//判空
SLNode* next = (*pphead)->next;//记录,以方便改变首节点位置
free(*pphead);
*pphead = next;
}
SLNode* SListFind(SLNode* phead,SLNDataType x)//查找
{
SLNode* cur = phead;
while (cur && cur->data != x)//停止寻找的两种情况:1.cur遍历了整个链表 2.找到了目标坐标
{
cur = cur->next;//遍历寻找数据域为x的节点
}
return cur;//找到了就返回该节点,否则会返回NULL
}
void SListInsertAfter(SLNode* pos, SLNDataType x)//pos后插入
{
SLNode* newnode = BuySListNode(x);//创建要插入的新节点
SLNode* next = pos->next;//记录被锁定节点的后一节点
pos->next = newnode;
newnode->next = next;//两步进行链接
}
//删除pos节点有两种情况:1.pos位于首节点————直接头删即可
//2.pos不位于首节点————需要先找到pos的前一个结点
void SListEraseNode(SLNode** pphead, SLNode* pos)
{
assert(pphead && *pphead);
SLNode* posPrev = *pphead;
if (pos == *pphead)//头节点,直接头删即可
{
SListPopFront(pphead);
}
else
{
while (posPrev->next != pos)//遍历找到pos的前一个节点
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
}
}
void SListDestroy(SLNode** pphead)//销毁单链表
{
assert(pphead);
SLNode* cur = *pphead;
while (cur)//遍历节点进行释放
{
SLNode* tmp = cur->next;//保存节点
free(cur);
cur = tmp;
}
*pphead = NULL;
}
(4) 测试结果
总结:
这里仅仅是单链表简单的增删查改功能的实现,大家掌握了后最好还可以去找到一些对应的习题进行练习,这里就不再多说。如果文章有错误还望各位多多指正,万分感谢,再见。