数据结构---链表操作技巧 - 实践
一、双指针法(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)时,虚拟头结点仍能保证代码正常执行。 - 提高可读性:减少了边界条件的判断,使代码结构更清晰。
虚拟头结点是链表操作中的常用技巧,尤其在处理复杂链表问题时能显著简化代码逻辑,降低出错概率。
浙公网安备 33010602011771号