1.移除链表元素
https://leetcode.cn/problems/remove-linked-list-elements/description/

c
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode* prev = dummy;
struct ListNode* curr = head;
while (curr != NULL) {
if (curr->val == val) {
prev->next = curr->next;
free(curr);
curr = prev->next;
} else {
prev = curr;
curr = curr->next;
}
}
struct ListNode* newHead = dummy->next;
free(dummy);
return newHead;
}
我们来理清一下思路以及画图,数据结构做题不画图玩不了一点(嘻嘻)。
解题思路:
因为头节点也可能被删除,所以我们需要一个虚拟头节点(dummy node)来统一操作。设置一个虚拟头节点 dummy,其 next 指向原链表头。然后使用两个指针 prev 和 curr 遍历链表,prev 始终指向当前节点的前一个节点,curr 指向当前节点。
-
如果 curr.val == val,则删除该节点:prev.next = curr.next,然后 curr 后移。
-
否则,prev 和 curr 都后移,最后返回 dummy.next 作为新的头节点。
其实思路还是很好懂的,如果不懂的话我们就来画图看一下:

这是我们的初始情况对吧,val初始值为6不为3,所以我们的prev和curr都往后移,也就是:
c
prev = curr;
curr = curr->next;
对吧,直到,prev指向2,curr指向6,如下所示:

此时,我们需要让prev跳过6,也就是prev->next = curr->next;,再接着移动curr到下一个位置即可。
以此往复1,最后返回dummy->next即可。
2.反转链表
2.1迭代法
https://leetcode.cn/problems/reverse-linked-list/description/

c
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* prev = NULL;
ListNode* curr = head;
while(curr)
{
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
反转思路:
我们需要改变每个节点的 next 指针,让它指向前一个节点。
为了做到这一点,需要三个指针:
-
prev:指向当前节点的前一个节点(初始为 NULL)
-
curr:指向当前正在处理的节点(初始为 head)
-
next:临时保存当前节点的下一个节点(防止链表断开后丢失)
核心步骤(循环直到 curr 为 NULL):
-
保存 curr->next 到 next(因为马上要修改 curr->next)。
-
将 curr->next 指向 prev(反转指针)。
-
移动 prev 到 curr(准备处理下一个节点)。
-
移动 curr 到 next(继续遍历)。
循环结束后,prev 指向原链表的最后一个节点,也就是新链表的头节点。
初始条件:

接下来先保存next,然后反转指针,接下来移动指针:

接下来依次类推:

直至最后完成反转即可。
边界情况
-
空链表:head == NULL,循环不执行,直接返回 prev(也是 NULL),正确。
-
只有一个节点:循环一次,curr->next 指向 NULL,返回该节点本身。
2.2头插法
头插法是一种构建链表的方法:每次新节点都插入到链表的最前面,成为新的头节点。
例如:依次插入1、2、3,头插的结果是 3→2→1(因为1插入后链表为[1],再插入2成为[2→1],再插入3成为[3→2→1])。
而本题正是利用这个特性:把原链表的节点按原顺序依次头插到新链表,得到的就是反转后的链表。
c
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* p = head; // p 用来遍历原链表,初始指向原头
head = NULL; // 将 head 重新用作新链表的头,初始为空链表
while (p) { // 当 p 不为空,即还有节点要处理
struct ListNode* s = p; // s 保存当前要拆下的节点(即 p 指向的节点)
p = p->next; // p 先移动到下一个节点(保存下一个位置,防止丢失)
s->next = head; // 将 s 的 next 指向当前新链表的头(即头插)
head = s; // 更新新链表的头为 s
}
return head; // 返回新链表的头
}
初始:

第一次循环(处理节点1)
-
s = p:s 指向节点1。
-
p = p->next:p 移动到节点2(此时 p 指向节点2,原链表剩余部分由 p 掌握)。
-
s->next = head:此时 head 为 NULL,所以 s->next 指向 NULL,即节点1变成新链表的最后一个节点(因为它的 next 是 NULL)。
-
head = s:新链表的头更新为节点1。
如下图所示:

下一次循环时,s又指向节点2,p指向节点3,s继续头插,节点2成为新的头节点,依次类推,在这里就不再画图了
3.链表的中间节点
https://leetcode.cn/problems/middle-of-the-linked-list/description/

3.1常规易懂法
c
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
ListNode* pcur = head;
int n = 0;
while(pcur)
{
pcur=pcur->next;
n++;
}
int steps = n/2;
pcur = head;
while(steps--)
{
pcur=pcur->next;
}
return pcur;
}
解题思路:
首先,题目要求返回我们的中间节点对吧,那么我们可不可以去定义个指针去遍历链表,找出总数,接下来我们不是要返回中间节点吗,找到它就好了,所以我们需要再一次去遍历链表,
这里为什么是 int steps = n/2;呢?因为你会发现,无论是当n为奇数时,假设n等于5,那么steps等于2,我们从节点1开始,走两次不就是节点3嘛,当n为偶数时,假设为6,steps等于3,不就是从1走到4嘛,正好符合我们题意,所以接下来找到中间节点,直接返回就可以了。
3.2快慢指针
c
**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
这是个什么意思呢?
想象两个人在一条直线上跑步:
-
一个人跑得快,每次跑两步
-
一个人跑得慢,每次跑一步
-
当快的人跑到终点时,慢的人刚好在中间位置。
在链表中,我们定义两个指针:
-
slow:每次走一步
-
fast:每次走两步
初始时,两个指针都指向头节点。然后同时移动:
-
slow = slow->next
-
fast = fast->next->next
-
当fast到达末尾(即fast为NULL或者fast->next为NULL)时,slow指向的就是中间节点。
注意:因为fast一次走两步,所以要确保每次移动前检查fast和fast->next不为空。
我们来画图举个例子看一下:
初始情况:当为奇数时

第一次:

第二次:

我们会发现确实如我们所说那样,偶数情况我们不再演示,原理相同。
4.链表分割
https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70

cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
// 创建两个哑节点(使用 new 分配)
ListNode* smallHead = new ListNode(0);
ListNode* largeHead = new ListNode(0);
ListNode* smallTail = smallHead;
ListNode* largeTail = largeHead;
ListNode* curr = pHead;
while(curr)
{
ListNode* next = curr->next;
if(curr->val<x)
{
smallTail->next=curr;
smallTail = curr;
}
else
{
largeTail->next=curr;
largeTail = curr;
}
curr->next = NULL;
curr = next;
}
smallTail->next=largeHead->next;
ListNode* newhead = smallHead->next;
delete smallHead;
delete largeHead;
return newhead;
}
};
解题思路:
我们使用两个辅助链表(通过哑节点实现)来分别收集小于 x 和大于等于 x 的节点,最后将它们连接起来。具体步骤:
创建两个哑节点(dummy head):
-
smallHead:用于存放小于 x 的链表头(它的 next 指向第一个小于 x 的节点)
-
largeHead:用于存放大于等于 x 的链表头
同时创建两个尾指针 smallTail 和 largeTail,初始指向各自的哑节点。
遍历原链表:
-
用 cur 指向当前节点,先保存 cur->next 到 next,防止断开后丢失后续节点。
-
如果 cur->val < x,则将其接到 smallTail 后面,然后移动 smallTail 到该节点。
-
否则接到 largeTail 后面,移动 largeTail。
-
注意:将当前节点接入新链表后,要将其 next 置为 NULL,确保它不再指向原链表的下一个节点(因为原顺序已不再需要)。
-
然后 cur = next 继续遍历。
连接两个链表:
-
将 smallTail->next 指向 largeHead->next(即大于等于链表的第一个实际节点)。
-
此时 largeTail 的 next 已经是 NULL(因为我们在接入每个节点时都置了 NULL),所以无需再处理。
返回新链表头:
-
新链表的头是 smallHead->next。
-
最后记得释放两个哑节点(避免内存泄漏)。
初始状态:
接下来:

