【专题九】链表

📝前言说明:

  • 本专栏主要记录本人的基础算法学习以及LeetCode刷题记录,按专题划分
  • 每题主要记录:(1)本人解法 + 本人屎山代码;(2)优质解法 + 优质代码;(3)精益求精,更好的解法和独特的思想(如果有的话)
  • 文章中的理解仅为个人理解。如有错误,感谢纠错

🎬个人简介:努力学习ing
📋本专栏:C++刷题专栏
📋其他专栏:C语言入门基础python入门基础C++学习笔记Linux
🎀CSDN主页 愚润泽

你可以点击下方链接,进行该专题内不同子专题的学习

点击链接开始学习
双指针(1)双指针(2)
双指针(3)双指针(4)
滑动窗口(1)滑动窗口(2)
滑动窗口(3)滑动窗口(4)
二分查找(1)二分查找(2)
前缀和(1)前缀和(2)
前缀和(3)位运算(1)
位运算(2)模拟算法
快速排序归并排序
链表哈希表
字符串
队列 + 宽搜优先级队列
BFS 解决 FloodFillBFS 解决最短路径
多源 BFSBFS 解决拓扑排序

题单汇总链接:点击 → 题单汇总(暂时未整理,因为还没刷完)


链表常用技巧和操作

常用技巧

  1. 画图模拟
  2. 使用虚拟头节点
  3. 不要吝啬空间,大胆去定义变量(可以让我们思路更清晰,且不会出现节点丢失问题)
  4. 快慢双指针
    • 判环
    • 找链表环的入口
    • 找链表中的倒数第 n 个节点

常用操作

  • new 新节点
  • 尾插
  • 头插(插出来的是逆序的)

2. 两数相加

题目链接:https://leetcode.cn/problems/add-two-numbers/description/
在这里插入图片描述

个人解

屎山代码:

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {
        ListNode dummy; 
        // 哨兵节点,哨兵节点的 next 会被设置成第一个头结点
        // 又因为设置一次以后,dummy->next就不会被改变了,所以dummy.next始终是链表的头结点
        // 让 cur 直接等于哨兵节点,统一操作,在第一次设置 cur->next的时候设置的就是 dummy ->next
        ListNode* cur = &dummy;
        int carry = 0;
        while(l1 || l2 || carry)
        {
            if(l1)
            {
                carry += l1->val;
                l1 = l1->next;
            }
            if(l2)
            {
                carry += l2->val;
                l2 = l2->next;
            }
            cur->next = new ListNode(carry % 10);
            cur = cur->next;
            carry /= 10;
        }
        return dummy.next;
    }
};

时间复杂度: O ( m + n ) O(m + n) O(m+n), m 和 n为两个链表的长度
空间复杂度: O ( m a x ( m , n ) ) O(max(m,n)) O(max(m,n))


24. 两两交换链表中的节点

题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description/
在这里插入图片描述

个人解

思路:

  • 注意,每一对交换完以后要连上后面的对
  • 步骤一: 0->next = 2;
  • 步骤二: 1->next = 2->next(也就是 3 );
  • 步骤三: 2->next = 1;
  • 然后: 1 的位置变成新的 0, 3 变成新的 1, 3->next 变成新的 2

屎山代码:

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode dummy(0, head);
        ListNode* cur0 = &dummy;
        ListNode* cur1 = head;
        while(cur1 && cur1->next)
        {
            ListNode* cur2 = cur1->next;
            // 交换
            cur0->next = cur2;
            cur1->next = cur2->next;
            cur2->next = cur1;
            // 更新 cur0 和 cur1
            cur0 = cur1;
            cur1 = cur1->next;
        }
        return dummy.next;
    }
};

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)


143. 重排链表

题目链接:https://leetcode.cn/problems/reorder-list/description/
在这里插入图片描述

优质解

代码

class Solution {
public:

    ListNode* reverseList(ListNode* head)
    {
        // 反转一个链表需要 pre, cur, nxt 三个节点
        ListNode* pre = nullptr, *cur = head;
        while(cur)
        {
            ListNode* nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre; // pre 指向反转后链表的头结点
    }

    ListNode* FindMid(ListNode* head) // 后半部分可能更长
    {
        ListNode* p1 = head, * p2 = head;
        while(p2 && p2->next)
        {
            p1 = p1->next;
            p2 = p2->next->next;
        }
        return p1;
    }
    void reorderList(ListNode* head) 
    {
        // 1. 找到中间节点
        ListNode* mid = FindMid(head);
        // 2. 反转后半部分链表
        ListNode* head2 = reverseList(mid);
        // 3. 在把后半部分链表插入前面的链表
        while(head2->next)  // 为什么不是 head2 (因为后半部分更长,而原来的前半部分的最后一个节点的next指针:刚好指向的是后半部分反转以后的最后一个节点,因此相当于已经把"长"出来那一部分插入了)
        {
            ListNode* nxt1 = head->next;
            ListNode* nxt2 = head2->next;
            head2->next = head->next;
            head->next = head2;
            head = nxt1;
            head2 = nxt2;
        }
    }
};

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)


