目录
🚀 前言:链表——程序员的"花式跳绳"挑战
当你第一次学习链表时,是不是也经历过这样的心路历程?
- 初见链表:"这不就是一堆节点串在一起吗?比数组简单多了!" 😃
- 尝试写插入:"等等,我的节点怎么丢了?指针指向哪了?" 😅
- 面对环形链表:"这题...是在玩贪吃蛇吗?" 🐍
- 遇到困难题:"为什么翻转链表能衍生出这么多变种?!" 🤯
别担心!链表问题正是算法世界里的"花式跳绳"——看似简单的基础动作(增删改查),却能组合出各种让人眼花缭乱的难题。但一旦掌握核心技巧,你就能像算法高手一样,轻松玩转这些"数据绳索"!
准备好了吗?让我们拨开指针的迷雾,开始这场链表的奇妙冒险吧! 🎢
(小贴士:读到这里不妨试试——不借助额外空间,能立刻说清怎么判断链表是否有环吗?如果犹豫了,这篇博客正是为你准备的!😉)
链表基本操作
🌟 虚拟头结点:链表的「万能钥匙」
虚拟头结点(Dummy Node)是解决链表边界问题的神器,通过在真实头结点前添加一个不存储实际数据的哨兵节点
🔍 核心作用
- 统一处理逻辑:避免对头结点的特殊判断
- 防止空指针异常:保证链表永不为空
- 简化删除操作:轻松处理需要删除头结点的情况
注意:不要吝啬空间,放心大胆去定义
🌟 快慢指针:链表的「龟兔赛跑」算法
🔍 核心思想
让两个指针以不同速度遍历链表:
- 快指针每次走 2步(
fast = fast.next.next
) - 慢指针每次走 1步(
slow.next
)
注意:这里的快指针走2步,慢指针走一步并不是固定的,具体场景具体分析,可以改变每次的步数,但始终保证,快指针比慢指针快!
🌟 头插法:链表的「倒序构建」技巧
🔍 核心思想
头插法是一种逆序构建链表的高效方法,通过始终在链表头部插入新节点,可以实现:
- O(1)时间复杂度的插入操作
- 自然形成的链表结构
- 无需遍历尾部的快速构建
面对一些反转链表的题目,使用头插,轻松简单快速!
🚀 操作流程演示
原链表:1 -> 2 -> 3 -> None
步骤1:插入1
new_head: 1 -> None
步骤2:插入2
new_head: 2 -> 1 -> None
步骤3:插入3
new_head: 3 -> 2 -> 1 -> None
🌟 尾插法:链表的「顺序构建」技巧
🔍 核心思想
尾插法是一种顺序构建链表的标准方法,通过始终在链表尾部追加新节点,可以实现:
- 保持原始顺序的自然构建
- O(1)尾部插入(配合尾指针时)
- 广泛用于队列等需要保持顺序的场景
🚀 操作流程演示
输入:[1, 2, 3]
步骤1:插入1
dummy -> 1
tail指向1
步骤2:插入2
dummy -> 1 -> 2
tail指向2
步骤3:插入3
dummy -> 1 -> 2 -> 3
tail指向3
🚀 链表操作实战:从「青铜」到「王者」的逆袭之路
嘿,链表菜鸟(或者假装不是菜鸟的你)!是不是觉得链表操作就像在玩「贪吃蛇」——稍不留神,指针就咬到自己尾巴了?😅
别慌!今天我们就用头插法、尾插法、快慢指针这些骚操作,去暴打几道LeetCode经典题!
双指针例题:判断是否有环
思路:
定义两个指针,一个快指针,一个慢指针
- 快指针每次走两步
- 慢指针每次走一步
如果有环
- 快慢指针终究会在某一时刻,同时指向一个位置
- 如果没有环,快指针和慢指针最终均会走向结尾
💻 C++代码实现:
bool hasCycle(struct ListNode *head)
{
if(head==NULL)
{
return false;
}
struct ListNode*fast=head;
struct ListNode*slow=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
return true;
}
return false;
}
注意:防止使用空指针,一定要严格判断是否为空!
双指针例题:环形链表 II
思路:
依旧定义快慢指针
- 一个快指针,每次走2步
- 一个慢指针,每次走1步
当有环,两个人相遇的时候,慢指针走了r步,那么快指针走了2r步
当相遇在同一个位置,那么快指针一定比慢指针多走了n个环,令环长为c
则有2r-r = nc -> nc = r
此时,让原来慢指针从相遇的位置开始走r步,重新定义一个慢指针从头开始走
我们将原来的慢指针记作慢指针a,将新的慢指针记作慢指针b
- 慢指针a最终会回到最初和快指针相遇的位置,此时走了r步
- 而慢指针b也走了r步,也会到这个位置,与慢指针a相遇
他们两个都是每次走一步的,能走到同一个位置,说明在之前就已经发生了重合,相遇了
而他们第一次相遇的地方,就是环的入口!
💻 C++代码实现:
class Solution {
public:
ListNode *detectCycle(ListNode *head)
{
ListNode*Fast=head;
ListNode*Slow=head;
ListNode*meet=NULL;
ListNode*start=head;
while(Fast && Fast->next)
{
Slow=Slow->next;
Fast=Fast->next->next;
if(Fast==Slow)
{
meet=Slow;
while(1)
{
if(start==meet)
{
return meet;
}
meet=meet->next;
start=start->next;
}
}
}
return NULL;
}
};
头插例题:翻转链表
思路:
看碟下菜,这是一道简单题,我们直接头插即可
不过,在头插之前,需要先将第一个节点的next置为nullptr,因为它是充当翻转后链表的最后一个节点,它的next需要为空
💻 C++代码实现:
lass Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr) return nullptr;
ListNode* vir = new ListNode(0);
vir->next = head;
ListNode* cur = head;
ListNode* next = cur->next;
cur->next = nullptr;
cur = next;
while(cur){
next = cur->next;
cur->next = vir->next;
vir->next = cur;
cur = next;
}
return vir->next;
}
};
尾插法:合并2个升序链表
思路:
先创建一个虚拟头结点,来进行后续的尾插
遍历两个链表,哪个链表的当前节点小,就先将将哪个链表的节点插入
当一个链表为空的时候,结束遍历,直接将不为空的那个链表连接到之前合并的链表尾部
💻 C++代码实现:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* vir = new ListNode(0);
ListNode* cur1 = list1;
ListNode* cur2 = list2;
ListNode* cur = vir;
while(cur1 && cur2){
if(cur1->val > cur2->val) {
cur->next = cur2;
cur2 = cur2->next;
cur = cur->next;
}else{
cur->next = cur1;
cur1 = cur1->next;
cur = cur->next;
}
}
if(cur1) cur->next = cur1;
if(cur2) cur->next = cur2;
return vir->next;
}
};
尾插例题:合并k个升序链表
思路:
虽然是一道困难题,但思路其实很清晰
我们仍然是尾插,但每次尾插从两个链表当前节点的最小节点,变成k个链表当前节点的最小节点
简单,直接遍历所有链表的当前节点,将最小的插入即可
但遍历所有链表并选出最小节点有点麻烦,因此,我们可以使用堆来帮我们筛选出最小节点
先将所有链表的头结点放入小根堆中,堆顶元素就是最小的头结点
从小根堆中取最小节点进行尾插,取后就要删
尾插后,重新将最小节点的下一个节点放入小根堆中,小跟堆重新排列
如果最小节点的下一个节点为空,则不放入小跟堆中
当小根堆中没有节点,即代表合并完毕
💻 C++代码实现:
class Solution {
public:
struct cmp{
bool operator()(ListNode* x,ListNode* y){
return x->val > y->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
//创建一个小根堆
priority_queue<ListNode*,vector<ListNode*>,cmp> heap;
//填充小根堆
for(auto x : lists) if(x) heap.push(x);
//合并K个链表
ListNode* ret = new ListNode(0);
ListNode* cur = ret;
while(!heap.empty()){
ListNode* node = heap.top();
heap.pop();
cur->next = node;
cur = cur->next;
node = node ->next;
if(node) heap.push(node);
}
ListNode* value = ret->next;
delete ret;
return value;
}
};
🌟 结语:链表算法——从「指针懵逼」到「信手拈链」
恭喜你!读完这篇博客,你已经成功解锁了「链表生存指南」!🎉
曾经,你是不是也这样?
- 看到
head->next->next
就头皮发麻,感觉像在拆炸弹💣- 写个反转链表,结果把链表拧成了中国结🧶
- 面试官问「怎么判断环」,你差点说出「用眼睛看」👀
但现在,你已经掌握了:
🔹 头插法——让链表倒着长,比倒背圆周率还简单
🔹 尾插法——像排队买奶茶一样自然构建链表
🔹 快慢指针——链表界的「龟兔赛跑」,环不环的,跑两步就知道
🔹 虚拟头结点——妈妈再也不用担心我删错头节点了下次面试官再出链表题,请优雅地甩出代码,然后淡淡地说:
「这道题啊,我用三种解法,您想听哪种?」 😎