LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解)

LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解)

本篇补全 Hot100 链表专题剩余 5 道题:25 / 138 / 148 / 23 / 146

目录

  • [LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解)](#LeetCode Hot 100 | 链表(下)· 分组翻转与设计(C++ 题解))
    • [一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难](#一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难)
    • [二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等](#二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等)
    • [三、148. Sort List(排序链表)🟡 中等](#三、148. Sort List(排序链表)🟡 中等)
      • 题目描述
      • 图解
      • [解题思路(对应你的 middle + meger + 递归)](#解题思路(对应你的 middle + meger + 递归))
      • [C++ 代码](#C++ 代码)
    • [四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难](#四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难)
    • [五、146. LRU Cache(LRU 缓存)🟡 中等](#五、146. LRU Cache(LRU 缓存)🟡 中等)
      • 题目描述
      • 图解
      • [解题思路(对应你的:哈希 + 环形双向链表)](#解题思路(对应你的:哈希 + 环形双向链表))
      • [C++ 代码](#C++ 代码)
    • 总结

一、25. Reverse Nodes in k-Group(K 个一组翻转链表)🔴 困难

题目描述

给你链表的头节点 head,每 k 个节点一组进行翻转,返回修改后的链表。

k 是正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯改变节点内部的值,而是需要实际进行节点交换。

示例 1:

复制代码
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

复制代码
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

图解

解题思路(严格对应下面这份代码)

你的代码核心是三件套:dummynode0、一组内的头插翻转。

1. 先量长度

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode dummy(0, head);
        ListNode *node0 = &dummy;
        ListNode *cur = head;
        ListNode *prev = nullptr;
        ListNode *tmp = nullptr;
        int len = 0;
        
        //get len
        while(cur) {
            len++;
            cur = cur->next;
        }
        cur = head;
        
        while (len >= k) {
            len -= k;
            for (int i = 0; i < k; i++) {
                tmp = cur->next;
                cur->next = prev;
                prev = cur;
                cur = tmp;
            }
            
            tmp = node0->next;
            tmp->next = cur;
            node0->next = prev;
            node0 = tmp;
        }
        
        return dummy.next;
    }
};

先知道总共能完整翻转几组。后面用 while (len >= k),每翻一组就 len -= k

对示例 [1,2,3,4,5], k=2len=5,能翻 2 组,最后单独的 5 不动------这正好对应代码逻辑。

2. 一组内:经典 4 行头插

cpp 复制代码
for (int i = 0; i < k; i++) {
    tmp = cur->next;
    cur->next = prev;
    prev = cur;
    cur = tmp;
}

这一段和「反转链表」一模一样。循环结束时:

  • prev:本段新头
  • cur:下一段起点(还没翻)
  • 本段旧头(翻转前 node0->next)现在变成段尾

3. 用 node0 把段接回主链

cpp 复制代码
tmp = node0->next;   // 旧段头 = 新段尾
tmp->next = cur;     // 段尾接到剩余链表
node0->next = prev;  // 前驱接到新段头
node0 = tmp;         // 前驱挪到段尾,准备下一组

dummy 挂在最前面,最后 return dummy.next。整段过程原地完成,没有额外数组。

为什么正确?

每组恰好翻 k 个,组与组之间靠 node0 串起来;不足 k 的尾巴因为进不了 while (len >= k),保持原序。

复杂度: 时间 O(n)(每个节点最多进翻转循环一次),空间 O(1)

C++ 代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode dummy(0, head);
        ListNode *node0 = &dummy;
        ListNode *cur = head;
        ListNode *prev = nullptr;
        ListNode *tmp = nullptr;
        int len = 0;
        
        //get len
        while(cur) {
            len++;
            cur = cur->next;
        }
        cur = head;
        
        while (len >= k) {
            len -= k;
            for (int i = 0; i < k; i++) {
                tmp = cur->next;
                cur->next = prev;
                prev = cur;
                cur = tmp;
            }
            
            tmp = node0->next;
            tmp->next = cur;
            node0->next = prev;
            node0 = tmp;
        }
        
        return dummy.next;
    }
};

二、138. Copy List with Random Pointer(随机链表的复制)🟡 中等

题目描述

给你一个长度为 n 的链表,每个节点包含一个额外的随机指针 random,它可指向链表中的任何节点或空。

请构造这个链表的 深拷贝,并返回拷贝后的头节点。

示例:

复制代码
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

图解

解题思路(对应你的两遍哈希)

你没有用「交错插入」那套 O(1) 空间技巧,而是更直观的 unordered_map<Node*, Node*>

第一遍:只造新节点

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        unordered_map<Node*, Node*> umap;
        Node *cur = head;
        
        while (cur) {
            umap[cur] = new Node(cur->val);
            cur = cur->next;
        }
        
        cur = head;
        
        while (cur) {
            Node *newNode = umap[cur];
            newNode->next = umap[cur->next];
            newNode->random = umap[cur->random];
            cur = cur->next;
        }
        
        return umap[head];
    }
};

此时新节点的 next/random 都还是空的,但「旧地址 → 新地址」映射齐了。

第二遍:接线

cpp 复制代码
Node *newNode = umap[cur];
newNode->next = umap[cur->next];
newNode->random = umap[cur->random];

关键点:umap[nullptr] 在 C++ 里对 operator[] 会插入默认值------你这里依赖的是:当 cur->next/random 为空时,映射后也应为空。若判空更稳妥可以写成三元,但以你 AC 的版本为准,讲解按这版走。

最后 return umap[head]

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

C++ 代码

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        unordered_map<Node*, Node*> umap;
        Node *cur = head;
        
        while (cur) {
            umap[cur] = new Node(cur->val);
            cur = cur->next;
        }
        
        cur = head;
        
        while (cur) {
            Node *newNode = umap[cur];
            newNode->next = umap[cur->next];
            newNode->random = umap[cur->random];
            cur = cur->next;
        }
        
        return umap[head];
    }
};

三、148. Sort List(排序链表)🟡 中等

题目描述

给你链表的头节点 head,请将其按 升序 排列并返回排序后的链表。

示例 1:

复制代码
输入:head = [4,2,1,3]
输出:[1,2,3,4]

图解

解题思路(对应你的 middle + meger + 递归)

这就是链表归并排序,拆成你代码里的三个函数:

1. middle(head):断开左右两半

快慢指针前进,同时用 pre 记 slow 前驱:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middle(ListNode *head) {
        ListNode *slow = head;
        ListNode *fast = head;
        ListNode *pre;
        
        while(fast && fast->next) {
            pre = slow;
            fast = fast->next->next;
            slow = slow->next;
        }
        pre->next = nullptr;
        return slow;
    }

    ListNode * meger(ListNode *left, ListNode *right) {
        ListNode dummy;
        ListNode *node = &dummy;
        
        while (left && right) {
            if (left->val < right->val) {
                node->next = left;
                left = left->next;
            } else {
                node->next = right;
                right = right->next;
            }
            
            node = node->next;
        }
        
        if (left || right)
            node->next = left == nullptr ? right : left;
        
        return dummy.next;
    }
    ListNode* sortList(ListNode* head) {
        if (!head || !head->next)
            return head;
        
        ListNode *head2 = middle(head);
        
        ListNode *left = sortList(head);
        ListNode *right = sortList(head2);
        
        return meger(left, right);
    }
};

[4,2,1,3]:断开后左 4→2,右 1→3

注意:返回的是右半 head ,左半仍从原 head 开始------所以后面才能 sortList(head) / sortList(head2)

2. 递归

cpp 复制代码
if (!head || !head->next) return head;
ListNode *head2 = middle(head);
ListNode *left = sortList(head);
ListNode *right = sortList(head2);
return meger(left, right);

3. meger(你提交里的原函数名)

哑节点双指针,谁小接谁,最后接剩余。和「合并两个有序链表」同一套路。

复杂度: 时间 O(n log n),空间 O(log n)(递归栈)

说明:函数名 meger 是你 AC 提交里的原拼写,文章保留,不擅自改你的代码。

C++ 代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middle(ListNode *head) {
        ListNode *slow = head;
        ListNode *fast = head;
        ListNode *pre;
        
        while(fast && fast->next) {
            pre = slow;
            fast = fast->next->next;
            slow = slow->next;
        }
        pre->next = nullptr;
        return slow;
    }

    ListNode * meger(ListNode *left, ListNode *right) {
        ListNode dummy;
        ListNode *node = &dummy;
        
        while (left && right) {
            if (left->val < right->val) {
                node->next = left;
                left = left->next;
            } else {
                node->next = right;
                right = right->next;
            }
            
            node = node->next;
        }
        
        if (left || right)
            node->next = left == nullptr ? right : left;
        
        return dummy.next;
    }
    ListNode* sortList(ListNode* head) {
        if (!head || !head->next)
            return head;
        
        ListNode *head2 = middle(head);
        
        ListNode *left = sortList(head);
        ListNode *right = sortList(head2);
        
        return meger(left, right);
    }
};