23. 合并 K 个升序链表

题目链接:https://leetcode.cn/problems/merge-k-sorted-lists/description/
在这里插入图片描述


优质解

思路:

  • 使用堆priority_queue(默认是大根堆),我们改成小根堆,然后每次pop出一个节点
  • 如果插入priority_queue的是节点,并且还要是小根堆,则我们要自己实现比较器(底层return a > b
  • 注意,题目中lists存储的是各个链表的头结点(因为链表是由头结点组织的)
  • 解题思路:
    1. 由于所有链表都是升序的,所以我们可以先把所有链表的头结点加入堆(把元素插入pq的时候会自动调整堆),然后找里面的最小值作为头结点
    2. 那么下一个节点有两种选择:要么是其他链表的节点(已经在堆中),要么还是当前链表的下一个节点(因为不知道下一个节点和其他链表节点的大小关系)

代码:

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
        auto cmp = [](ListNode* a, ListNode* b){
            return a->val > b->val;
        };
        priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
        // 先加入所有链表的头结点
        for(auto head: lists)
        {
            if(head) // 可能会有空链表
                pq.push(head);
        }
        ListNode dummy; // 哨兵节点(创建对象-> 能分配内存)
        ListNode* cur = &dummy;
        while(!pq.empty())
        {
            auto mini = pq.top();
            pq.pop();
            cur->next = mini;
            cur = cur->next;
            if(mini->next) // 如果当前的链表后面还有节点
                pq.push(mini->next);
        }
        return dummy.next;
    }
};

时间复杂度: O ( N ∗ l o g K ) O (N*logK) O(NlogK)

  • N 是所有链表的节点总数,K 是链表数量(也是堆里面的节点数量),节点插入删除调整堆的时间复杂度是 O ( l o g K ) O(logK) O(logK)

空间复杂度: O ( k ) O(k) O(k)

注意:

  • priority_queue的原型:
template<
    class T,                     // 元素类型
    class Container = vector<T>, // 底层容器
    class Compare = less<T>      // 比较器类型
> class priority_queue;
  • 比较器Compare
auto cmp = [](ListNode* a, ListNode* b){
    return a->val > b->val;
}

底层提供a > b实现的是小根堆。(默认的是a < b大根堆)

  • decltype(cmp)decltype可以自动推导类型

25. K 个一组翻转链表

题目链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/description/
在这里插入图片描述


优质解

思路:

// 一个链表反转后 pre 指向头结点,cur 指向下一个要反转的链表的头结点
// 每个区间的链表我们可以翻转了,怎么链接起来呢?
// 我们可以多创建一个节点,用来将每个区间反转后的链表进行链接
// 创建一个 p0 始终指向上一个链表的末尾, 且 p0->next 记录了下一个链表反转前的起点
// 上一个链表的链表尾(p0) 要链接 反转后链表的链表头 pre
// 反转后链表的链表尾(p0->next, 原来的链表头) 要链接 下一个链表的链表头(cur)

代码:

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* count = head;
        int n = 0;
        while(count != nullptr)
        {
            n++;
            count = count -> next;
        }
        ListNode dummy(0, head);
        ListNode* p0 = &dummy; // 第一次修改时直接设置 dummy, dummy 跟随 p0
        ListNode* pre = nullptr;
        ListNode* cur = head;
        for(int i = 0; i < n / k; i++) // 总共要进行 n/k 次,区间长为 k 的链表的反转
        {
            for(int j = 0; j < k; j++) // 一组内反转
            {
                ListNode* nxt = cur->next;
                cur->next = pre;
                pre = cur;
                cur = nxt;
            }
            ListNode* p0_nxt = p0->next; // 反转后链表的末尾,用于 p0 更新
            p0->next->next = cur;
            p0->next = pre;
            p0 = p0_nxt;
        } 
        return dummy.next;
    }
};

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)


🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愚润泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值