一、前言
大家最开始接触单链表时,大多都是用不带哨兵位的方法实现的。不带哨兵位时我们通常需要用到二级指针去接收所定义的SLNode*phead的地址才能实现自己想要的,那为什么带哨兵位用一级指针就可以呢?我想大家对此或多或少都会有些疑惑,甚至自己也想不通要怎样实现带哨兵位的单链表。我接下来对带哨兵位的单链表进行实现并对上述问题进行讲解。
二、带哨兵位单链表的实现
Fir:创建头文件、函数实现源文件、主函数源文件
这都是程序员敲代码的老三样了
Sec:头文件的初始准备
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
typedef struct SList
{
SLDataType val;
struct SList* next;
}sl;
之后我们在函数实现的时候要现在头文件中声明,再去实现,方便主函数调用。
Thir:函数实现
1、创造新节点
sl * CreatNewNode(SLDataType x)
{
sl* newnode = (sl*)malloc(sizeof(sl));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
我们创造一个新节点,就要为它申请一个空间,用malloc函数来申请
如果申请失败,perror(''malloc fail'')打印出申请失败的原因
并且exit(-1)会向主机环境返回不成功的终止状态。
如果成功,对其进初始化并且return返回
我们在插入节点时都会用到这个函数,并且,我们对这个单链表的要求是需要带哨兵位的,我们需要自己创建一个哨兵位,也会用到这个函数来创建。
2、初始化函数
sl* InitSList()
{
sl* phead = CreatNewNode(-1);
phead->next = NULL;
return phead;
}
我们既然所要求的是带哨兵位的单链表,那么我们希望的是它本来就有的,既然没有,那我们在刚开始就要自己创建,所以我们在初始化的时候,申请一个哨兵位出来,并且对这个哨兵位里面的值进行赋值为-1(随机值)。
所以为什么带哨兵位的单链表可以用一级指针?
重点来了 ,我们在函数实现,比如头插、尾插的过程中。由于不带哨兵位的单链表我们在主函数会创建一个头节点,进而会对这个头节点里面的值进行改变,既然涉及到改变,我们都知道,形参只不过是实参的临时拷贝,在函数内修改过后出作用域就销毁了,并不会对实参有影响,所以当我们要改变实参的值时,我们就需要将实参的地址传过去,找到它的地址就可以修改了,所以不带哨兵位的单链表才会运用到二级指针。
我们创建的哨兵位里面存在一个随机值(我们上面给的-1),我们也不会进行修改,哨兵位的永远指向下一节点,我们头插、尾插从头到尾,都不会对哨兵位有任何影响,所以我们就不需要传过去它的地址。所以我们就用一级指针就可以了。
3、打印函数
void SListPrint(sl*phead)
{
assert(phead->next);
sl* cur = phead->next;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
phead是我们的哨兵位,真正链表的开始在它的下一个位置,所以我们断言时,判断的是phead->next是否为空。
4、尾插函数
void InsertPushBack(sl* phead, SLDataType x)
{
sl* newnode = CreatNewNode(x);
if (phead->next == NULL)
{
phead->next = newnode;
}
else
{
sl* cur = phead->next;
while(cur->next != NULL)
{
cur = cur->next;
}
cur->next = newnode;
}
}
创造一个新节点后,我们首先判断哨兵位指向的下一个节点是否为空,是的话就直接插入,不是的话就要去找这个链表的尾巴,在最后插入。
找尾巴的时候,定义一个结构体指针cur,将哨兵位指向的下一个节点赋给cur,用cur来找尾巴。因为我们的哨兵位是不能改变的,当找到尾巴(cur->next==NULL)时,直接将让cur->next==newnode。
5、头插函数
void InsertPushFront(sl* phead, SLDataType x)
{
sl* newnode = CreatNewNode(x);
if (phead->next == NULL)
{
phead->next = newnode;
newnode->next = NULL;
}
else
{
sl* cur = phead->next;
phead->next = newnode;
newnode->next = cur;
}
}
头插也是一样,我们创造一个新节点后,判断哨兵位指向的下一个节点是否为空,如果为空,那么就直接phead->next=NULL,如果不为空,我们需要先定义一个结构体指针cur将phead->next存起来,因为如果我们直接将phead->next=newhead的话,我们就找不到原来phead指向的下一个节点了,所以插入之前我们需要将phead->next存起来。
5、尾删函数
void PopBack(sl* phead)
{
assert(phead->next);
sl* cur = phead->next;
while (cur->next->next != NULL)
{
cur = cur->next;
}
free(tail->next);
cur->next = NULL;
}
先断言,目的是为了防止phead->next=NULL,空了就不能再删除了
如果不为空,就开始找尾巴,操作与尾插相同。
找到之后我们将它free释放掉就可以了
6、头删函数
void Popfront(sl* phead)
{
assert(phead->next);
sl* cur = phead->next->next;
free(phead->next);
phead->next = cur;
}
断言与尾删相同。
但是需要注意的是,这里删除第一个节点时,只需要将phead直接连接第二个节点就好了。
先定义一个结构体指针cur,将第二个节点赋给它,再释放掉phead->next。最后直接将phead->next=cur就好了。
7、查找函数
sl* SListFind(sl* phead, SLDataType x)
{
assert(phead->next);
sl* cur = phead->next;
while (cur)
{
if (cur->val == x)
{
return cur;
}
else
{
cur=cur->next;
}
}
return NULL;
}
这里的查找并不难,暴力遍历查找就可,判断条件就是查看节点中的值是否等于x,最后返回该节点。
8、在任意位置插入一个节点
void SLTInsert(sl* phead, sl* pos, SLDataType x)
{
assert(pphead);
assert(pos);
if (phead->next == pos)
{
InsertPushFront(phead, x);
}
else
{
sl* prev = phead->next;
while (prev->next != pos)
{
prev = prev->next;
}
sl* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
这里用两个assert函数来断言是因为我们要保证pos节点是链表中的一个有效节点
9、删除任意节点
void SLTErase(sl* phead, sl* pos)
{
assert(phead);
assert(pos);
if (phead->next == pos)
{
Popfront(phead);
}
else
{
sl* prev = phead->next;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
10、销毁函数
void SLTDestroy(sl* phead)
{
assert(phead->next);
sl* cur = phead->next;
while (cur)
{
sl* next = cur->next;
free(cur);
cur = next;
}
phead = NULL;
}
三、总结
我在这里主要想说的是为什么带哨兵为的单链表可以不用二级指针,其他函数的实现其实都与不带哨兵位的单链表函数实现差别不大。
这方面涉及到的知识是指针,我觉得这是C语言学习中最让人头疼的一个知识点,但是它真的很重要。