目录
0.前言
1. 移除链表元素
2. 反转链表
2.1 方法一(遍历反转链接关系)
2.2 方法二(节点头插构造新链表)
3.链表的中间节点
4. 链表中倒数第k个节点
5. 总结
0.前言
本文所有代码都已传入gitee,可自取
3链表OJ题p1 · onlookerzy123456qwq/data_structure_practice_primer - 码云 - 开源中国 (gitee.com)https://gitee.com/onlookerzy123456qwq/data_structure_practice_primer/tree/master/3%E9%93%BE%E8%A1%A8OJ%E9%A2%98p1
自我们在上一篇博客结束对单链表的实现之后,我们知道单链表的缺陷是很大的,单纯的单链表的增删查改的意义不大,在现实当中我们也很少使用效率极低的单链表作为主要的数据结构进行存储。可是我们为什么要重视单链表呢?
1. 正是由于单链表的天生缺陷,单链表这种数据结构衍生处许多的OJ题,许多的笔试题都是有关于单链表的。
2. 单链表虽然单拿出来不能打,但是单链表更多的是去作为更加复杂的数据结构的子结构,如哈希桶,邻接表。
本篇博客就开启我们单链表经典OJ刷题的第一弹:
1. 移除链表元素
203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/
题目要求我们把单链表所有为val的节点都删除,之后返回新链表的头结点。
直接进行遍历单链表一遍,进行暴力删除即可:
如果要删除一个节点cur,需要我们free掉cur这个节点,然后紧接着处理链接关系,把cur的前一个节点prv链接到cur的next下一个节点,而cur的next需要在我们free到cur节点之前保存下来,因为释放cur之后我们就不能通过cur节点找到next节点了。
同时cur的prv节点需要我们在上一次迭代的时候对prv进行保存,如果没有进行删除那上一次的cur就是这一次的prv;如果上一次进行了删除,那这一次的prv节点仍保持不变,例如我们1->2->6->3,prv为2,cur为6,cur_next为3,也即当我们删除到cur的时候,删除完之后链表变为1->2->3,此时cur往下迭代为3,prv此时仍然是2。
可是处理链接关系的情况仅仅是prv链接到cur_next这么简单吗?我们需要考虑特殊情况,prv事实上是不一定存在的,也即我们删除的节点cur是头结点的时候,此时处理链接关系就不再需要prv链接到cur_next,因为此时prv压根不存在,是NULL,此时就相当于头删,这样处理链接关系就没有压力了,说实话此时就只需更新head即可。
代码如下所示:
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* cur = head,*prv = NULL;
//cur一直遍历到空
while(cur)
{
//记录方便下次更新
struct ListNode* cur_next = cur->next;
//找到要删除的节点
if(cur->val == val)
{
//要删除的节点是头结点
if(prv == NULL)
{
free(cur);
//更新head
head = cur_next;
}
else //删除的节点此时是非头节点prv->cur->next
{
free(cur);
prv->next = cur_next;
}
//删除之后prv保持不变
}
else //该节点不是删除节点
{
prv = cur;
cur = cur_next;
}
//迭代更新
cur = cur_next;
}
return head;
}
2. 反转链表
剑指 Offer II 024. 反转链表 - 力扣(LeetCode)https://leetcode.cn/problems/UHnkqh/
这个题有两种方法。
2.1 方法一(遍历反转链接关系)
方法一:可以直接暴力遍历一遍,依次迭代,把prv->cur变成prv<-cur,反转链接关系,即可完成反转链表。
这里需要记住两个点,第一个点是cur和prv的更新,我们下一次反转是把prv变成现在的cur,cur变成cur_next,开始新一轮反转,这需要我们在反转之前记录下cur_next。第二点是记得更新head,因为我们反转之后,新的head其实是原链表的tail尾节点。
方法一 代码如下:
ListNode* reverseList(ListNode* head) {
//遍历该链表的所有节点,prv->cur => prv<-cur
ListNode* prv = nullptr;
ListNode* cur = head;
while(cur)
{
//记录cur_next
ListNode* cur_next = cur->next;
//进行反转
cur->next = prv;
//迭代更新
prv = cur;
cur = cur_next;
}
//原链表最后一个节点为现在的首节点
return prv;
}
2.2 方法二(节点头插构造新链表)
我们只需要把原链表的节点从头依次拆卸,头插到新链表plist中。最后新链表plist呈现的就是反转后的原链表。
不过我们头插就需要注意一个点,那我们头插的方法就是新来的节点作为plist新的head,让新头节点链接原有的plist链表实体。
ListNode* reverseList(ListNode* head) {
ListNode* cur = head;
ListNode* plist = nullptr;
while(cur)
{
ListNode* cur_next = cur->next;
//cur节点头插到plist
cur->next = plist;
plist = cur;
//迭代遍历下一个节点
cur = cur_next;
}
return plist;
}
3.链表的中间节点
876. 链表的中间结点 - 力扣(LeetCode)https://leetcode.cn/problems/middle-of-the-linked-list/
单链表我们只能朝一个方向去遍历,寻找链表的中间节点,我们固然可以先遍历一遍计算出单链表的长度len,然后len/2,计算出一半的长度,再遍历从头走len/2步从而找到中间节点。
这些写法非常的暴力简单,可是如果我们限制条件,必须只能遍历一遍找到中间节点,上面计算长度的方法就失灵了。这时候我们有新的方法:快慢指针法!!!
我们设计两个指针fast和slow,这两个指针同时从head开始走,但是速度不一样,slow指针就和平常迭代一样,一次走一步,slow = slow->next,fast一次走两步,fast = fast->next->next。,
然后我们分情况讨论,如果链表有奇数个节点,假设1->2->3->4->5,那中间节点就是3,此时slow和fast同时从1出发,fast一次走两步,当fast走了两次到达5,这最后一个节点的时候,此时slow恰走到中间节点3。即奇数个节点时fast->next == NULL的时候停止。
如果链表有偶数个节点,假设1->2->3->4->5->6,那中间节点就是3和4,题设要求我们求中间节点中偏后的那个,也就是说我们要寻求的目标是4。slow和fast同时从1出发,fast一次走两步,当fast走了三次,即走到NULL的时候(即6->NULL),slow此时也走到了中间节点4。即偶数个节点时fast == NULL的时候停止。
总结一下,slow和fast从head开始,就是一直让fast保持走两步,slow保持走一步,无论奇数/偶数个节点,当fast走到NULL / fast->next==NULL的时候停止,此时slow所处的位置就是中间节点。
struct ListNode* middleNode(struct ListNode* head){
//快慢指针,快指针2步,慢指针1步
//快指针为NULL / 快指针的next为空结束
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
4. 链表中倒数第k个节点
剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/
对于这个题我们仍然是使用快慢指针,一开始快慢指针fast和slow都在head头部,先让快指针fast先走k步,然后让快指针fast和慢指针slow在各自位置同时出发,当fast走到NULL的时候,此时slow所处的位置就是倒数第K个节点的位置。
然而这个题还是有一个坑的,首先我们自我认识,我们作为接口的设计者,是不是应该考虑到,外部用户在使用的时候,传来的参数不一定都是有效的合法的,也就是说存在这种非法情况:比如我们一个链表只有5个节点,然后传来的k是6,让我们求倒数第6个节点,比链表长度还长,倒数第6个节点并不存在,是NULL,所以我们必须要考虑这一非法情况。如果在fast走K步迭代的半路上fast提前变成NULL,就是非法情况,len<k是这样的,当然如果len == k,这是合法情况,fast会在最后一次迭代时变成NULL,所以我们选择在每次迭代之前一步进行检查即可。
struct ListNode* getKthFromEnd(struct ListNode* head, int k){
//特殊情况
if(head==NULL)
return NULL;
struct ListNode* fast = head;
struct ListNode* slow = head;
while(k--)
{
//如果k大于链表的长度,则会出现fast为空的情况
//当然如果k==len。fast也会变为空,不过此时是合法情况,所以我们要在迭代之前检查即可避免错杀这种情况
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
5. 总结
经典链表OJ题第一弹的内容结束了,还是那句话,看十遍不如码一遍,希望大家看完之后能够亲自写一遍。最后送给大家一个我认为写数据结构相关OJ所需要拥有的重要技能步骤。