【算法】——链表问题来多少,我就解决多少!

目录

​编辑

🚀 ​​前言:链表——程序员的"花式跳绳"挑战​​

链表基本操作

🌟 ​​虚拟头结点:链表的「万能钥匙」​​

🌟 ​​快慢指针:链表的「龟兔赛跑」算法​​

🌟 ​​头插法:链表的「倒序构建」技巧​​

🌟 ​​尾插法:链表的「顺序构建」技巧​

🚀 ​​链表操作实战:从「青铜」到「王者」的逆袭之路​​

双指针例题:判断是否有环

双指针例题:环形链表 II

头插例题:翻转链表

 尾插法:合并2个升序链表

尾插例题:合并k个升序链表

🌟 ​​结语:链表算法——从「指针懵逼」到「信手拈链」​​


🚀 ​​前言:链表——程序员的"花式跳绳"挑战​

当你第一次学习链表时,是不是也经历过这样的心路历程?

  • ​初见链表​​:"这不就是一堆节点串在一起吗?比数组简单多了!" 😃
  • ​尝试写插入​​:"等等,我的节点怎么丢了?指针指向哪了?" 😅
  • ​面对环形链表​​:"这题...是在玩贪吃蛇吗?" 🐍
  • ​遇到困难题​​:"为什么翻转链表能衍生出这么多变种?!" 🤯

别担心!链表问题正是算法世界里的​​"花式跳绳"​​——看似简单的基础动作(增删改查),却能组合出各种让人眼花缭乱的难题。但一旦掌握核心技巧,你就能像算法高手一样,轻松玩转这些"数据绳索"!

准备好了吗?让我们拨开指针的迷雾,开始这场链表的奇妙冒险吧!​​ 🎢

(小贴士:读到这里不妨试试——不借助额外空间,能立刻说清怎么判断链表是否有环吗?如果犹豫了,这篇博客正是为你准备的!😉)

链表基本操作

🌟 ​​虚拟头结点:链表的「万能钥匙」​

​虚拟头结点(Dummy Node)是解决链表边界问题的神器,通过在真实头结点前添加一个​​不存储实际数据的哨兵节点​

​🔍 核心作用​

  1. ​统一处理逻辑​​:避免对头结点的特殊判断
  2. ​防止空指针异常​​:保证链表永不为空
  3. ​简化删除操作​​:轻松处理需要删除头结点的情况

注意:不要吝啬空间,放心大胆去定义

🌟 ​​快慢指针:链表的「龟兔赛跑」算法​

​🔍 核心思想​

让两个指针以​​不同速度​​遍历链表:

  • ​快指针​​每次走 ​​2步​​(fast = fast.next.next
  • ​慢指针​​每次走 ​​1步​​( slow.next

注意:这里的快指针走2步,慢指针走一步并不是固定的,具体场景具体分析,可以改变每次的步数,但始终保证,快指针比慢指针快!

🌟 ​​头插法:链表的「倒序构建」技巧​

​🔍 核心思想​

头插法是一种​​逆序构建链表​​的高效方法,通过始终在链表头部插入新节点,可以实现:

  1. ​O(1)时间复杂度​​的插入操作
  2. ​自然形成​​的链表结构
  3. ​无需遍历尾部​​的快速构建

面对一些反转链表的题目,使用头插,轻松简单快速! 

​​​🚀 操作流程演示​

原链表: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

🌟 ​​尾插法:链表的「顺序构建」技巧

​🔍 核心思想​

尾插法是一种​​顺序构建链表​​的标准方法,通过始终在链表尾部追加新节点,可以实现:

  1. ​保持原始顺序​​的自然构建
  2. ​O(1)尾部插入​​(配合尾指针时)
  3. ​广泛用于队列​​等需要保持顺序的场景

🚀 操作流程演示​

输入:[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 就头皮发麻,感觉像在拆炸弹💣
  • 写个反转链表,结果把链表拧成了中国结🧶
  • 面试官问「怎么判断环」,你差点说出「用眼睛看」👀

但现在,你已经掌握了:
🔹 ​​头插法​​——让链表倒着长,比倒背圆周率还简单
🔹 ​​尾插法​​——像排队买奶茶一样自然构建链表
🔹 ​​快慢指针​​——链表界的「龟兔赛跑」,环不环的,跑两步就知道
🔹 ​​虚拟头结点​​——妈妈再也不用担心我删错头节点了

下次面试官再出链表题,请优雅地甩出代码,然后淡淡地说:
​「这道题啊,我用三种解法,您想听哪种?」​​ 😎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值