四、23. Merge k Sorted Lists(合并 K 个升序链表)🔴 困难

题目描述

给你一个链表数组,每个链表都已经按升序排列。请把所有链表合并成一个升序链表并返回。

示例:

复制代码
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]

图解

解题思路(对应你的两两合并,不是堆)

你的主循环非常直接:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* merge(ListNode *left, ListNode *right) {
        ListNode dummy;
        ListNode *cur = &dummy;
        
        while (left && right) {
            if (left->val < right->val) {
                cur->next = left;
                left = left->next;
            } else {
                cur->next = right;
                right = right->next;
            }
            cur = cur->next;
        }
        
        if (left || right)
            cur->next = (left == nullptr) ? right : left;
        
        return dummy.next;
    }
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if (lists.empty())
            return nullptr;
        
        while (lists.size() > 1) {
            vector<ListNode*> tmp;
            for (int i = 0; i < lists.size(); i+=2) {
                ListNode *head1 = lists[i];
                ListNode *head2 = nullptr;
                if (i + 1 < lists.size())
                    head2 = lists[i + 1];
                tmp.push_back(merge(head1, head2));
            }
            lists = tmp;
        }
        
        return lists[0];
    }
};
  • 每轮把链表条数大约减半
  • 奇数条时 head2=nullptrmerge 等于原样保留那一条
  • merge 仍是哑节点双指针

