单链表学习导航
一.前言
本文为我对链表学习的过程与理解,适合新手学习交流。内容如题,我将结合画图和截图带领大家更好的理解单链表结构,学习和使用它增删查改的功能。(VS2019编译器所展示)
二.准备工作
1.对单链表运行原理的简单理解
如上图,单链表是通过结构体里面放置一个新的结构体指针的形式,使用下一个结构体的地址来找到该结构体中的数据的,链表相比于顺序表具有存储数据不连续,方便修改的特点。但单链表的缺点也很明显,只能从第一个节点开始遍历,比较局限。
2.区域化编辑
此目的是为了更好的理解代码,在前文中我也提到过,但模块化看起来清晰更有利于我们理解以及使用、修改代码。(还有美观...)
三.SList.h头文件引用区
1.单链表节点的创建
在该文件中,我们将链表结构体定义,设置int为链表中数据的存储形式(方便展示),同时节点中增加下一个节点的地址。重定义链表方便使用该结构体。
#define SLTDateType int
typedef struct SListNode
{
SLTDateType Data;
struct SListNode* Next;
}SListNode;
2.单链表功能函数的声明
在头文件中声明这些函数后直接使用即可,先列出来目标,后面咱们一一实现。
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
其中,有的函数功能传入的参数为二级指针,为什么呢?因为函数传入的参数是实参的一份临时拷贝,如果传入的是一级指针,那么我们可以改变这个指针指向的数据,而如果我们想要改变这个指针的地址呢?同样的道理,我们需要一个指向这个指针的地址的指针,也就是二级指针。
四.SListTest.c测试区
该区域主要为所写功能函数的测试模块,以此来判断函数的正确性、是否完善。
int main()
{
SListNode* List = NULL;
SListTest1(List);
SListTest2(List);
SListTest3(List);
SListTest4(List);
SListTest5(List);
SListTest6(List);
SListTest7(List);
return 0;
}
类似于菜单,不过少了些富丽堂皇的界面,因为写了菜单之后很可能会影响到我们调试的效率,将功能函数实现并调试完成之后,再完成菜单界面是个更好的选择。测试区域可以自行定义,我也将在下文中结合该区域来展示函数的实现效果。
五.SList.c功能实现区
1.动态申请一个节点
链表节点的申请可以使用malloc和calloc函数,此处我们使用malloc,记得判断返回值是否为空。
//动态申请链表节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("BuySListNode::malloc");
return;
}
newnode->Data = x;
return newnode;
}
此处还可以在返回节点之前将该节点的下一个地址置空,初始化也是种不错的选择。可以看个人需求选择。
2.单链表打印
将第一个节点传入函数,通过节点中的下一个指针一一遍历每个节点中存储的数据,并打印出来,最后节点为空时停止即可。
//打印链表
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur)
{
printf("%d->", cur->Data);
cur = cur->Next;
}
printf("NULL\n");
}
3.单链表尾插
尾插需要考虑两种情况,第一种则是链表为空的情况,这种情况直接让该链表指向开辟的节点上即可;第二种情况为非空的情况,这种情况比较复杂,我们来画图解决:
地址都是瞎编的,原理如此,代码实现如下:
//尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
//判断是否为空链表
if (*pplist==NULL)
{
*pplist = BuySListNode(x);
(*pplist)->Next = NULL;
}
else
{
//查找尾部
SListNode* ptr = *pplist;
while (ptr->Next)
{
ptr = ptr->Next;
}
//插入
ptr->Next = BuySListNode(x);
ptr->Next->Next = NULL;
}
}
结合打印链表函数效果如图:
4.单链表头插
头插的实现和尾插一样,分空链表和非空链表两种情况。二者区别在于非空情况,头插演示如图:
原来没有头插的非空链表,pplist指向头节点的地址。
插入头节点的非空链表演示如上图,代码实现如下图。
//头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
if (*pplist == NULL)
{
*pplist = BuySListNode(x);
(*pplist)->Next = NULL;
}
else
{
SListNode* ptr = BuySListNode(x);
ptr->Next = *pplist;
*pplist = ptr;
}
}
实现效果如图:
5.单链表尾删
尾删分为三种情况:
第一种为链表是否为空,若为空,可以选择报错也可以选择直接return(没有内容咋删...);
第二种为链表不为空,但只有一个节点的情况,这种情况我们需要直接将链表置空;
第三中为多节点的情况,这时我们要将NULL节点之前的节点置空,其中需要NULL节点前前个节点来判断,因为还要free掉不要用的内存,该情况如下图所示。
具体实现如下:
//尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
if ((*pplist)->Next == NULL)
{
//单个链表块
free(*pplist);
*pplist = NULL;
}
else
{
//2个及以上链表块
SListNode* ptr = *pplist;
while (ptr->Next->Next)
{
ptr = ptr->Next;
}
free(ptr->Next);
ptr->Next = NULL;
}
}
效果如下:
6.单链表头删
头删只需要考虑两种情况就行了。
第一种为判断链表是否为空;
第二种不论是否为多节点,指向头节点地址的指针都会向后移动到下一个节点的地址上,因此不需要考虑这下一个节点是否为空。
代码实现如下:
//头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* Del = *pplist;
*pplist = (*pplist)->Next;
free(Del);
Del = NULL;
}
具体效果如图:
7.单链表查找
查找目标节点并返回该节点的地址,传值选用一级指针就行了,因为有返回值为一级指针,这类函数的方法也可以代替上面的二级指针的使用(个人觉得这样不好看)。
查找需要遍历,除非找到目标节点,否则到NULL为止停下,直接返回查找节点即可。
//查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
SListNode* find = plist;
while (find && find->Data != x)
find = find->Next;
return find;
}
实现效果如图:
8.单链表在pos(指定)位置之后插入
这个函数和头插函数多节点的情况很像,不过该函数不需要改变头节点指针的位置,因此传入一级指针即可。
插入前需判断是否传入的为空指针,然后插入就行啦,不过写入的顺序要正确,否则可能会丢失原先的地址哦?
//在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* next = pos->Next;
SListNode* newnode = BuySListNode(x);
pos->Next = newnode;
newnode->Next = next;
}
效果如下:
9.单链表在pos(指定)位置之后删除
这个函数还比较简单,注意传值是否为空指针和指向的下一节点是否为空就行了。
没有判断后者的话可是会非法访问的...不得不说链表对于初学者而言坑太多了。
//删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (pos->Next == NULL)
return;
else
{
SListNode* NNext = pos->Next->Next;
free(pos->Next);
pos->Next = NNext;
}
}
实现效果如下:
10.销毁单链表
类似于顺序表的销毁,不同的是需要free掉前者,且遍历的是链表节点的指针。
//销毁
void SListDestroy(SListNode* plist)
{
assert(plist);
SListNode* Del = plist;
while (Del)
{
plist = plist->Next;
free(Del);
Del = plist;
}
}
六.源码
1.SList.h源码
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#define int SLTDateType
typedef struct SListNode
{
SLTDateType Data;
struct SListNode* Next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* phead);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);
2.SList.c源码
#include "SList.h"
//动态申请链表节点
SListNode* BuySListNode(SLTDateType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("BuySListNode::malloc");
return;
}
newnode->Data = x;
return newnode;
}
//打印链表
void SListPrint(SListNode* phead)
{
SListNode* cur = phead;
while (cur)
{
printf("%d->", cur->Data);
cur = cur->Next;
}
printf("NULL\n");
}
//尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
if (*pplist==NULL)
{
*pplist = BuySListNode(x);
(*pplist)->Next = NULL;
}
else
{
//查找尾部
SListNode* ptr = *pplist;
while (ptr->Next)
{
ptr = ptr->Next;
}
//插入
ptr->Next = BuySListNode(x);
ptr->Next->Next = NULL;
}
}
//头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
if (*pplist == NULL)
{
*pplist = BuySListNode(x);
(*pplist)->Next = NULL;
}
else
{
SListNode* ptr = BuySListNode(x);
ptr->Next = *pplist;
*pplist = ptr;
}
}
//尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
if ((*pplist)->Next == NULL)
{
//单个链表块
free(*pplist);
*pplist = NULL;
}
else
{
//2个及以上链表块
SListNode* ptr = *pplist;
while (ptr->Next->Next)
{
ptr = ptr->Next;
}
free(ptr->Next);
ptr->Next = NULL;
}
}
//头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* Del = *pplist;
*pplist = (*pplist)->Next;
free(Del);
Del = NULL;
}
//查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
SListNode* find = plist;
while (find && find->Data != x)
find = find->Next;
return find;
}
//在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* next = pos->Next;
SListNode* newnode = BuySListNode(x);
pos->Next = newnode;
newnode->Next = next;
}
//删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
assert(pos);
if (pos->Next == NULL)
return;
else
{
SListNode* NNext = pos->Next->Next;
free(pos->Next);
pos->Next = NNext;
}
}
//销毁
void SListDestroy(SListNode* plist)
{
assert(plist);
SListNode* Del = plist;
while (Del)
{
plist = plist->Next;
free(Del);
Del = plist;
}
}
七.结语
链表内容对于新手而言还是比较多的,坑也是无处不在,需要诸位的细心与耐心,除了单链表还有很多其他形式的,不过都大同小异,自己多手搓几遍就能弄清楚了,总之,我会和各位一起加油的!