Hello各位小伙伴们,上期我们讲解了顺序表,今天让我们一起来学习一下单链表吧。
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
图中指针变量 plist保存的是第一个结点的地址,我们称plist此时"指向"第一个结点,如果我们希望plist"指向"第二个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点。
结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码:
cpp
struct SListNode
{
int data;//每个节点中要保存的数据
struct SListNode*next;//每个节点中要包含下一个节点的地址
}
实现单链表
和上期我们讲解的实现顺序表相同我们在实现单链表的时候创建3个文件,一个头文件,两个.C文件,实现单链表我们从如下几个方面来完成!
头文件:
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDatatype;
typedef struct SListNode
{
int data;
struct SListNode* next;
}SLTNode;
//链表的打印
void SLTPrint(SLTNode* phead);
//链表的尾插
void SLTPushBack(SLTNode** phead,SLDatatype x);
//链表的头插
void SLTPushFront(SLTNode** pphead, SLDatatype x);
//链表的尾删
void SLTPopBack(SLTNode** pphead);
//链表的头删
void SLTPopBack(SLTNode** pphead);
//链表的查找
SLTNode* SLTFind(SLTNode* pphead, SLDatatype x);
//在指定位置插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDatatype x);
//在指定位置之后插入数据
void SLTInsertBack(SLTNode* pos, SLDatatype x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseBack(SLTNode* pos);
//链表的销毁
void SListDestory(SLTNode** pphead);
1.链表的打印
实现链表的打印我们要遍历链表,同时创建一个新的结构体变量来记录头节点。
代码如下:
cpp
void SLTPrint(SLTNode* phead)
{
SLTNode* pur = phead;
while (pur != NULL)
{
printf("%d->", pur->data);
pur = pur->next;
}
printf("NULL\n");
}
第一次学习单链表的同学一定要注意这里的pur = pur->next,这里是让pur这个节点走到下一步的关键!
2.链表的尾插
大致逻辑在进行尾插的时候注意要申请新的空间来插入数据,同时要分为两种情况 1>.当链表不为空的时候,需要将链表遍历到最后一个节点的位置进行尾插。2>.链表为空的时候,直接将插入即可。
在进行链表的尾插操作时候我们注意到函数参数传递的变量是二级指针,我们先来看看传递一级指针的时候。
cpp
void SLTPushBack(SLTNode** phead,SLDatatype x);
cpp
void SLTPushBack(SLTNode*phead, SLTDatatype x)
{
SLTNode* newnode = SLTBuyNode(x);
//链表为空的情况下
if (phead == NULL)
{
phead = newnode;
}
else {
//链表不为空的情况下,需要先遍历找到尾节点
SLTNode* plist = phead;
while (plist->next != NULL)
{
plist = plist->next;
}
//申请新的节点空间,并将新的节点空间插入到为节点
plist->next = newnode;
}
}
让我们进行调试!
在进行调试的过程中我们将函数的实参部分与形参部分分别进行了调试,同时不仅仅将尾插函数进行调试也将空间的申请包含进来。我们发现随着程序一步步进行形参如我们所预料的那样值发生了变化,而实参部分并没有发生变化。这时我们就需要考虑是否是传参发生了问题!
将函数传参部分改为二级指针!
我们发现传递二级指针的时候就会实现实参的改变!
3.链表的头插
进行头插操作与尾插操作类似,不同之处是不用在讨论首节点为NULL的情况!
cpp
void SLTPushFront(SLTNode** pphead, SLTDatatype x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//链表不为空的情况下
newnode->next = *pphead;
*pphead = newnode;
//链表为空的情况下也是如此
}
4.链表的尾删操作
在进行链表尾删的时候我们要注意不能直接将尾节点释放掉,因为在未节点的头一个节点之前包含尾节点的指针,直接释放的化会导致野指针。
我们应该在创建一个变量SLTNode*perv在进行遍历的时候记录尾节点的前一个节点,将perv->next与尾节点同时释放掉。
代码操作:
cpp
//链表的尾删
void SLTPopBack(SLTNode** pphead)
{
//值得注意的是我们不能对空节点进行尾删
assert(*pphead && pphead);
if ((*pphead)->next == NULL)//当只剩下一个节点的时候
{
free(*pphead);
*pphead = NULL;
}
else {
//在遍历链表找尾节点的时候我们要找到尾节点的前一个节点
SLTNode* perv = *pphead;
SLTNode* ptail = perv->next;
while (ptail->next != NULL)
{
perv = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
perv->next = NULL;
}
}
5.链表的头删
在进行链表的头删操作时,要注意创建新的变量是删除操作能进行下去。
cpp
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = (*pphead)->next;//创建新的变量记录头节点的下一个指针
free(*pphead);
*pphead = pcur;
}
6.链表的查找
在进行链表的查找操作中我们要遍历链表
cpp
SLTNode* SLTFind(SLTNode* pphead, SLTDatatype x)
{
//直接遍历查找即可
SLTNode* pcur = pphead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
我们在test.c文件中进行测试:
cpp
void test06()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTNode* ret = SLTFind(plist,5);
if (ret == NULL)
{
printf("没找到了");
}
else {
printf("找到了");
}
}
int main()
{
//test01();//手动创建一个新节点
//test02();//尾插
//test03();//头插
//test04();//尾删
//test05();//头删
test06();//查找
return 0;
}
同时我们要注意一下函数的返回类型。
7.在指定位置之后插入数据
执行此操作我们不需要遍历链表,但要注意赋值的顺序。如果搞不懂的话可以创建一个临时变量来保存。
cpp
//在指定位置之后插入数据
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
SLTNode* newnode = SLTBuyNode(x);
newnode ->next= pos->next;
pos->next = newnode;
}
8.在指定位置插入数据
我们要遍历一遍链表然后找到pos之前的位置,进行节点的插入。
cpp
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
assert(pphead && pos && *pphead);//要找到pos节点和pos节点的prev节点
//当pos是头节点的时候,相当于头插
if (pos == *pphead)
{
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
else {
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos;
prev->next = newnode;
}
}
9.删除指定位置的节点
删除指定位置的节点,我们需要遍历链表找到pos之前的节点,同时要注意头节点的删除。
cpp
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
SLTNode* prev = *pphead;
//要注意头删
if (pos == *pphead)
{
SLTPopFront(pphead);
}
//需要遍历pos节点的头一个节点
else {
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
}
}
10.链表的销毁
进行链表的销毁:我们在申请空间的时候是不连续物理逻辑申请,所以在销毁时我们要对链表进行遍历销毁,同时我们要注意对头节点的销毁。
cpp
void SListDestory(SLTNode** pphead)
{
SLTNode* perv = *pphead;
while (perv)
{
SLTNode* next = perv->next;
free(perv);
perv = next;
}
free(*pphead);
*pphead = NULL;
}
ok,今天单链表的内容就到这里啦,各位小伙伴我们下期再见!