大家好,我今天想给大家分享的是c++数据结构中的单链表 ,在这里我们将自己实现一个单链表的基本的操作,以及函数实现。我相信你看了一定会有所收获的。
一、单链表介绍
单链表是一种常见的数据结构,它由一系列结点组成,每个结点包含两个部分:一个是存储数据的数据域,另一个是存储下一个结点地址的指针域。链表的第一个结点称为头结点,最后一个结点的指针域通常为空(在C语言中通常用NULL表示),表示链表的结束。
单链表的特点是:
1.结点之间的连接方向是单向的,即从头到尾。
2.每个结点包含数据部分和一个或多个指向其他结点的指针。
3.链表的头结点存储在链表的开始位置,不一定是第一个存储数据的结点。
4.链表的最后一个结点的指针指向空(NULL),表示链表结束。
5.单链表只可以顺序访问,不能直接访问任意位置的元素。
在C语言中,实现单链表通常需要使用结构体来定义链表结点的类型,然后通过动态内存分配函数(如malloc)来创建链表结点,并通过指针来建立结点之间的关系。
二、单链表节点组成代码实现
在单链表介绍中我们知道一个单链表包含一个数据域和指针域,我们就可以通一个结构体来存下这两部分。
typedef int SLDatatype;
typedef struct SLlistNode
{
SLDatatype data;
struct SLlistNode* next;
}SLTNode;
上述第一行我们对int重命名,这个作用就是方便我们以后修改数据类型,我们只需把int修改为我们需要的类型即可。我们next是结构体类型指针,因为我们的next指向的是下一个节点,下一个节点也是结构体,所以我们需要使用结构体类型指针。
三、单链表的基本接口函数实现
(1)SLBuyNode:开辟一个节点的空间。
在进行基本操作前提都是需要节点的,所以我们能将开辟节点的操作写为一个函数,这样在每次使用前调用即可。
SLTNode* SLBuyNode(SLDatatype x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("开辟空间失败");
exit(0);//直接退出程序;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
上述函数返回的就是一个有效节点,我们需要知道malloc函数开辟失败返回的是一个NULL,所以我们开辟后需要做判断。
(2)SLPshBack :尾插一个节点。
思路: 首先我们插入一个节点我们应该调用开辟空间的函数申请一块空间,其次分两种情况讨论:
1.如果链表是个空链表,那么我们直接把申请的节点空间直接给头节点。
2.如果非空我们就需要遍历单链表找到最后一个节点,然后把他的next指针指向我们申请的系节点即可。
void SLPushBack(SLTNode** pphead, SLDatatype x)
{
SLTNode* newnode = SLBuyNode(x);
SLTNode* ptail = *pphead;
if (*pphead == NULL) *pphead = newnode;
else
{
while (ptail->next != NULL) ptail = ptail->next;
ptail->next = newnode;
}
}
注意:单链表头节点本是一块我们从操作系统申请一块内存,本身就是一个SLTNode* 类型的指针。我们知道要使得实参改变我们就要采用传址的方式传参,不然无法使实参改变。显然这里要使得实参的头节点在链接一个节点,就是使得实参改变,它本身就是一个指针,所以形参我们就需要二级指针来接受实参,进行传址来改变实参。所以我们形参用的是SLTNode**二级指针。在本文章之后的二级指针也是同样的道理。
(3)SLPushfront:头插
这个实现就相对来说比较简单,就是把申请节点的next指向头节点,在更新头节点。其实当头节点为空的时候就是尾插。
void SLPushFront(SLTNode**pplist,SLDatatype x)
{
if(*pplist == NULL) SLPushBack(pplist,x);
else
{
SLTNode*newnode = SLBuyNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
}
注:上述代码只用else里边内容也可以,只是个人习惯这么写。
(4)SLPopBack:尾删
思路: 首先判断头节点是否为空,是的话我们把头节点释放即可,否则不判断在后面我们再找后面节点时候会出现对空指针解引用,这个是错误的。如果不为空我们就需要找到最后一个节点将其释放,然后把倒数第二个节点next置为空。如果不置为空,那么会导致链表的最后一个节点不再指向任何有效节点。这意味着你无法从链表的头部遍历到链表的尾部,因为你不知道链表的尾部在哪里。这在逻辑上是一个错误,因为链表的末尾应该有一个指向null的指针来标识链表的结束。在遍历的时候就会导致死循环。
我们来看图理解一下
void SLPopBack(SLTNode**pplist)
{
assert(*pplist);
SLTNode*ptail = *pplist;
SLTNode*pre = *pplist;
if(*pplist == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
while(ptail->next != NULL)//找到最后一个节点
{
pre = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
pre->next = NULL;
}
}
(5)SLPopFront:头删
思路:头删比较简单,我们只需要把头节点的下一个节点用一个变量储存起来,然后我们把头节点释放,然后把头节点指向我们刚才变量存起来的那个地址即可;
void SLPopFront(SLTNode**pplist)
{
if(*pplist == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SLTNode*cur = (*pplist)->next;//储存下一个节点;
free(*pplist);
*pplist = cur;//把头节点指向下一个节点;
}
}
注:上述代码用else里边的代码也可完成这个接口实现
(6)SLInsertposfront:在指定位置之前插入一个节点
思路:
1.如果pos就是头节点,那么在头节点前插入一个节点,那就是头插,直接调用头插函数完成即可。
2.我们需要找到pos位置前一个节点,通过这个节点建立与新节点的关系,那么就可以将这个新节L链接成功。
void SLInsertposfront(SLTNode**pplist,SLTNode*pos,SLDatatype x)
{
if(*pplist == pos) SLPushFront(pplist,x);
else
{
SLTNode*newnode = SLBuyNode(x);
SLTNode*cur = *pplist;
while(cur->next != NULL)
{
cur = cur->next;
}
newnode->next = pos;
cur->next = newnode;
}
}
(7)SLInsertposback:在pos位置后插入一个节点
思路:改变pos,newnode,pos->next之间的节点关系即可。
void SLInsertposback(SLTNode**pplist,SLTNode*pos,SLDatatype x)
{
SLTNode*newnode = SLBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
(8)SLEraseposfront:删除指定位置节点
思路:找到pos位置前一个节点,使其next指向pos指向的next然后释放pos即可
void SLErasepos(SLTNode**pplist,SLTNode*pos)
{
if (pos == *pplist)
{
SLPopFront(pplist);
}
else {
SLTNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
(9) SLEraseposback:删除pos位置后面一个节点
思路:释放pos->next,在让pos->next = pos->next->next即可。
void SLEraseposback(SLTNode**pplist,SLTNode*pos)
{
SLTNode*del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
(10)SLDestory:销毁单链表
思路:遍历单链表进行销毁
void SLDestory(SLTNode**pplist)
{
SLTNode*pcur = *pplist;
while(pcur)
{
SLTNode*tmp = (*pplist)->next;
free(pcur);
pcur = tmp;
}
*pplist = NULL;
}
四、测试函数的正确性
#include"SLlist.h"
void test()
{
SLTNode* plist = NULL;
SLPushBack(&plist, 10);
SLPushBack(&plist, 20);
SLPushBack(&plist, 30);
SLPushBack(&plist, 40);
SLPushBack(&plist, 50);
cout << "尾插结果";
SLprint(plist);cout << "\n";
SLPushFront(&plist,1000);
SLPushFront(&plist,2000);
cout << "头插结果";
SLprint(plist);cout << "\n";
SLPopBack(&plist);
cout << "尾删一次";
SLprint(plist); cout << "\n";
SLPopFront(&plist);
cout << "头删一次";
SLprint(plist);cout << "\n";
cout << "查找1000 ";
SLTNode*find = SLFind(plist,1000);
if(find == NULL) cout << "没找到!\n";
else cout << "找到了!\n";
cout << "在1000前插入5000结果 ";
SLInsertposfront(&plist,find,5000);
SLprint(plist);cout << "\n";
cout << "在1000后插入6000结果";
SLInsertposback(&plist,find,6000);
SLprint(plist);cout << "\n";
cout << "删除1000前的节点结果 ";
SLErasepos(&plist,find);
SLprint(plist);cout << "\n";
SLTNode*find1 = SLFind(plist,5000);
cout << "删除5000后面节点结果 ";
SLEraseposback(&plist,find1);
SLprint(plist);cout << "\n";
SLDestory(&plist);
}
int main()
{
test();
return 0;
}
运行结果图
我们可以看到我们写的函数是正确的。
五、致谢
感谢各位读者的阅读,如果这边文章对你有帮助的话,记得评论支持一下哦。让我们共同进步吧。 如果有存在错误或者更好的思路也请各位大佬对我进行指正,再次感谢您的阅读。