引言:为什么进行链表的学习?
- 考察能力独特:链表能很好地考察应聘者对指针操作、内存管理的理解和运用能力,还能检验代码的鲁棒性,比如处理链表的插入、删除操作时对边界条件的处理。
- 数据结构基础:链表是很多复杂数据结构和算法的基础,如图算法中的邻接表存储结构就用到了链表,在操作系统的内存管理中也常利用链表来管理空闲内存块等。
1 方法论
核心技巧 适用场景 时间复杂度优化 经典例题 ───────────────────────────────────────────────────────────
快慢指针 链表环检测、找中间节点等 O(n) 复杂度下高效处理 环形链表 Ⅱ (142) 虚拟头节点 头部插入/删除等边界简化操作 不改变复杂度但简化逻辑 删除链表的倒数第 N 个节点 递归 链表反转、合并有序链表等 利用递归特性简化操作逻辑 合并两个有序链表 (21)
*设计链表
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:
val
和next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果是双向链表,则还需要属性
prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。实现
MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
#define MAX(a, b) ((a) > (b) ? (a) : (b))typedef struct {struct ListNode *head;int size;
} MyLinkedList;struct ListNode *ListNodeCreat(int val) {struct ListNode * node = (struct ListNode *)malloc(sizeof(struct ListNode));node->val = val;node->next = NULL;return node;
}MyLinkedList* myLinkedListCreate() {MyLinkedList * obj = (MyLinkedList *)malloc(sizeof(MyLinkedList));obj->head = ListNodeCreat(0);obj->size = 0;return obj;
}int myLinkedListGet(MyLinkedList* obj, int index) {if (index < 0 || index >= obj->size) {return -1;}struct ListNode *cur = obj->head;for (int i = 0; i <= index; i++) {cur = cur->next;}return cur->val;
}void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {if (index > obj->size) {return;}index = MAX(0, index);obj->size++;struct ListNode *pred = obj->head;for (int i = 0; i < index; i++) {pred = pred->next;}struct ListNode *toAdd = ListNodeCreat(val);toAdd->next = pred->next;pred->next = toAdd;
}void myLinkedListAddAtHead(MyLinkedList* obj, int val) {myLinkedListAddAtIndex(obj, 0, val);
}void myLinkedListAddAtTail(MyLinkedList* obj, int val) {myLinkedListAddAtIndex(obj, obj->size, val);
}void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {if (index < 0 || index >= obj->size) {return;}obj->size--;struct ListNode *pred = obj->head;for (int i = 0; i < index; i++) {pred = pred->next;}struct ListNode *p = pred->next;pred->next = pred->next->next;free(p);
}void myLinkedListFree(MyLinkedList* obj) {struct ListNode *cur = NULL, *tmp = NULL;for (cur = obj->head; cur;) {tmp = cur;cur = cur->next;free(tmp);}free(obj);
}
2 快慢指针
2.1 介绍
溯找前驱;链表超长且环大时,快指针环内多圈追赶,有性能损耗。快慢指针是链表操作的高效技巧,通过设置快、慢两个指针,快指针每次移动两步,慢指针每次移动一步,利用二者速度差达成特定目标。
- 适用场景:
- 链表环检测:精准判断链表有无环,有环时能定位环入口,如在复杂数据结构中排查循环引用隐患。
- 找中间节点:像归并排序前期需快速平分链表,快慢指针遍历一次即可定位中点,为后续有序处理奠基。
- 判断奇偶节点:依快慢指针最终位置,轻松判别节点奇偶性,辅助特殊逻辑,如奇偶位交替变换。
- 时间复杂度优化原理:利用速度差,无环时快指针率先触尾结束遍历;有环时快指针必在环内 “追上” 慢指针,单次 O (n) 遍历就解决问题,找中点同理,快到尾时慢恰在中点。
- 选择策略:但凡涉及链表节点位置判断,像找特定节点、查环,尤其单次遍历需多元位置信息,优先启用。
- 局限性:单向链表中,快慢指针难以直接回
2.2 练习
(1)环形链表Ⅱ
环形链表 IIhttps://leetcode.cn/problems/c32eOV/
给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着
next
指针进入环的第一个节点为环的入口节点。如果链表无环,则返回null
。为了表示给定链表中的环,我们使用整数
pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果pos
是-1
,则在该链表中没有环。注意,pos
仅仅是用于标识环的情况,并不会作为参数传递到函数中。说明:不允许修改给定的链表。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
struct ListNode *detectCycle(struct ListNode *head) {struct ListNode* fast = head;struct ListNode* slow = head;while(fast != NULL && fast->next != NULL) {slow = slow->next;fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直⾄相遇
if (slow == fast) {struct ListNode* index1 = fast;struct ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;}
return index2; // 返回环的⼊⼝}}
return NULL;
}
(2)相交链表
相交链表https://leetcode.cn/problems/intersection-of-two-linked-lists/
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。图示两个链表在节点
c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {struct ListNode * pA = headA;struct ListNode * pB = headB;int lengthA = 0;int lengthB = 0;while(headA != NULL){lengthA ++;headA = headA->next;}while(headB != NULL){lengthB ++;headB = headB->next;}if(lengthA < lengthB){for(int i = 0;i < abs(lengthA - lengthB);i++){pB = pB->next;}}else{for(int i = 0;i < abs(lengthA - lengthB);i++){pA = pA->next;}}while(pA != NULL && pB != NULL){if(pA == pB){return pA;}pA = pA->next;pB = pB->next;}return NULL;}
(3)删除链表的倒数第N个结点
删除链表的倒数第 N 个结点https://leetcode.cn/problems/SLwz0R/
给定一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/struct ListNode* removeNthFromEnd(struct ListNode* head, int n){// 创建虚拟头节点,方便处理删除头节点的情况struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));dummy->val = 0;dummy->next = head;struct ListNode* first = dummy;struct ListNode* second = dummy;// 让 first 指针先移动 n + 1 步for (int i = 0; i <= n; i++) {first = first->next;}// 同时移动 first 和 second 指针,直到 first 指针到达链表末尾while (first != NULL) {first = first->next;second = second->next;}// 此时 second 指针指向要删除节点的前一个节点struct ListNode* temp = second->next;second->next = second->next->next;free(temp);// 获取新的头节点struct ListNode* newHead = dummy->next;free(dummy);return newHead;
}
3 虚拟头节点
3.1 介绍
虚拟头节点是链表操作的辅助 “锚点”,不存实质数据,占位简化逻辑。
- 适用场景:
- 头部插入删除:常规操作头节点需额外边界判断,虚拟头使插入、删除逻辑统一,降低出错概率。
- 链表初始化:为空链表开篇或构建新结构 “打头阵”,后续添加节点顺理成章。
- 多链表操作:合并多链表时,提供统一起始,梳理合并流程,代码结构更清晰。
- 时间复杂度优化原理:统一头部操作,规避重复判断,多次操作下稳定耗时,提升整体效率。
- 选择策略:频繁头部操作或统一管理多链表,又或需明确起始且简化头节点处理时,它是首选。
- 局限性:占用额外空间存储虚拟节点;少量头部操作时,引入虚拟头会使代码繁杂,得不偿失。
3.2 练习
(1)移除链表元素
移除链表元素https://leetcode.cn/problems/remove-linked-list-elements/
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
struct ListNode* removeElements(struct ListNode* head, int val) {struct ListNode dummy;dummy.next = head;struct ListNode* current = &dummy;while (current->next) {if (current->next->val == val) {struct ListNode* temp = current->next;current->next = current->next->next;free(temp);} else {current = current->next;}}return dummy.next;
}
struct ListNode* removeElements(struct ListNode* head, int val) {struct ListNode* phead = NULL;struct ListNode* ptail = NULL;struct ListNode* pcur = head;while(pcur){if(pcur->val != val){if(phead == NULL){phead = ptail = pcur;}else{ptail->next = pcur;ptail = pcur;}}pcur = pcur->next;}if(ptail)ptail->next = NULL;return phead;
}
4 递归
4.1 介绍
递归是基于自身定义问题求解的策略,将链表大问题拆解为同构子问题处理。
- 适用场景:
- 链表反转:从尾到头逐节点反转,递归贴合天然顺序,代码简洁直观。
- 合并有序链表:对比、拼接子链表,递归处理层次分明,轻松融合多链表。
- 链表遍历操作:深度优先遍历修改节点值等,递归按链表结构递进,无需复杂循环。
- 时间复杂度优化原理:分解难题,各子问题独立递归求解,省却多层嵌套循环,顺链表结构操作,削减冗余步骤。
- 选择策略:操作有递归特性,子问题相似,如反转、合并场景;追求代码精简、逻辑通透且时间要求适度时优先考虑。
- 局限性:长链表易引发栈溢出,因递归需栈存调用状态;空间占用多,函数调用开销也会拖慢性能。
4.2 练习
(1)反转链表
反转链表https://leetcode.cn/problems/UHnkqh/
给定单链表的头节点
head
,请反转链表,并返回反转后的链表的头节点。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/struct ListNode* reverseList(struct ListNode* head){struct ListNode *prev = NULL;struct ListNode *current = head;struct ListNode *nextNode;while (current != NULL) {// 保存当前节点的下一个节点nextNode = current->next;// 将当前节点的 next 指针指向前一个节点current->next = prev;// 更新前一个节点为当前节点prev = current;// 更新当前节点为之前保存的下一个节点current = nextNode;}// 最后 prev 指向反转后链表的头节点return prev;
}
(2)合并两个有序链表
合并两个有序链表https://leetcode.cn/problems/merge-two-sorted-lists/
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {// 创建虚拟头节点struct ListNode dummy = {0, NULL};struct ListNode* tail = &dummy;// 遍历两个链表,比较节点值并合并while (list1 && list2) {if (list1->val < list2->val) {tail->next = list1;list1 = list1->next;} else {tail->next = list2;list2 = list2->next;}tail = tail->next;}// 将剩余的节点连接到新链表尾部if (list1) {tail->next = list1;}if (list2) {tail->next = list2;}return dummy.next;
}
学习时间 2025.02.11