目录
ID:HL_5461
引言
在开始动手写之前,我们先来了解链表是什么:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
当然,链表有单链表、双向链表、带头链表、循环链表等多种,这一篇我们只讨论无头非循环的单链表。该链表用图片形象化表示,大概长这样:
一、定义
不同于顺序表,链表我们只通过结构体定义一个结点,这个结点包含两个东西:一个是该节点存储的数据data,一个是指向下一个结点的指针next。我们的链表就是通过这些指针指向来把一个个结点连成一个链表。
图示
typedef int SLTDataType;//定义SLTDataType为int,方便以后修改存储的数据类型
typedef struct SLTNode
{
SLTDataType data;//结点数据
struct SLTNode* next;//指向下一个结点的指针
}SLTNode;
代码
二、开辟新结点
链表在没有存放任何数据时,只有一个指向空的指针,也就说,我们想要创建一个链表,只需要在主函数中输入“SLTNode* pList = NULL;”,我们就算已经创建好了一个尚未存放数据的链表。这似乎比顺序表方便多了,因为我们不需要写一个函数对它进行初始化了。但是,我们需要写一个开辟新结点的函数,同时,这个函数也方便了以后对于链表进行插入。
首先我们直接使用malloc函数为结点开辟一个结点结构体大小的空间,同时返回该结点的指针,将之赋值给newnode。当然,对于malloc函数开辟空间,很重要一点就是判断开辟是否失败,即指针是否成功,失败结束程序,成功我们就可以将要存储的数据存进新开辟的结点空间中的data里,同时为了避免使用野指针,将该结点的next置为空。
来看代码:
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//用malloc函数为结点开辟空间
if (newnode == NULL)//如果开辟失败
{
perror("BuySListNode");//打印开辟失败
exit(-1);//程序以非正常形式结束
}
else//如果开辟成功
{
newnode->data = x;//将数据存入data
newnode->next = NULL;//next指针置空
}
return newnode;//返回结点指针
}
三、插入
1.头插
直接上图讲解:
运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。
令新结点的next指向头结点指向的位置。
令头指针指向新结点。
插入结点指针改变的顺序很重要,我们只能先改变新结点的next令它指向头结点,再令头指针指向它,而不能先令头指针指向它,那样我们就找不到后面的链了。来,我们还是用图说话!
注意,下面这个是一个错误图例!!
这里,我们先改变的是pList指向的地址,不难发现,后面的一大长条链我们都找不到了。当然,这后果可不是单纯“找不到”这么简单,由于无法找到后面这些结点的地址,而这些结点又都是malloc动态开辟的,我们无法将其释放,这就造成了内存泄漏。嗯~恭喜你创造了一个电脑病毒~(bushi)
看了上面的解说你是不是觉得自己会了【奸笑】,好吧,那咱们来看一个错误样例:
//这是一个错误示范
void SLTPushFront(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
newnode->next = phead;//新结点的next指向头指针指向的结点
phead = newnode;//令头指针指向新节点
}
这段代码咋一看好像没啥问题,没毛病,都是咱前面讲的思路。来来来,觉得没毛病的让我先问你一个问题:pList的指向真的改变了吗?
许多人看到SLTNode* phead就觉得自己传了指针过去用phead接收,所以想当然地认为pList已经改变了,实际真的如此吗?我们画图来理解:
首先我们要明确一点:所谓指针指向某一块区域,实际是指该指针变量存放的是那块区域的地址。
这里,我们假设头结点地址为0x000000417,也就说SLTNode*类型的变量pList存放的值为0x000000417。此时我们调用SLTPushFront(头插)函数,同时该函数创建一个临时变量phead来存放pList的值,同样是0x000000417。
在执行函数过程中,我们修改了phead的值为新结点的地址,假设为0x000000410。此时改变的只是形参,并没有改变pList的值。
总结一下:虽然phead是SLTNode*类型的变量,是一个指针,但我们要改变的是pList这个指针变量的值,所以要传的是pList的地址,也就说要传一个二级指针。
学废了吗~来看正确代码:
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
newnode->next = *pphead;//新结点的next指向头指针指向的结点
*pphead = newnode;//令头指针指向新节点
}
2.尾插
对于尾插,我们有必要分两种情况讨论:一种是链表中没有结点的,一种是链表中有结点的。图解和代码我都会分这两种情况来讨论。
(1)没有结点
没有结点的情况其实和头插类似,话不多说,上图:
运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。
为了与前面的头插保持一致,我们不妨也令新结点的next指向头结点指向的位置。当然这一步其实没太大必要,因为无论变不变next都为NULL。
令头指针指向新结点。
我们先实现一下这部分代码,最后再对两部分代码进行合并。
当然,正如前面头插强调的,别忘了使用二级指针!
//这段代码不全
if (*pphead == NULL)//如果头指针指向空,即链表内无结点
{
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
newnode->next = *pphead;//新结点的next指向头指针指向的结点
*pphead = newnode;//令头指针指向新节点
}
(2)已有结点
没啥可说的,直接上图哈~
运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。
令新结点的next指向原来尾结点的next指针指向的位置。同样没啥必要,但是咱养成好习惯哈~
令原来尾结点的next指针指向新结点的位置。
但是,怎么找到尾结点的位置呢?链表不像顺序表,我们要想找到最后一个,需得设一个指针让它从头开始遍历,直到找到个结点的next为NULL才算找到了最后一个结点。我们看图。
创建一个SLTNode*类型的指针tail,刚开始令它等于*pphead也就是等于PList,指向头结点。
放入一个循环,令tail = tail->next,即指针不断向后一个结点走,直到最后一个结点停下,由此,循环结束的条件为tail->next = NULL。
嗯~当然我们还是先来看看坑~下面是一个错误示例:
//这是一个错误示范
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
SLTNode* tail = *pphead;//tail指向头结点
while (tail)//tail不为空时
{
tail = tail->next;//往下一个结点走
}
tail = newnode;
这段代码和我们之前讨论的区别就在tail最后所停的位置,我们所讨论的,tail停在了最后一个结点,这里的tail停在了最后一个结点的后一个位置。我们还是画图来辅助理解。
错误示范中,while循环的结束条件是tail==NULL,循环结束,此时tail停留在了最后一个结点的next位置上,也就说,tail的值为NULL。
此时我们再令tail = newnode,所改变的只是tail的指向,让它的值由原来的NULL变为了新结点的地址,并没有把它“连到”原来的链上。
所以,有一点还是有必要强调的:改变结构体,要使用结构体指针!
OK,来看正确代码:
//这段代码不全
else//链表内有结点
{
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
SLTNode* tail = *pphead;//tail指向头结点
while (tail->next)//tail的next不为空时
{
tail = tail->next;//往下一个结点走
}
newnode->next = tail->next;//新结点指向最后一个结点的next
tail->next = newnode;//最后一个结点指向新结点
}
(3)尾插总结
对于尾插,我们需要牢记必须分为已有结点和未有结点两种情况讨论!
话不多说,我直接把尾插的完整代码放出来:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
if (*pphead)//如果头指针指向空,即链表内无结点
{
newnode->next = *pphead;//新结点的next指向头指针指向的结点
*pphead = newnode;//令头指针指向新节点
}
else//链表内有结点
{
SLTNode* tail = *pphead;//tail指向头结点
while (tail->next)//tail的next不为空时
{
tail = tail->next;//往下一个结点走
}
newnode->next = tail->next;//新结点指向最后一个结点的next
tail->next = newnode;//最后一个结点指向新结点
}
}
3.中间插入
中间插入我们分成在pos位置前插入和在pos位置后插入两种情况讨论。对于前插和后插,我认为最大的不同就是一个需要遍历链表找pos位置一个不需要,还有一个有头插情况一个有尾插情况。当然,这些都让我们接下来逐个分析。
(1)在pos位置前插入
插入前,我们先要设置一个指针prev遍历链表来找pos位置,当然prev所停的位置有些讲究,我们后面画图讨论。然后我们将新开辟的结点的next指向pos,再将pos前面的指针指向新结点。
创建结构体指针prev,刚开始指向头结点,然后不断让prev=prev->next,让它在链表中遍历。
在pos的前一个结点停下,即跳出循环的条件为prev->next==pos。
这里要特别注意,跳出循环的条件不能是prev==pos,否则会出现下面这种情况:
若跳出循环的条件是prev==pos,就无法将pos结点的上一个结点的next指向新结点,因为我们不能知道上一个结点的地址,也就无法通过上一个结点的地址找到对应的next。链表指针只能顺着next的指向从前往后走,不能通过后一个找到前一个。
当prev停在了pos的前一个结点,此时我们再让 新结点的next指向pos。
然后让prev的next指向新结点。
你以为pos前插入结束了吗?恭喜你,想多了~我们不妨考虑一下,如果pos是头结点呢?也就说,我们得分情况,上面讨论的是pos不是头结点的情况,如果pos为头结点,我们得实现结点的头插。头插前面讲过,在此不再赘述,直接看代码。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);//pphead不为空
assert(pos);//pos不为空
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
if (*pphead == pos)//如果头指针指向pos,为头插
{
newnode->next = pos;//新结点的next指向pos结点
*pphead = newnode;//令头指针指向新节点
}
else//头指针不指向pos
{
SLTNode* prev = *pphead;//prev指向头结点
while (prev->next != pos)//prev的next不为pos结点时
{
prev = prev->next;//往下一个结点走
}
newnode->next = pos;//新结点指向最后一个结点的next
prev->next = newnode;//最后一个结点指向新结点
}
}
(2)在pos位置后插入
后插比前插方便得多,我们甚至不需要遍历链表。后插不可能出现头插情况,因为所插位置的前面至少存在一个结点,但可能存在pos为最后一个结点,即尾插情况,不过无需分开讨论。
直接看代码。
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);//pos不为空
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
newnode->next = pos->next;//newnode指向pos的下一个结点
pos->next = newnode;//pos指向newnode
}
四、删除
1.头删
我们首先使用一个结构体指针类型的变量head记录下来要删除的头结点。这里的记录是为了后面的释放空间。
改变头指针指向,令它越过头结点指向后一个结点 。因为这里需要改变头指针的值,所以这里的函数应该传二级指针。
释放head所指向的空间。
看代码:
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);//二级指针不为空
assert(*pphead);//链表不为空
SLTNode* head = *pphead;//创建指针变量使它指向头结点
*pphead = (*pphead)->next;//头指针指向头结点的下一个
free(head);//释放头结点空间
}
2.尾删
(1)只有一个结点
尾删和尾插一样,同样得考虑是否只有一个结点的问题,我们分情况来讨论,先来看只有一个结点的情况:
只有一个节点的情况其实就是头删,我们用tail指针记录结点位置然后修改头指针再对tail进行释放。当然这里其实可以直接将PList所指结点释放再对PList置空。但为了和之前保持一致,我们不妨多走两步。
修改PList指向。
释放tail空间。
来看只有一个结点情况的代码:
//这段代码不全
if ((*pphead)->next == NULL)//只有头结点一个结点
{
SLTNode* tail = *pphead;//令tail指针指向头结点
*pphead = (*pphead)->next;//指向后一个
free(tail);//释放tail空间
}
(2)有多个结点
再来看有多个结点的情况:
我们仍然是将tail指针从前往后遍历,直到找到倒数第二个结点。注意这里不可遍历到最后一个结点!
释放尾结点,并将tail的next置空。
来看这部分代码:
//这段代码不全
else//有多个结点
{
SLTNode* tail = *pphead;//令tail指针指向头结点
while (tail->next->next)//不是倒数第二个结点
{
tail = tail->next;//向后遍历
}
free(tail->next);//释放尾结点
tail->next = NULL;//tail的next指针置空
}
(3)尾删总结
直接看代码吧~
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);//二级指针不为空
assert(*pphead);//链表不为空
SLTNode* tail = *pphead;//令tail指针指向头结点
if ((*pphead)->next == NULL)//只有头结点一个结点
{
*pphead = (*pphead)->next;//指向后一个
free(tail);//释放tail空间
}
else//有多个结点
{
SLTNode* tail = *pphead;//令tail指针指向头结点
while (tail->next->next)//不是倒数第二个结点
{
tail = tail->next;//向后遍历
}
free(tail->next);//释放尾结点
tail->next = NULL;//tail的next指针置空
}
}
3.中间删除
(1)删除pos位置结点
删除pos位置结点还是要分删除头结点和删除中间结点两种情况讨论,前面已有详细说明,这里就一笔带过。
先来看删除头结点的情况,当然,嫌麻烦的也可以直接调用SLTPopFront函数。
令头指针指向pos的后一个结点。
释放pos结点。
再来看删除中间结点情况:
定义prev指针使其遍历到pos前一个。
prev的next指针指向pos的后一个结点。
释放pos结点。
look代码:
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);//二级指针不为空
assert(*pphead);//链表不为空
assert(pos);//pos不为空
if (pos == *pphead)//pos为头结点
{
*pphead = pos->next;//头指针指向pos的下一个结点
free(pos);//释放pos
}
else//pos为中间结点
{
SLTNode* prev = *pphead;//令prev指针指向头结点
while (prev->next != pos)//不是pos的前一个结点
{
prev = prev->next;//向后遍历
}
prev->next = pos->next;//prev指向pos的后一个结点
free(pos);//释放pos
}
}
(2)删除pos后结点
后删要简单的多,我们无需考虑头结点情况,不用分开讨论。
使用posNext指针记录pos的后一个结点,即要删除的结点。
使pos指向posNext的后一个结点。
释放posNext结点。
没啥可讲的,so easy~来看代码!
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);//pos不为空
assert(pos->next);//要删的结点不为空
SLTNode* posNext = pos->next;//posNext为pos的后一个节点
pos->next = posNext->next;//pos指向posNext的后一个结点
free(posNext);//释放posNext
posNext = NULL;//指针置空(可有可无)
}
五、查找
查找的话,直接一个指针从头遍历到尾,有就返回该值所在地址,没有则返回空指针。上代码叭~
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;//从头遍历
while (cur)//cur不为空
{
if (cur->data == x)//找到了
{
return cur;//返回此时地址
}
else//没找到
{
cur = cur->next;//往后找
}
}
//遍历结束仍未找到
return NULL;//返回空指针
}
六、修改
看代码:
void SLTModify(SLTNode* pos, SLTDataType x)
{
assert(pos);//pos不为空
pos->data = x;//将pos位置的数据修改为x
}
结尾
无头结点非循环单链表到此就结束啦~完整代码照例放在了我的码云,欢迎前来串门:
目前咱不确定我还会不会再开一篇文章总结一下单链表的所有代码(无他,我懒),如果更了,我会把链接放到下面哒~
Finally,若有错误,欢迎大家批评斧正!