对示例三轮思想:先合并前两条,第三条单独留下;再把结果和第三条合并。

复杂度: 设总节点 N、链表数 k,大约 O(N log k)

C++ 代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* merge(ListNode *left, ListNode *right) {
        ListNode dummy;
        ListNode *cur = &dummy;
        
        while (left && right) {
            if (left->val < right->val) {
                cur->next = left;
                left = left->next;
            } else {
                cur->next = right;
                right = right->next;
            }
            cur = cur->next;
        }
        
        if (left || right)
            cur->next = (left == nullptr) ? right : left;
        
        return dummy.next;
    }
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if (lists.empty())
            return nullptr;
        
        while (lists.size() > 1) {
            vector<ListNode*> tmp;
            for (int i = 0; i < lists.size(); i+=2) {
                ListNode *head1 = lists[i];
                ListNode *head2 = nullptr;
                if (i + 1 < lists.size())
                    head2 = lists[i + 1];
                tmp.push_back(merge(head1, head2));
            }
            lists = tmp;
        }
        
        return lists[0];
    }
};

五、146. LRU Cache(LRU 缓存)🟡 中等

题目描述

请你设计并实现一个满足 LRU(最近最少使用)缓存约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化
  • int get(int key) 如果关键字 key 存在于缓存,返回值,否则返回 -1
  • void put(int key, int value) 插入或更新;若容量满了,先淘汰最久未使用的

getput 必须在 O(1) 平均时间复杂度完成。

图解

解题思路(对应你的:哈希 + 环形双向链表)

你自己实现了双向链表节点,并用 root 做成 环形哨兵

cpp 复制代码
class LRUCache {
struct ListNode {
    ListNode *prev;
    ListNode *next;
    
    int value_;
    int key_;
    ListNode(int key = 0, int value = 0) {
        prev = NULL;
        next = NULL;
        value_ = value;
        key_ = key; 
    }
};

public:
    int capacity_;
    ListNode *root;
    unordered_map<int, ListNode *> umap;
    
    LRUCache(int capacity) {
        capacity_ = capacity;
        root = new ListNode();
        root->next = root;
        root->prev = root;
    }
    
    int get(int key) {
        if (umap.count(key)) {
            remove(umap[key]);
            headInsert(umap[key]);
            return umap[key]->value_;
        } else {
            return -1;
        }
    }
    
    void put(int key, int value) {
        if (umap.count(key)) {
            umap[key]->value_ = value;
            remove(umap[key]);
        } else {
            umap[key] = new ListNode(key, value);
        }
        
        headInsert(umap[key]);
        
        if (umap.size() > capacity_) {
            ListNode *tmp = umap[root->prev->key_];
            umap.erase(root->prev->key_);
            remove(tmp);
        }
    }
    
