前言🐱
hello,大家好啊,今天继续刷题,本次刷的是链表专题。
链表练习🐦
不带哨兵位:
一定一定要考虑清楚多种情况再写代码。
注意不能一开始就动了head,因为要返回head,如果一开始就用head来操作,那后面就找不到链表的起点了。
此题是通过返回头结点来改变实参,因此不需要传二级指针。
要删除中间某一个,需要先找到其前一个,下一次就要让cur指向val的下一个。
- 中间有几个val
- 链表为空
- 全是val
- 第一个就是val,会出现prev-> next 空指针解引用的问题,而且如果直接free掉第一个节点的,链表的头也应该变。
因此我们只需要直接干掉第一个节点,并让head指向下一个节点,然后再让cur 往后移动一位。
做题时,先处理正确情况,然后再针对特殊情况处理,有时候,某些特殊情况是能合并一起处理的,比如上面的全是val 和第一个是val的情况就可以合并处理了。空链表和普通情况也是能合并处理。
考虑到经常需要用到cur的下一个节点,因此我们最好提前保存起来。
完全照着画图的思路写的
struct ListNode *removeElements(struct ListNode *head, int val)
{
struct ListNode *prev = NULL;
struct ListNode *cur = head;
while (cur)
{
struct ListNode *next = cur->next;
if (cur->val == val)
{
if (prev == NULL)
{
free(cur);
head = next;
cur = next;
}
else
{
prev->next = next;
free(cur);
cur = NULL;
cur = next;
}
}
else
{
prev = cur;
cur = next;
}
}
return head;
}
自己一开始写的,比较挫。
struct ListNode *removeElements(struct ListNode *head, int val)
{
struct ListNode *prev = NULL;
struct ListNode *cur = head;
while (cur)
{
while (cur != NULL && cur->val != val)
{
prev = cur;
cur = cur->next;
}
if(cur == NULL)
{
break;
}
if (cur->val == val)
{
if (prev == NULL)
{
struct ListNode *next = cur->next;
head = next;
free(cur);
cur = NULL;
cur = next;
}
else
{
prev->next = cur->next;
free(cur);
cur = NULL;
cur = prev->next;
}
}
}
return head;
}
带哨兵位:
强行开辟一个哨兵位的头结点,这样prev就不会出现空指针解引用的问题了。
注意手动开辟的guardHead一定要手动释放。
struct ListNode *removeElements(struct ListNode *head, int val)
{
struct ListNode* guardHead = (struct ListNode*)malloc(sizeof(struct ListNode));
guardHead->next = head;
struct ListNode* cur = head;
struct ListNode* prev = guardHead;
while(cur)
{
struct ListNode* next = cur->next;
if(cur->val == val)
{
prev->next = next;
free(cur);
cur = NULL;
cur = next;
}
else
{
prev = cur;
cur = cur->next;
}
}
head = guardHead->next;
free(guardHead);
guardHead = NULL;
return head;
}
三指针翻转:
其实只要把箭头翻过来就行。
n1指向NULL,n2指向第一个节点,
但是这样n2就找不到下一个节点了,因此还需n3来指向n2的下一个。
n2指向n1,然后n2赋值给n1,n3赋值给n2,n3往后移动
n2指向NULL时就结束了,n1就是新链表的头,
注意n3指向NULL时可能会产生空指针解引用。
注意还要考虑极端情况,空链表或只有一个结点的情况。
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL || head->next == NULL)
return head;
struct ListNode *n1 = NULL, *n2 = head, *n3 = head->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3 != NULL)
n3 = n3->next;
}
head = n1;
return head;
}
头插法:
这里的头插不需要创建新节点。 取原链表中的节点,头插到新节点。
把cur拿下来的同时,还需保存cur的next,不然插下来之后就找不到了。
迭代记录next的位置,并让cur指向newHead。头插的节点要变成新的头。
考虑空链表和一个节点的情况,发现恰巧也能满足。
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* newHead = NULL;
struct ListNode* cur = head;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = newHead;
newHead = cur;
cur = next;
}
head = newHead;
return head;
}
递归:
在稿纸上画图分析就行,递归核心就是假设后面n-1个已经翻转了
以链表1->2->3->4->5举例
先去到最后一个节点5,然后让5指向4,4要指向空
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL || head->next == NULL)
{
return head;
}
struct ListNode* cur = head;
struct ListNode* next = cur->next;
struct ListNode* newHead = reverseList(next);
next->next = cur;
cur->next = NULL;
return newHead;
}
开数组:
观察题目节点的数据范围,额外开一个数组。存放所有的val,借助count翻转所有的val即可
特殊情况,链表为空或单个节点,也能满足
struct ListNode* reverseList(struct ListNode* head)
{
int array[5010] = {0};
struct ListNode* cur = head;
int count = 0;
int i = 0;
while(cur)
{
count++;
array[i++] = cur->val;
cur = cur->next;
}
cur = head;
count--;
while(cur)
{
cur->val = array[count--];
cur = cur->next;
}
return head;
}
两次循环:
借助count统计链表节点个数,第二次循环时只遍历n/2即可
class Solution {
public:
ListNode* middleNode(ListNode* head) {
if(head->next == NULL)
return head;
int count = 0;
ListNode* cur = head;
while(cur)
{
count++;
cur = cur->next;
}
cur = head;
count = count / 2;
while(count--)
{
cur = cur->next;
}
return cur;
}
};
数组:
对链表进行遍历,同时将遍历到的元素依次放入数组 A 中。如果我们遍历到了 N 个元素,那么链表以及数组的长度也为 N ,对应的中间节点即为 A[N/2]
class Solution {
public:
ListNode* middleNode(ListNode* head)
{
vector<ListNode*> A = {head};
while (A.back()->next != nullptr)
A.push_back(A.back()->next);
return A[A.size() / 2];
}
};
快慢指针:
如果只能遍历一遍链表呢?
利用快慢指针,快指针一次走2步,慢指针一次走一步,当fast指向NULL或者fast的下一个指向NULL 由于题目要求,偶数个返回中间的第二个节点,恰好就是slow
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode *fast = head, *slow = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
牛客
暴力循环:
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
int len = 0;
struct ListNode* cur = head;
while(cur != NULL)
{
cur = cur->next;
len++;
}
for(int i=0; i<len-k; i++)
{
head = head->next;
}
return head;
}
注意,牛客的题细节更多,需要考虑更多情况。
上面代码在牛客就跑不过。
考虑k <= 0 或者空链表的情况
考虑节点的总个数 < k
class Solution
{
public:
ListNode *FindKthToTail(ListNode *pListHead, unsigned int k)
{
if (pListHead == NULL || k <= 0)
return nullptr;
int count = 0;
ListNode *cur = pListHead;
while (cur)
{
count++;
cur = cur->next;
}
if (count < k)
return nullptr;
cur = pListHead;
count -= k;
while (count--)
{
cur = cur->next;
}
return cur;
};
};
快慢指针:
首先让快指针先行k步,然后让快慢指针每次同行一步,直到快指针指向空节点,慢指针就是倒数第K个节点。
当然 fast先走k-1步也是可以的,相应的,slow fast一起走的时候fast结束位置就要改变一下
注意考虑空链表以及k>链表长度以及k为负数的问题
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(k--)
{
if(fast == NULL)
return NULL;
fast = fast->next;
}
while(fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
牛客的OJ很恶心,很难调 1.自测常规情况,可以过 2.打印一些标记值,不显示输出标记值。比如:段错误 3.思考极端情况场景测试用例(链表为空,k大于链表长度,k为负数等等)
力扣环境很舒服,错了会报测试用例,根据测试用例去改就行
牛客体验相当糟糕,报错报的很模糊
class Solution
{
public:
ListNode *FindKthToTail(ListNode *pListHead, unsigned int k)
{
if (pListHead == nullptr || k <= 0)
return nullptr;
ListNode *slow = pListHead, *fast = pListHead;
while (k--)
{
fast = fast->next;
if(fast == nullptr)
return nullptr;
}
while (fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
尾声 🐶
🌵🌵
写文不易,如果有帮助烦请点个赞~ 👍👍👍
🌹🌹Thanks?(・ω・)ノ🌹🌹
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。 附GitHub仓库链接
|