数据结构---链表操作技巧
一、双指针法(Two Pointers)
1. 快慢指针(Floyd判圈算法)
- 原理:用两个指针(快指针每次走2步,慢指针每次走1步),若链表有环则会相遇。
- 应用场景:
- 检测链表是否有环(相遇则有环)。
- 找环的入口节点(相遇后慢指针从起点、快指针从相遇点同速走,相遇处为入口)。
- 找链表中点(快指针到末尾时,慢指针在中点)。
- 代码示例(找中点):
ListNode* middleNode(ListNode* head) {ListNode *slow = head, *fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow; // 慢指针指向中点 }
2. 前后指针(窗口,固定间距)
- 原理:两个指针保持固定距离,用于找倒数第k个节点。
- 应用场景:删除倒数第k个节点(前指针先移动k步,再与后指针同速移动)。
- 代码示例(删除倒数第k个节点):
ListNode* removeNthFromEnd(ListNode* head, int k) {ListNode* dummy = new ListNode(0);dummy->next = head;ListNode *first = dummy, *second = dummy;// 先让first移动k+1步(包含dummy)for (int i = 1; i <= k+1; i++) {first = first->next;}// 同时移动first和second,直到first到末尾while (first) {first = first->next;second = second->next;}// second.next即为倒数第k个节点,删除它second->next = second->next->next;return dummy->next; }
二、链表翻转(Reverse Linked List)
1. 迭代法
- 原理:通过修改指针方向,逐个反转节点指向。
- 应用场景:反转链表(如求解回文链表、K个节点一组反转)。
- 代码示例:
ListNode* reverseList(ListNode* head) {ListNode* prev = nullptr;ListNode* curr = head;while (curr) {ListNode* nextTemp = curr->next; // 保存下一个节点curr->next = prev; // 反转指针prev = curr; // 前指针后移curr = nextTemp; // 当前指针后移}return prev; // 新的头节点 }
2. 递归法
- 原理:通过递归到链表尾部,再逐层反转指针。
- 代码示例:
ListNode* reverseListRecursive(ListNode* head) {// 递归终止条件:空节点或尾节点if (!head || !head->next) return head;ListNode* newHead = reverseListRecursive(head->next);head->next->next = head; // 反转当前节点与后继节点的指针head->next = nullptr; // 原头节点变为尾节点,next置空return newHead; // 新头节点为原尾节点 }
三、递归处理(Recursion)
1. 适用场景
- 结构对称的操作:如合并有序链表、判断回文链表。
- 子问题与原问题形式相同:如翻转链表、树状结构转换。
- 代码示例(合并两个有序链表):
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {// 递归终止:任一链表为空时返回另一个if (!l1) return l2;if (!l2) return l1;// 比较当前节点值,递归合并剩余部分if (l1->val < l2->val) {l1->next = mergeTwoLists(l1->next, l2);return l1;} else {l2->next = mergeTwoLists(l1, l2->next);return l2;} }
四、哈希表映射(Hash Map)
1. 应用场景:复制带随机指针的链表(Random List Node)
- 问题:链表节点包含
val
、next
和random
(指向任意节点或空),需复制整个链表。 - 解决方案:
- 用哈希表记录原节点到新节点的映射。
- 先复制
val
和next
,再通过映射处理random
指针。
- 代码示例:
Node* copyRandomList(Node* head) {if (!head) return nullptr;unordered_map<Node*, Node*> map;// 第一步:复制节点并建立映射Node* curr = head;while (curr) {map[curr] = new Node(curr->val);curr = curr->next;}// 第二步:处理next和random指针curr = head;while (curr) {map[curr]->next = map[curr->next];map[curr]->random = map[curr->random];curr = curr->next;}return map[head]; }
五、链表分割(Partition List)
1. 原理:根据值将链表分为两部分(小于x的节点在前,其余在后)。
- 应用场景:LeetCode 86. 分隔链表。
- 代码示例:
ListNode* partition(ListNode* head, int x) {ListNode* small = new ListNode(0); // 小于x的链表头ListNode* large = new ListNode(0); // 大于等于x的链表头ListNode* s = small;ListNode* l = large;// 遍历原链表,按值分类while (head) {if (head->val < x) {s->next = head;s = s->next;} else {l->next = head;l = l->next;}head = head->next;}// 合并两部分链表,尾节点置空l->next = nullptr;s->next = large->next;return small->next; }
六、快慢指针找环(Floyd判圈算法扩展)
1. 检测环并找入口
- 步骤:
- 快慢指针相遇时,慢指针走了
a
步,快指针走了2a
步,环长为b
,则相遇点距入口为a - k*b
(k为整数)。 - 让慢指针从起点、快指针从相遇点同速走,相遇处即为环入口。
- 快慢指针相遇时,慢指针走了
- 代码示例:
ListNode* detectCycle(ListNode* head) {if (!head || !head->next) return nullptr;ListNode *slow = head, *fast = head;// 第一步:找相遇点while (fast && fast->next) {slow = slow->next;fast = fast->next->next;if (slow == fast) break;}if (!fast || !fast->next) return nullptr; // 无环// 第二步:找环入口slow = head;while (slow != fast) {slow = slow->next;fast = fast->next;}return slow; }
七、哨兵节点(Sentinel Node)
1. 与虚拟头节点的区别
- 虚拟头节点:固定在链表头部,用于简化头节点操作。
- 哨兵节点:泛指用于标记边界的特殊节点(如尾哨兵
nullptr
),用于判断链表结束。 - 应用场景:遍历链表时判断是否到达末尾(
curr != nullptr
)。
八、归并排序(链表版)
1. 原理:
- 用快慢指针找中点,将链表分为两半。
- 递归排序两半链表,再合并有序链表。
- 应用场景:对链表进行排序(时间复杂度O(n log n),优于冒泡/插入排序)。
- 代码示例:
ListNode* sortList(ListNode* head) {// 终止条件:空链表或单节点链表if (!head || !head->next) return head;// 找中点ListNode *slow = head, *fast = head->next;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}// 分割链表ListNode* secondHalf = slow->next;slow->next = nullptr; // 切断第一半与第二半的连接// 递归排序两半链表ListNode* l1 = sortList(head);ListNode* l2 = sortList(secondHalf);// 合并有序链表return mergeTwoLists(l1, l2); }
技巧总结与应用场景对比
技巧 | 核心思想 | 典型场景 | 时间复杂度 |
---|---|---|---|
双指针(快慢) | 不同速度移动指针 | 找中点、检测环、找环入口 | O(n) |
链表翻转 | 反转指针方向 | 回文链表、K组反转、链表逆序 | O(n) |
递归处理 | 将问题分解为子问题 | 合并有序链表、树状转换、对称操作 | O(n) |
哈希表映射 | 记录节点映射关系 | 复制带随机指针的链表 | O(n) |
链表分割 | 按值分类构建新链表 | 分隔链表(如小于x的节点在前) | O(n) |
归并排序 | 分治思想+双指针+合并有序链表 | 链表排序 | O(n log n) |
注意事项
- 内存管理:动态创建的节点需手动释放(C++中用
delete
,Java中由GC处理)。 - 边界条件:处理空链表、单节点链表时避免指针越界。
- 空间复杂度:哈希表等技巧可能增加O(n)空间,需根据场景选择。
九、虚拟头结点(Dummy Node)
1. 原理
虚拟头结点(Dummy Node)是一个人为创建的、不存储实际数据的节点,它的next
指针指向链表的真正头结点。通过引入这个额外节点,可以统一处理链表的各种操作(尤其是涉及头结点修改的情况),避免因头结点为空或被删除而导致的边界条件判断问题。
2. 应用场景
- 链表的插入操作(特别是在头结点前插入新节点)。
- 链表的删除操作(特别是删除头结点)。
- 合并两个有序链表。
- 反转链表的部分节点等。
3. 代码示例(删除特定值的节点)
不使用虚拟头结点时,需要单独处理头结点被删除的情况:
ListNode* removeElements(ListNode* head, int val) {// 单独处理头结点需要删除的情况while (head != nullptr && head->val == val) {ListNode* temp = head;head = head->next;delete temp;}ListNode* curr = head;// 处理非头结点的删除while (curr != nullptr && curr->next != nullptr) {if (curr->next->val == val) {ListNode* temp = curr->next;curr->next = curr->next->next;delete temp;} else {curr = curr->next;}}return head;
}
使用虚拟头结点后,所有节点的删除操作可以统一处理:
ListNode* removeElements(ListNode* head, int val) {// 创建虚拟头结点,其next指向真正的头结点ListNode* dummy = new ListNode(0);dummy->next = head;ListNode* curr = dummy; // 从虚拟头结点开始遍历while (curr->next != nullptr) {if (curr->next->val == val) {ListNode* temp = curr->next;curr->next = curr->next->next;delete temp;} else {curr = curr->next;}}ListNode* result = dummy->next; // 真正的头结点可能已被修改delete dummy; // 释放虚拟头结点的内存return result;
}
4. 优势总结
- 简化逻辑:无需单独判断头结点是否需要修改或删除,所有节点的操作方式保持一致。
- 避免空指针异常:当链表为空(
head == nullptr
)时,虚拟头结点仍能保证代码正常执行。 - 提高可读性:减少了边界条件的判断,使代码结构更清晰。
虚拟头结点是链表操作中的常用技巧,尤其在处理复杂链表问题时能显著简化代码逻辑,降低出错概率。