    void remove(ListNode *list) {
        ListNode* prev = list->prev;
        ListNode* next = list->next;
        
        prev->next = next;
        next->prev = prev;
    }
    
    void headInsert(ListNode *list) {
        ListNode *tmp;
        tmp = root->next;
        
        root->next = list;
        list->prev = root;
        list->next = tmp;
        tmp->prev = list;
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

约定:

  • root->next 一侧是 最新
  • root->prev 一侧是 最旧(淘汰就删它)
  • umap[key] -> ListNode* 保证 O(1) 定位

两个原语

  • remove(node):左右邻居互连,摘出去
  • headInsert(node):插到 root 后面(变成最新)

get

命中:removeheadInsert,返回 value_;否则 -1

put

  • 已存在:改值,先 remove
  • 不存在:new ListNode 放进 umap
  • 然后 headInsert
  • umap.size() > capacity_:删 root->prev,并 umap.erase

这就是标准 LRU:读写都把节点提到最前,满了丢最旧。

复杂度: get/put 均摊 O(1)

C++ 代码

cpp 复制代码
class LRUCache {
struct ListNode {
    ListNode *prev;
    ListNode *next;
    
    int value_;
    int key_;
    ListNode(int key = 0, int value = 0) {
        prev = NULL;
        next = NULL;
        value_ = value;
        key_ = key; 
    }
};

public:
    int capacity_;
    ListNode *root;
    unordered_map<int, ListNode *> umap;
    
    LRUCache(int capacity) {
        capacity_ = capacity;
        root = new ListNode();
        root->next = root;
        root->prev = root;
    }
    
    int get(int key) {
        if (umap.count(key)) {
            remove(umap[key]);
            headInsert(umap[key]);
            return umap[key]->value_;
        } else {
            return -1;
        }
    }
    
    void put(int key, int value) {
        if (umap.count(key)) {
            umap[key]->value_ = value;
            remove(umap[key]);
        } else {
            umap[key] = new ListNode(key, value);
        }
        
        headInsert(umap[key]);
        
        if (umap.size() > capacity_) {
            ListNode *tmp = umap[root->prev->key_];
            umap.erase(root->prev->key_);
            remove(tmp);
        }
    }
    
    void remove(ListNode *list) {
        ListNode* prev = list->prev;
        ListNode* next = list->next;
        
        prev->next = next;
        next->prev = prev;
    }
    
    void headInsert(ListNode *list) {
        ListNode *tmp;
        tmp = root->next;
        
        root->next = list;
        list->prev = root;
        list->next = tmp;
        tmp->prev = list;
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

总结

题号 题目 难度 你的写法核心 时间复杂度
25 K 个一组翻转链表 🔴困难 dummy + 分组头插 O(n)
138 随机链表的复制 🟡中等 两遍哈希映射 O(n)
148 排序链表 🟡中等 归并(middle/meger) O(n log n)
23 合并 K 个升序链表 🔴困难 两两 merge O(N log k)
146 LRU 缓存 🟡中等 哈希 + 环形双向链表 O(1)

链表下篇的本质: 指针关系能画清楚,代码就只是把图翻译成 next/prev

相关推荐
choumin2 小时前
行为型模式——中介者模式
c++·设计模式·中介者模式·行为型模式
Lee_jerome13 小时前
《C/C++编译全家桶:g++四阶段、.a与.so区别、CMake构建、ARM交叉编译,这篇全讲透了》
c语言·开发语言·arm开发·c++·编译·cmake·cmakelists
冰心孤城13 小时前
C++ 与 C#混合编程 示例 (基于VS)
java·c++·c#
一只旭宝17 小时前
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析
开发语言·c++·面试
神明不懂浪漫17 小时前
【第三章】链表
开发语言·数据结构·经验分享·笔记·链表
开发者联盟league17 小时前
Java 通过 JNA 调用 C++ DLL:关键流程总结
java·c++·jna
tkevinjd17 小时前
力扣131-分割回文串
算法·leetcode·深度优先
库克克18 小时前
【C++】智能指针
java·开发语言·c++
一拳一个呆瓜20 小时前
【STL】iostream 编程:输入流成员函数
c++·stl