1. 链表基础与LeetCode刷题入门
作为一名程序员,刷算法题是提升编码能力的必经之路。而链表作为数据结构中最基础也最重要的部分之一,往往是算法入门的最佳选择。我最近重新开始刷LeetCode,决定从链表相关的简单题目入手,一方面熟悉平台操作,另一方面巩固链表的基础知识。
链表是由一系列节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。与数组不同,链表在内存中不是连续存储的,这使得它在插入和删除操作上具有优势,但访问特定位置的元素则需要从头遍历,效率较低。
在开始刷题前,我们需要先掌握链表的基本操作:
- 创建和遍历链表
- 在链表头部/尾部插入节点
- 删除指定节点
- 反转链表
- 检测链表是否有环
这些基本操作是解决更复杂链表问题的基础。在LeetCode上,链表相关的题目通常会给出链表节点的定义,如下所示:
c复制struct ListNode {
int val;
struct ListNode *next;
};
理解这个结构体定义是解决链表问题的第一步。val存储节点的值,next指针指向下一个节点,最后一个节点的next为NULL。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 移除链表元素(LeetCode 203)
2.1 问题描述与初步解法
第一个题目是移除链表中所有值等于给定val的节点。我的第一版代码如下:
c复制struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode *p,*pre;
while (head != NULL && head->val == val) {
struct ListNode* temp = head;
head = head->next;
free(temp);
}
if(head == NULL){
return head;
}
pre = head;
p = head->next;
while(p != NULL){
if(val == p->val){
pre->next = p->next;
free(p);
p = pre->next;
}else{
pre = p;
p = pre->next;
}
}
return head;
}
这段代码的思路是:
- 先处理头节点等于val的情况
- 然后遍历链表,删除所有值等于val的节点
虽然这个解法能通过测试,但存在几个问题:
- 代码逻辑不够简洁
- 需要单独处理头节点的情况
- 指针操作容易出错
2.2 使用哨兵节点优化
通过学习和参考官方题解,我改进了代码,使用了哨兵节点(dummy node)技术:
c复制struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* dummyHead = malloc(sizeof(struct ListNode));
dummyHead->next = head;
struct ListNode* temp = dummyHead;
while (temp->next != NULL) {
if (temp->next->val == val) {
temp->next = temp->next->next;
} else {
temp = temp->next;
}
}
return dummyHead->next;
}
哨兵节点的优势在于:
- 统一处理头节点和其他节点的情况
- 简化了指针操作逻辑
- 减少了边界条件的判断
注意:使用哨兵节点后,记得在最后释放哨兵节点的内存,避免内存泄漏。
3. 两两交换链表中的节点(LeetCode 24)
3.1 问题分析与解法
这道题要求我们两两交换链表中的相邻节点。例如,1->2->3->4变为2->1->4->3。
我的解法同样使用了哨兵节点:
c复制struct ListNode* swapPairs(struct ListNode* head) {
struct ListNode dummyHead;
dummyHead.next = head;
struct ListNode* temp = &dummyHead;
while (temp->next != NULL && temp->next->next != NULL) {
struct ListNode* node1 = temp->next;
struct ListNode* node2 = temp->next->next;
temp->next = node2;
node1->next = node2->next;
node2->next = node1;
temp = node1;
}
return dummyHead.next;
}
关键点在于:
- 使用temp指针记录当前处理的位置
- 每次处理两个节点,调整它们的next指针
- 更新temp指针到下一对节点的前一个位置
3.2 交换操作的详细步骤
让我们详细分解交换两个节点的过程:
- 初始状态:temp -> node1 -> node2 -> ...
- temp->next = node2 (将temp指向node2)
- node1->next = node2->next (将node1指向node2后面的节点)
- node2->next = node1 (将node2指向node1)
- 更新temp = node1 (移动temp到下一对节点的前一个位置)
这样,node1和node2的位置就完成了交换,整个过程只需要修改三个指针的指向。
4. 删除链表的倒数第N个节点(LeetCode 19)
4.1 双指针法原理
这道题要求删除链表中倒数第n个节点。最直观的方法是:
- 第一次遍历计算链表长度L
- 第二次遍历到第L-n个节点并删除
但这种方法需要两次遍历。更高效的方法是使用双指针(快慢指针):
c复制struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode *thead, *p, *temp, *d;
thead = (struct ListNode*)malloc(sizeof(struct ListNode));
thead->next = head;
temp = thead;
p = thead;
d = thead->next;
int i = 0;
while (p != NULL && i < n) {
p = p->next;
i++;
}
if (p == NULL) {
free(thead);
return head;
}
while (p->next != NULL) {
p = p->next;
temp = temp->next;
d = d->next;
}
temp->next = d->next;
free(d);
struct ListNode* newHead = thead->next;
free(thead);
return newHead;
}
双指针法的核心思想:
- 快指针p先走n步
- 然后快慢指针同步前进
- 当快指针到达末尾时,慢指针正好指向倒数第n个节点的前驱
4.2 边界条件处理
在实现双指针法时,有几个边界条件需要特别注意:
- n大于链表长度时的处理
- 删除的是头节点时的处理
- 链表为空时的处理
使用哨兵节点可以简化这些边界条件的处理,这也是为什么我在代码中加入了thead节点。
5. 相交链表(LeetCode 160)
5.1 巧妙的双指针解法
这道题要求找出两个链表相交的起始节点。最直观的方法是使用哈希表存储访问过的节点,但这样需要额外的空间。
更巧妙的方法是使用双指针:
c复制struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *pA = headA;
struct ListNode *pB = headB;
while (pA != pB) {
pA = (pA == NULL) ? headB : pA->next;
pB = (pB == NULL) ? headA : pB->next;
}
return pA;
}
这个解法的精妙之处在于:
- 两个指针分别遍历两个链表
- 当一个指针到达链表末尾时,跳转到另一个链表的头部继续遍历
- 如果链表相交,两个指针最终会在交点相遇
- 如果不相交,两个指针会同时到达NULL
5.2 数学原理分析
假设链表A的非公共部分长度为a,链表B的非公共部分长度为b,公共部分长度为c。
指针pA走过的路径:a + c + b
指针pB走过的路径:b + c + a
可以看到,两个指针走过的总长度相同,因此如果有交点,必定会在交点相遇;如果没有交点,则会同时到达NULL。
6. 链表刷题经验总结
通过这五道链表题目的练习,我总结了以下几点经验:
-
哨兵节点:在处理链表问题时,使用哨兵节点可以大大简化边界条件的处理,特别是涉及头节点操作的情况。
-
双指针技巧:这是解决链表问题的强大工具,可以用于检测环、找中点、找倒数第n个节点等问题。
-
指针操作顺序:在修改指针指向时,要注意操作的顺序,避免丢失对节点的引用。
-
内存管理:在C语言中,删除节点后要及时释放内存,避免内存泄漏。
-
画图辅助:对于复杂的指针操作,先在纸上画出链表结构,标出每个步骤的指针变化,有助于理清思路。
提示:在面试中,解决链表问题时,建议先和面试官确认是否可以修改原链表,以及是否需要保持原链表的结构。
链表作为基础数据结构,掌握它的各种操作和常见问题的解法,不仅有助于通过技术面试,更能提升对指针和内存管理的理解。建议初学者从简单题目开始,逐步挑战更复杂的问题,同时注意总结每种解法的适用场景和优缺点。