往复循环即可,以此类推,最后连接链表的时候需要注意:
c
将 smallTail->next 指向 largeHead->next
循环完是这样的:
c
small: s -> 1 -> 2 -> 2 -> NULL
large: l -> 4 -> 3 -> 5 -> NULL
最后连接即可。
5.链表的回文结构
https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa

cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
// 反转链表函数(返回新头)
ListNode* reverseList(ListNode* head)
{
ListNode* prev = NULL;
ListNode* cur = head;
while (cur)
{
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
bool chkPalindrome(ListNode* A)
{
if (A == NULL || A->next == NULL)
return true; // 空或只有一个节点是回文
// 1. 找到中间节点(快慢指针)
ListNode* slow = A;
ListNode* fast = A;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
// 此时 slow 指向中间节点(如果偶数,指向第二个中间节点)
// 2. 反转后半部分(从 slow 开始)
ListNode* right = reverseList(slow); // 反转后的头
ListNode* left = A;
// 3. 比较
bool isPalindrome = true;//假设
while (right)
{
if (left->val != right->val)
{
isPalindrome = false;
break;
}
left = left->next;
right = right->next;
}
return isPalindrome;
}
};
这道题的代码看似很长,但其实逻辑还是比较简单的,就是把我们的前几道题给综合了那么一小下
解题思路:(以链表 1 -> 2 -> 2 -> 1 为例)
核心思想:找到中点,反转后半部分,然后比较
-
找到链表的中间节点(使用快慢指针)。
-
将后半部分链表反转。
-
比较前半部分和后半部分是否相等。
由于之前说过查找中间节点和反转链表的相关逻辑与题目,在此来说一下关于比较的逻辑:
比较前半部分和后半部分:
现在有两个链表头:
-
left = head(指向第一个节点1)
-
right = 反转后的头(指向最后一个节点1)
依次比较 left->val 和 right->val,然后同时后移,直到 right 为 NULL(因为后半部分可能比前半部分短一个,如果奇数个节点,前半部分多一个中间节点,但中间节点不用比较,因为回文时中间节点任意)。
对于偶数个节点,比较过程:
-
left=1, right=1,相等,都后移:left=2, right=2
-
left=2, right=2,相等,right 后移为 NULL,结束。
-
全部相等,返回 true。
6.相交链表
https://leetcode.cn/problems/intersection-of-two-linked-lists/description/

c
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
ListNode* currA = headA;
ListNode* currB = headB;
int lenA = 0;
int lenB = 0;
while(currA)
{
currA=currA->next;
lenA++;
}
while(currB)
{
currB=currB->next;
lenB++;
}
int gap = abs(lenA-lenB);
ListNode* longlist = headA;
ListNode* shortlist = headB;
if(lenA<lenB)
{
longlist = headB;
shortlist = headA;
}
while(gap--)
{
longlist=longlist->next;
}
while(shortlist!=longlist)
{
longlist=longlist->next;
shortlist=shortlist->next;
}
return longlist;
}
代码逻辑思路:
计算两个链表的长度:
-
用 currA 遍历链表 headA,统计节点个数 lenA。
-
用 currB 遍历链表 headB,统计节点个数 lenB。
-
遍历结束后,currA 和 currB 都指向 NULL,但长度值已记录。
确定长链表和短链表:
-
计算长度差 gap = abs(lenA - lenB)。
-
假设 longlist 指向较长的链表头,shortlist 指向较短的链表头。
-
通过比较 lenA 和 lenB 来正确赋值。
让长链表先走 gap 步(差距步):
-
这样长链表和短链表剩余的长度相同。
-
如果链表原本相交,此时两个指针距离相交点的步数相同。
同时移动两个指针,直到相遇:
-
在 while (shortlist != longlist) 循环中,每次两个指针各走一步。
-
如果链表相交,它们会在相交点相遇,此时返回该节点。
-
如果链表不相交,它们最终会同时到达 NULL,循环条件不成立,返回 NULL
今天就先到这里,下次我们继续来复盘更多单链表经典题目~