👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:Leetcode + 面试/笔试
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注
一、移除链表元素
1.1 题目描述
Leetcode链接:移除链表元素
1.2 思路一
- 创建两个值指针
prev
和cur
,一开始将prev
置为NULL
,cur
指向头结点- 接着让
prev
和cur
往后遍历,如果cur指向的结点不等于val,则让prev
指向cur
,cur
往后遍历。- 如果
cur
指向的结点等于val
,说明该结点就是要删除的结点,则先让prev
指向的结点的next
指向cur
的next
,然后释放cur
(删除节点),再让cur
指向prev->next
继续遍历。所以,prev
指针的作用就是记录要删除的结点的前一个结点,使删除结点释放后再链接。- 注意两个小细节:第一个,如样例二中,如果链表为空,直接返回
NULL
。第二个,如样例三中,如果链表内都是要删除的结点,就要单独处理(相当于头删)。(千万不能对空指针进行使用!)
【样例一动图展示】
【样例三动图展示】
1.3 思路一代码实现
struct ListNode* removeElements(struct ListNode* head, int val)
{
if(head == NULL)
return NULL;
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while (cur != NULL)
{
if (cur->val != val)
{
prev = cur;
cur = cur->next;
}
else
{
//单独处理样例三(头删)
if (prev == NULL)
{
head = cur->next;
free(cur);
cur = head;
}
else
{
prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
}
return head;
}
1.4 思路二 – 尾插
直接将不等于
val
的结点尾插到新链表,这就不用考虑样例三了。但是要注意,尾插后的tail
的next
要置为NULL
,如样例一所示,如果不置为空,最后尾插5节点时,tail
的next
还指向6这个结点,而6这个结点最后被释放了,为了防止野指针问题,最后一定要把tail的next置为空
【动图展示】
1.5 思路二代码实现
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* newnode = NULL;
struct ListNode* cur = head; //遍历原链表
struct ListNode* tail = NULL;//遍历新链表
while (cur != NULL)
{
if (cur->val != val)
{
//若一开始newnode为空,相当于一次赋值
if(newnode == NULL)
{
newnode = tail = cur;
}
else
{
//尾插
tail->next = cur;
tail = tail->next;
}
cur = cur->next;
}
else
{
//如果是要释放的结点
//则记录释放结点的下一个结点
struct ListNode* next = cur->next;
free(cur);
cur = next;
}
//防止野指针
if (tail)
{
tail->next = NULL;
}
}
return newnode;
}
二、翻转单链表
2.1 题目描述
Leetcode链接:翻转单链表
2.2 思路一 – 三指针
- 初始化三个指针
prev
、cur
、next
,分别为NULL
,指向头结点、指向cur
的next
- 接下来翻转链表,一开始让cur的next指向prev,然后再分别向后迭代三个指针
【动图展示】
由动图最后可观察到,当cur
指向NULL
时,翻转结束,并且最后只用返回prev
,即是翻转后的链表
2.3 思路一代码实现
struct ListNode* reverseList(struct ListNode* head)
{
if (head == NULL) return NULL;
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next1 = cur->next;
while (cur)
{
//翻转
cur->next = prev;
//迭代
prev = cur;
cur = next1;
//当nex1走到空指针就没必要走了
if (next1)
next1 = next1->next;
}
return prev;
}
2.4 思路二 – 头插
头插到新链表
- 初始化指针cur指向头结点,并且新建一个指针
newnode
指向NULL
- 将
cur
的next指向的next
指向newnode
结点上,在每次头插前,要记录cur
的下一个结点记为next
,并且头插后要更新newnode
,然后再让cur
指向next
- 最后继续重复以上操作,直到
cur
指向NULL
结束,返回newnode
。
【动图展示】
2.5 思路二代码实现
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head;
struct ListNode* newnode = NULL;
while (cur)
{
//每次遍历前记录cur的下一个结点
struct ListNode* next = cur->next;
//头插
cur->next = newnode;
//更新newnode
newnode = cur;
cur = next;
}
return newnode;
}
三、链表的中间结点
3.1 题目描述
Leetcode链接:链表的中间结点
3.2 思路
这题用快慢指针就非常快了,定义两个指针
fast
和slow
,同时指向头结点,接下来让fast走两步,slow走一步。说到这里,就要分两种情况讨论:
- 当结点个数为奇数时(如示例一)
由上动图观察可以得到,结点个数为奇数个时,当slow
指向中间节点,fast
的结束条件是fast->next = NULL
- 当结点个数为偶数时(如示例二)
*由上动图观察可以得到,结点个数为偶数个时,当
slow
指向中间节点,fast
的结束条件是fast = NULL
3.3 代码实现
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast,*slow;
slow = head;
fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
四、链表中倒数第k个结点
Leetcode链接:链表中倒数第k个结点
4.1 题目描述
4.2 思路
这题同样也可以用快慢指针来做
- 定义两个指针
fast
和slow
,同时指向头结点- 接着先让
fast
走k
步- 然后再让
slow
和fast
一起向后走一步- 最后
slow
指向的结点就是链表中倒数第k个结点
【动图演示】
从上动图可以发现,当fast
指向空时,slow
正好指向倒数第k
个结点
4.3 代码实现
struct ListNode* getKthFromEnd(struct ListNode* head, int k)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
//先让fast走k步
while (k --)
{
fast = fast->next;
}
//接着让fast和slow同时向后走1步
while (fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
面试官灵魂拷问:你确定你的代码没有问题?
虽然这题就这么过了,但我发现有一个小bug,当k
大于结点个数,若先让fast
走k
步,这不就越界了吗?怎么没有被检查出来?因此,严谨一点,补上一句代码
五、合并两个有序链表
Leetcode链接:合并两个有序链表
5.1 题目描述
5.2 做法
可以参考这篇博客 -->【算法基础】归并排序
代码实现
【不带哨兵位】
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if (list1 == NULL) return list2;
if (list2 == NULL) return list1;
//创建两个指针分别遍历两个链表
struct ListNode* cur1 = list1,*cur2 = list2;
struct ListNode* head = NULL,*tail = NULL;
while (cur1 && cur2)
{
if (cur1->val < cur2->val)
{
if (head == NULL)
{
head = tail = cur1;
}
else
{
tail->next = cur1;
tail = tail->next;
}
cur1 = cur1->next;
}
else
{
if (head == NULL)
{
head = tail = cur2;
}
else
{
tail->next = cur2;
tail = tail->next;
}
cur2 = cur2->next;
}
}
if (cur1)
{
tail->next = cur1;
}
if (cur2)
{
tail->next = cur2;
}
return head;
}
【带哨兵位】
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* cur1 = list1,*cur2 = list2;
//创建一个带哨兵位的头结点
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
//定义一个指针来遍历
struct ListNode* tail = guard;
tail->next = NULL;
while (cur1 && cur2)
{
if (cur1->val < cur2->val)
{
//尾插
tail->next = cur1;
tail = tail->next;
cur1 = cur1->next;
}
else
{
//尾插
tail->next = cur2;
tail = tail->next;
cur2 = cur2->next;
}
}
//如果cur1还有结点,直接尾插
if (cur1)
{
tail->next = cur1;
}
//如果cur2还有结点,直接尾插
if (cur2)
{
tail->next = cur2;
}
//最后销毁哨兵位头结点(防止内存泄漏)
struct ListNode* head = guard->next;
free(guard);
return head;
}
六、分割链表
6.1 题目描述
LeetCode链接:分割链表
6.2 思路
- 创建两个带哨兵位的结点,一个存放<x的结点(Lguard),一个存放>x的结点(Bguard),并且创建两个指针来分别遍历<x的结点(Ltail)和>x的结点(Btail),最后再创建指针cur来遍历原结点。
- 接下来,如果cur指向的结点小于x,就将这个结点尾插到Lguard,否则就尾插到Bguard。
- 下一步将两个链表链接起来
- 最后释放掉两个哨兵位头结点
【动图展示】
为什么要将Btail的next置为NULL呢?
原因是:结点5在原结点的next是指向最后一个结点2的,如果不置为NULL,此时最后的Btail的next是指向2这个结点的,然后链接起来就会形成环。
6.3 代码实现
struct ListNode* partition(struct ListNode* head, int x)
{
//创建两个带哨兵位的结点
//一个存放<x的结点(Lguard),一个存放>x的结点(Bguard)
struct ListNode* Lguard = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* Bguard = (struct ListNode*)malloc(sizeof(struct ListNode));
//再创建两个指针来分别遍历<x的结点(Ltail)和>x的结点(Btail)
struct ListNode* Ltail = Lguard,*Btail = Bguard;
//初始化
Ltail->next = NULL;
Btail->next = NULL;
//cur遍历原结点
struct ListNode* cur = head;
while (cur)
{
if (cur->val < x)
{
//尾插到Lguard
Ltail->next = cur;
Ltail = Ltail->next;
}
else
{
//尾插到BGuard
Btail->next = cur;
Btail = Btail->next;
}
cur = cur->next;
}
//链接
Ltail->next = Bguard->next;
//Bguard的尾结点要置为NULL,防止链表成环
Btail->next = NULL;
//释放哨兵位
head = Lguard->next;
free(Lguard);
free(Bguard);
return head;
}
七、链表的回文结构
7.1 题目描述
LeetCode链接:回文结构
7.2 思路
- 首先先使用快慢指针找到原链表的中间结点
- 再逆序中间结点以后的结点
- 将逆序后的结点与未被逆序的结点比较,如果相等返回
true
,不相等返回false
7.3 代码实现
//逆序函数
struct ListNode* reverseList(struct ListNode* head)
{
if (head == NULL) return NULL;
struct ListNode* prev = NULL;
struct ListNode* cur = head;
struct ListNode* next1 = cur->next;
while (cur)
{
//翻转
cur->next = prev;
//迭代
prev = cur;
cur = next1;
//当nex1走到空指针就没必要走了
if (next1)
next1 = next1->next;
}
return prev;
}
bool isPalindrome(struct ListNode* head)
{
//1。首先先使用快慢指针找到中间结点
struct ListNode* slow = head,*fast = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
//来到此处,slow就是中间结点
//2.逆序中间结点以后的结点
struct ListNode* rmid = reverseList(slow);
//3.比较
while (head && rmid)
{
if (head->val == rmid->val)
{
head = head->next;
rmid = rmid->next;
}
else
return false;
}
return true;
}
八、相交链表
LeetCode链接:相交链表
8.1 题目描述
8.2 思路
- 首先对于相交链表来说,它们最后一个结点的地址一定是相等的。所以可以先遍历一遍链表,判断它们的尾结点的地址是否相等
- 接下来为了能找到两链表相等的结点,可以先求出两个链表的大小(此步骤在第一步遍历链表可实现),让长链表先走它们的长度差步,为的就是它们能够在相对位置同时出发。
- 最后再遍历一遍链表,如果发现有两个结点地址相等,则返回对应结点
8.3 代码实现
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
//1.遍历一遍链表,判断尾结点是否相等
struct ListNode* tailA = headA,*tailB = headB;
//lengthA和lengthB初始化为1,包含了最后一个结点
int lengthA = 1,lengthB = 1;
//找尾
while (tailA->next != NULL)
{
lengthA++;
tailA = tailA->next;
}
while (tailB->next != NULL)
{
lengthB++;
tailB = tailB->next;
}
//尾结点不相等,返回NULL
if (tailA != tailB)
return NULL;
//2.先让长链表先走差距步
//计算出差距步
int gap = abs(lengthA - lengthB);
//假设一开始headA最长
struct ListNode* longest = headA;
struct ListNode* shortest = headB;
//可能存在headB比headA长的情况
if (lengthA < lengthB)
{
longest = headB;
shortest = headA;
}
//长链表先走差距步
while (gap--)
{
longest = longest->next;
}
//最后让longest和shortest一起走,
//如果发现它们的地址相等,说明这是一个相交链表
while (longest != shortest)
{
longest = longest->next;
shortest = shortest->next;
}
//当longest和shortest相等会来到此处
return longest;//返回shortest也一样
}