携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
前言
在介绍完复杂度以后,接下来就是数据结构的学习了,先从最简单的线性表入手,本文是基于C语言实现的。
本文就来分享一波作者对数据结构单链表的学习心得与见解。本篇属于第三篇,书接上文,继续介绍线性表的单链表的一些内容。
笔者水平有限,难免存在纰漏,欢迎指正交流。
单链表(续)
单链表修改
修改的前提是什么?得先找到要修改的元素的位置所在,所以可以使用查找函数,找到位置后直接把值改了就好。
void ModifySLList(SLLNode* pHead, SLLDataType tar, SLLDataType mod)
{
assert(pHead);
SLLNode* tmp= FindSLList(pHead, tar);
tmp->data = mod;
}
单链表插入
一般讲插入都是在某个位置前面插入,设某位置为pos,能不能在pos后面插入呢?都可以,只不过对于单链表来说前插麻烦些,为什么?因为单链表的关系是单向的啊,在pos前面插入就必须要有pos前一个结点的信息,而pos无法得到前一个结点的位置。不过虽然麻烦点,但是还是可以实现的,我们先来看前插。
如何得到前面结点的信息?创建指针prev,只要prev->next不为pos也就是下一个结点不是pos所在结点就继续往后找,直到找到为止,找到的话prev就指向pos前一个结点了。这时候插入新结点,先把pos的值给新结点的next指针,这样新结点就指向pos位置的结点,再把新结点的地址给prev的next指针,这样prev位置的结点就指向新结点了,这样就完成了插入。要注意有可能找不到pos,这种情况一般是pos传的有问题,属于异常情况。同时pos不能为NULL,用assert(pos)检测一下即可。
那要是pos指向的是第一个结点呢?那不就变成头插了吗,直接复用头插函数就行了。
void InsertSLList(SLLNode** ppHead, SLLNode* pos, SLLDataType tar)
{
asseert(ppHead);
assert(pos);
if(*ppHead == pos)
PushFrontSLList(ppHead, tar);
else
{
SLLNode* prev = *ppHead;
SLLNode* newNode = CreateSLLNode(tar);
while(prev->next != pos)
{
prev = prev->next;
assert(prev);
}
newNode->next = pos;
prev->next = newNode;
}
}
那么后插如何实现呢?其实更简单些,先把pos->next的值给新结点的next指针,让新结点指向pos后面的结点,再把新结点的地址给pos的next指针,让pos位置的结点指向新结点,这样就完成了插入。
void InsertAfterSLList(SLLNode* pos, SLLDataType tar)
{
assert(pos);
SLLNode* newNode = CreateSLLNode(tar);
newNode->next = pos->next;
pos->next = newNode;
}
在pos位置之前插入需要改变pos位置前一个结点的指向关系,就要拿到该结点,由于单链表的结构特性,pos位置的结点无法找到前一个结点,就需要从头去找pos位置前一个结点,比较麻烦。
而要是在pos位置之后插入,pos位置的结点就是要插入结点的前一结点,这时候要修改指向关系就很方便了。
进阶思考
在pos位置前插入,要求时间复杂度为O(1) 。
思路:替换法插入
这要求什么意思呢?我们原来要在pos位置的前面插入结点是不是要先找到pos位置前面的结点呀,那可不可以不找就实现插入了呢?
我们先在pos位置后面插入一个结点,再把pos位置结点的data值和新插入结点的值交换一下,这样是不是就可以了呢?妙啊~🤩
void InsertSLList(SLLNode* pos, SLLDataType tar)
{
assert(pos);
SLLNode* newNode = CreateSLLNode(pos->data);
newNode->next = pos->next;
pos->next = newNode;
pos->data = tar;
}
算是使用了InsertAfterSLList的思路而克服了InsertSLList的一些缺点,比如说时间复杂度更低了。
缺陷:无明显缺陷
单链表移除
其实单链表移除也有两种,一是移除pos当前位置结点,二是移除pos位置后面一个结点。
先看第一种。有没有可能pos指向的是第一个结点?那这时候是不是就变成头删啦?直接复用头删函数。当pos不指向头结点时,如何移除结点?可以参考一下尾删的思路,我们要找到pos前一个结点,然后把pos的值先拷贝到一个指针中,再把pos->next的值给前一个结点的next指针,这样就改变了前一个结点的指向,越过pos位置的结点转而指向pos后面一个结点,最后再把pos位置的结点释放即可。要注意一下,链表为空时就不能再删了,所以要检测一下assert(*ppHead)。
void EraseSLList(SLLNode** ppHead, SLLNode* pos)
{
assert(ppHead);
assert(pos);
assert(*ppHead);
if(*ppHead == pos)
PopFrontSLList(ppHead);
else
{
SLLNode* prev = *ppHead;
while(prev->next!= pos)
{
prev = prev->next;
assert(prev);
}
prev->next = pos->next;
free(pos);
}
}
第二种删后面的就比较简单了,也是可以参考尾删,先把pos后面一个结点的地址放到创建的del指针,把pos下一个结点的next指针给pos位置结点的的next指针,从而让pos位置结点的next指针指向下下个结点,然后再通过del指针释放pos后面一个结点。要注意,pos指向的是尾结点时就无法删除了,尾结点后面还有结点吗?没有了。所以要检测一下,assert(pos->next) 。
void EraseAfterSLList(SLLNode* pos)
{
assert(pos);
assert(pos->next);
SLLNode* del = pos->next;
pos->next = pos->next->next;
free(del);
}
删除pos位置的结点同样需要拿到前一结点去修改指向关系,由于单链表的结构特性,pos位置的结点无法找到前一个结点,就需要从头去找pos位置前一个结点,比较麻烦。
而要是删除pos位置后一个结点,那就方便多了,pos位置的结点就是要删除结点的前一结点,直接就可以修改指向关系了。
进阶思考
删除pos位置结点,但是要求是时间复杂度为O(1) 。
思路: 替换法删除
这个要求是什么意思呢?原来我们要删除pos位置结点是不是还要去找到pos前面的结点?是不是就要从头去找?那可不可以不用pos前面的结点而实现pos位置结点的删除呢?
在这种情况下,由于单链表本身的结构特性,我们无法直接删除pos位置的结点,但是我们不是可以删除pos后面的一个结点吗?那可不可以先把pos位置的结点的data值和它后面结点的值交换一下,然后再把它后面结点给删掉,来一手“狸猫换太子”。
void EraseSLList(SLLNode* pos)
{
assert(pos);
assert(pos->next);
SLLDataType tmp = pos->data;
pos->data = pos->next->data;
pos->next->data = tmp;
SLLNode*del = pos->next;
pos->next = pos->next->next;
free(del);
}
本质上就是EraseAfterSLList的改进版,所以也有着相同的缺陷。
缺陷:pos不能是尾结点。
单链表打印
把每个结点的data打印出来,直接像找尾一样向后遍历,遇到NULL打印NULL;
void PrintSLList(SLLNode* pHead)
{
while (pHead)
{
printf("%d->", pHead->data);
pHead = pHead->next;
}
printf("NULL\n");
}
单链表销毁
单链表的销毁就是要把剩下的所有结点全部释放掉,怎么实现?定义两个指针cur和next,一前一后,如果只定义一个指针cur,那么释放完当前结点后就找不到后面的结点了,所以在释放之前要先把下一个结点的地址放到next指针,等释放完后再把地址转交给cur,再向后移动释放后面的结点,全部结点释放完后要记得让头指针指向NULL。链表为空不用担心,因为这种情况下函数什么也不干。
void DestorySLList(SLLNode** ppHead)
{
assert(ppHead);
SLLNode* cur = *ppHead;
SLLNode* next = cur->next;
while (cur != NULL)
{
next = cur->next;
free(cur);
cur = next;
}
*ppHead = NULL;
}
带头单链表
这里的头指的是哨兵头结点,里面存的值不是有效数据,该结点只是作为辅助结点。
设计为带头单链表就不需要在头插尾插和插入移除等函数中传入二级指针了,因为不会改变头指针。
单链表实际运用中很少带头,OJ题也基本不带头,所以相对来说不怎么深入带头的单链表,有些题可能会用到带哨兵结点的思路。
总结
单链表只适合头插头删,时间复杂度都是O(1),其他的操作其实都不太高效。所以什么时候用单链表呢?只需要头插、头删或者只用于作为其他数据结构的子结构时比较适用,不然一般都不会单独使用。
如果想要做到任意位置高效插入删除,可以考虑双向链表。
刷题推荐
- 删除链表中等于给定值 val 的所有结点。 203. 移除链表元素 - 力扣(LeetCode)
- 反转一个单链表。206. 反转链表 - 力扣(LeetCode)
- 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则 返回第二个中间结点。876. 链表的中间结点 - 力扣(LeetCode)
- 输入一个链表,输出该链表中倒数第k个结点。 链表中倒数第k个结点牛客题霸牛客网 (nowcoder.com)
- 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有 结点组成的。21. 合并两个有序链表 - 力扣(LeetCode)
- 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结 点之前 。链表分割牛客题霸牛客网 (nowcoder.com)
- 链表的回文结构。链表的回文结构牛客题霸牛客网 (nowcoder.com)
- 输入两个链表,找出它们的第一个公共结点。160. 相交链表 - 力扣(LeetCode)
- 给定一个链表,判断链表中是否有环。141. 环形链表 - 力扣(LeetCode)
- 给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL。142. 环形链表 II - 力扣(LeetCode)
- 给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点 或空结点。要求返回这个链表的深度拷贝。 138. 复制带随机指针的链表 - 力扣(LeetCode)
以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~