LeetCode 链表 队列 栈的问题

@

双向链表

146. LRU 缓存

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
  • 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。Link
  • 实现思路
  • 链表节点需存储 key value 键值对,否则只能删除尾节点,但无法清楚尾节点的哈希映射
  • get 函数
    • 哈希映射找不到:返回 -1
    • 哈希映射找到了:从链表中将此节点移出、然后移动到链表头部、返回该节点的值
  • put 函数
    • 哈希映射找不到:
      • 缓存已满:删除链表尾节点,创建新节点,插入链表头部,更新哈希映射
      • 缓存未满:创建新节点,插入链表头部,更新哈希映射
        • 合并:缓存已满则删除链表尾节点,然后创建新的并插入到头部,更新哈希映射
    • 哈希映射找到了: 将节点移动到头部,更新节点的 value 值

  • 双向链表用于缓存,哈希映射用于查找
  • 借用现有的 list 容器以及迭代器实现双向链表的功能
    • list 的底层实现就是基于双向链表
    • 删除缓存中的关键字、将关键字插入到头部;实现了关键字提前
    • 操作缓存以后注意 更新哈希映射
    • List 底层基于双向链表实现,清除一个元素后其他元素的迭代器不受影响,但是需要对当前元素的迭代器更新哈希映射才行
class LRUCache {
public:
LRUCache(int capacity) : cap(capacity){
}
int get(int key) {
if (hashtable.find(key) == hashtable.end()) return -1;
auto key_value = *hashtable[key]; // * iterator, 即取出list中那一对 pair 的值
// 先将该缓存删除,然后插入到 list 的首部,实现最近使用
cache.erase(hashtable[key]);
cache.push_front(key_value);
hashtable[key] = cache.begin(); // 更新哈希映射
return key_value.second;
}
void put(int key, int value) {
// 如果关键字没在缓存中
if (hashtable.find(key) == hashtable.end()) {
if (hashtable.size() == cap) { // 如果缓存已满
// 删除最后一个缓存,清除其哈希映射
hashtable.erase(cache.back().first);
cache.pop_back();
}
}else {
cache.erase(hashtable[key]); // 关键字在缓存中
}
cache.push_front({key, value}); // 将关键字加入缓存首部,并更新哈希映射
hashtable[key] = cache.begin();
}
private:
int cap;
list<pair<int,int>> cache;
unordered_map<int, list<pair<int,int>>::iterator> hashtable;
};

  • 手动实现双向链表的方法
  • 建立头、尾节点后先将其链接起来
  • 建立了key、节点指针的映射,因此移动节点后不需要更新哈希映射
  • 先从链表移除节点、再移动到链表头部
    • 从链表移除节点 node
      在这里插入图片描述

    • 将节点 node 移动到链表头部
      在这里插入图片描述

class LRUCache {
public:
struct DLinkNode {
int key, value;
DLinkNode *prev, *next;
DLinkNode (){};
DLinkNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {};
};
LRUCache(int capacity) : cap(capacity){
// 建立头、尾两个节点便于查找
head = new DLinkNode(-1, -1);
tail = new DLinkNode(-1, -1);
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (hash.find(key) == hash.end()) return -1;
DLinkNode* node = hash[key];
// 将此节点从原来的位置删除,然后移动到头部
removeNode(node); // 非常重要
moveHead(node);
return node->value;
}
void put(int key, int value) {
if (hash.find(key) == hash.end()) {
if (hash.size() == cap) {
DLinkNode *node = removeTail();
// 删除最后一个节点,并清除哈希映射
hash.erase(node->key);
delete node;
}
// 增加节点并建立哈希映射
addHead(key, value);
hash[key] = head->next;
}else {
DLinkNode* node = hash[key];
node->value = value;
// 将此节点从原来的位置删除并移动到头部
removeNode(node);
moveHead(node);
}
}
void moveHead(DLinkNode* node) {
DLinkNode *nextNode;
nextNode = head->next;
// 将当前节点插入到 head 和 head 的下一节点中间,即头部
node->prev = head;
node->next = nextNode;
nextNode->prev = node;
head->next = node;
}
void addHead(int key, int value) {
// 创建一个新的节点,并移动到链表头部
DLinkNode *node = new DLinkNode(key, value);
moveHead(node);
}
void removeNode(DLinkNode* node) {
// 将当前节点从链表中移除
DLinkNode *prevN = node->prev;
DLinkNode *nextN = node->next;
prevN->next = nextN;
nextN->prev = prevN;
}
DLinkNode* removeTail() {
DLinkNode* node = tail->prev; // 链表的尾部节点
removeNode(node); // 将此节点从链表移除
return node; // 返回
}
private:
int cap;
DLinkNode *head;
DLinkNode *tail;
unordered_map<int, DLinkNode*> hash;
};

快慢指针

  • 快慢指针的适用范围
    • 找到链表的中点
    • 找到链表的倒数第 k 个节点
    • 判定环形链表

148. 排序链表

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

归并排序,先递归二等分,然后最后合并链表

  • 快慢指针找链表中点

  • 起始时令 fast 领先 slow 一个节点 较为重要!

  • 奇数个节点时找到中点 slow

  • 偶数个节点时找到中心左边的节点 slow

ListNode* fast = head->next; // 领先 slow 一个节点
ListNode* slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* fast = head;
ListNode* slow = head;
  • 令 fast 和 slow 起始位置相同
  • 奇数个节点时找到中点 slow
  • 偶数个节点时找到中心右边的节点 slow

class Solution {
public:
ListNode* sortList(ListNode* head) {
if (!head || !head->next) {
return head;
}
ListNode* fast = head->next;
ListNode* slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* temp = slow->next;
slow->next = nullptr; // 将链表一分为2
ListNode* left = sortList(head);
ListNode* right = sortList(temp);
ListNode* h = new ListNode(0);
ListNode* dummyhead = h;
// 对链表进行合并排序
while (left && right) {
if (left->val < right->val) {
h->next = left;
left = left->next;
}else {
h->next = right;
right = right->next;
}
h = h->next;
}
h->next = left != nullptr ? left : right;
return dummyhead->next;
}
};

143. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … Link

  • 不开辟新的空间
    • 快慢指针找到链表的中点并切开链表
    • 将链表的后半部分进行反转
    • 将两部分链表按序进行连接
class Solution {
public:
void reorderList(ListNode* head) {
if (!head || !head->next || !head->next->next) {
return;
}
ListNode *slow = head, *fast = head;
// 快慢指针找到链表中点
// [1,2,3,4] 偶数个节点,slow 为中心右边的节点
// [1,2,3,4,5] 奇数个节点,slow 为中间
// 无论奇偶,左半部分总比右半部分多一个节点
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// 翻转后半段的链表,并将前半段链表末尾置空
ListNode* head2 = reverseList(slow->next);
slow->next = nullptr; // 切开链表,否则合并时无结束标志
// 合并两段链表
ListNode *temp1, *temp2;
while (head && head2) {
temp1 = head->next;
head->next = head2;
temp2 = head2->next;
head2->next = temp1;
head = temp1;
head2 = temp2;
}
}
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* prev = nullptr;
ListNode* temp;
while (head) {
temp = head->next;
head->next = prev;
prev = head;
head = temp;
}
return prev;
}
};

  • 使用双向队列存储所有的链表节点
    • 将所有节点加入双向队列
    • 队头指向队尾,弹出队头;队尾指向队头,弹出队尾
    • 根据队列中剩余的节点数量进行最后的处理
class Solution {
public:
void reorderList(ListNode* head) {
deque<ListNode*> q;
while (head) {
q.push_back(head);
head = head->next;
}
// q.size() > 2 确保一次能弹出两个节点进行处理
while (!q.empty() && q.size() > 2) {
q.front()->next = q.back();
q.pop_front();
q.back()->next = q.front();
q.pop_back();
}
// 根据剩余的节点数量分别处理
if (q.size() == 2) {
q.front()->next = q.back();
q.pop_front();
q.back()->next = nullptr;
q.pop_back();
}else {
q.front()->next = nullptr;
q.pop_front();
}
}
};

剑指 Offer II 027. 回文链表

给定一个链表的 头节点 head ,请判断其是否为回文链表。Link

  • 快慢指针等分链表,翻转后半部分逐一比较
    • 节点为偶数个:保证两部分节点数相同
    • 节点为奇数个:保证前半部分节点数多一个
    • 故起始时令 fast 领先 slow 一个节点
    • 必须保证将链表切开,即两部分链表的末尾均为空节点
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
ListNode* fast = head->next;
ListNode* slow = head;
ListNode* p = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* q = reverseList(slow->next);
slow->next = nullptr; // 断开节点连接
while (p && q) {
if (p->val == q->val) {
p = p->next;
q = q->next;
}else {
return false;
}
}
return true;
}
ListNode* reverseList(ListNode* head) {
// 翻转链表实现函数
if (!head || !head->next) return head;
ListNode *prev = nullptr;
ListNode *next;
while (head) {
next = head->next;
head->next = prev;
prev = head;
head = next;
}
return prev;
}
};

82. 删除排序链表中的重复元素 II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。Link

  • 递归法
  • 1 - 1 - 1 - 2 - 2 - 3 - 4 - 5
  • 1、头节点等于下一节点
    • 找到第一个不等于头节点的位置 2、返回其递归值
    • 相当于删除了本次重复的部分 2 - 2- 3- 4- 5
  • 2、头节点不等于下一节点 3 - 4 - 5
    • 当前节点指向下一节点的递归结果
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (!head || !head->next) return head;
if (head->val != head->next->val) {
head->next = deleteDuplicates(head->next);
}else {
ListNode* move;
move = head->next;
while (move && head->val == move->val) {
move = move->next;
}
return deleteDuplicates(move);
}
return head;
}
};
  • 一次遍历法
  • 1 - 1 - 1 - 2 - 2 - 3 - 4 - 5
  • 1、创建 哑节点 便于处理删除头节点的情况
    • 元素相同时,只改变当前节点的下一节点指向,不改变当前节点
    • 使用 cur.next 进行处理,结束时 cur.next = 2,即 哑节点 的下一节点从 2 开始,也就是递归处理 头节点为 2 的情况
  • 2、创建临时变量存储相同元素的值
    • 这样就不用保留一个节点来进行值的判断
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode* dummyHead = new ListNode(0, head);
ListNode* cur = dummyHead;
// 确保有两个节点存在
while (cur->next && cur->next->next) {
// cur.next = 1; 2
if (cur->next->val == cur->next->next->val) {
int x = cur->next->val;
while (cur->next && cur->next->val == x) {
cur->next = cur->next->next;
}
// cur.next = 2; 相当于
}else {
cur = cur->next;
}
}
return dummyHead->next;
}
};

142. 环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。Link

  • 如链表有环、快慢指针定会相遇
  • 假设入环后,快指针每次走 b 步,慢指针每次走 a 步,t 次以后两者相遇,环中共有 L 个节点
    • t * (b - a) / L 为快指针需要跑多少圈才能与慢指针相遇,其应该为整数
    • n = t * (b - a) / L 越小,说明走的圈数越少,此时 b - a = 1
    • b - a 为任意整数,均能实现两者相遇,区别就是要走的圈数
  • 以 a = 1, b = 2 为例,假设慢指针走了 s 步,则快指针走了 f = 2s 步,同时快指针比慢指针多走了 n 个环的长度,即 f = s + nL,可得 f = 2nL, s = nL;
    • 假设环的入口前有 K 个节点,则慢指针走到入口共需要 K + nL 步,因为每多走 L 步就能回到入口
    • 此时令快指针指向头节点,快、慢指针同时各走一步,K 步以后便可以共同到达入口
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if (!head || !head->next) {
return head;
}
ListNode* slow = head, *fast = head;
while (true) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
fast = head;
while (true) {
slow = slow->next;
fast = fast->next;
if (slow == fast) {
break;
}
}
return slow;
}
};

链表翻转

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

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。Link

  • 递归翻转
    • 判断是否大于两个节点,少于两个节点直接返回头节点
    • 翻转前两个节点,将头节点指向递归翻转的结果
    • 1 > 2 > 3 > 4 > 5 ---- 2 > 1 > swap(3 > 4 > 5)
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 递归的终止条件
if (!head || !head->next) {
return head;
}
ListNode* nexthead = head->next->next; // 下一次翻转的头节点
ListNode* cur = head->next;
cur->next = head; // 两个节点翻转
head->next = swapPairs(nexthead); // 与后面的结果相连接
return cur;
}
};
  • 一种更简洁的实现方式
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode *next = head->next;
head->next = swapPairs(next->next); // 下一次的翻转
next->next = head; // 本次两个节点的翻转
return next;
}
};

25. K个一组翻转链表

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

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

设计一个只使用常数额外空间的算法来解决此问题 Link

  • 两两交换链表的升级版
  • 思路
    • 利用辅助栈,将 k 个非空节点压入栈中,依次弹出完成翻转,将第 k + 1 个节点进行递归翻转,栈中最后一个元素指向下一次翻转的结果
    • 选择前 k 个节点,存储第 k + 1 个节点,将前 k 个节点切开然后进行链表翻转,头节点指向下一次翻转的结果,选择不了 k 个节点,直接返回 head

辅助栈法

class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
int cnt = 0;
stack<ListNode*> st;
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* cur = dummyhead;
// 将前 k 个非空节点入栈
// head 判空很重要,否则 1 > 2 > 3 > NULL
// k = 4 时,会将空节点 NULL,这显然不符合题意
while (head != nullptr && cnt < k) {
st.push(head);
cnt++;
head = head->next;
}
// 确保有 k 个节点,进行翻转
if (cnt == k) {
while (!st.empty()) {
cur->next = st.top();
st.pop();
cur = cur->next;
}
// 栈中最后一个节点指向下一次翻转结果
cur->next = reverseKGroup(head, k);
}
return dummyhead->next;
}
};

切割链表法

class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
if (!head || !head->next) return head;
int cnt = 1; // cur 指向了第一个节点,因此从 1 计数
ListNode* prev = head;
ListNode* cur = head;
ListNode* next;
while (cur != nullptr && cnt < k) {
cur = cur->next;
cnt++;
}
// 此处 cur 判空非常重要
// 1 > 2 > NULL k = 3 时,此时虽然 cnt 为 3,但是 cur 为空,说明不够 k 个节点,不需要进行翻转
if (cur != nullptr && cnt == k) {
next = cur->next; // 保存下一次翻转的头节点
cur->next = nullptr; // 切开前 k 个节点
cur = head->next; // 从第 2 个节点开始翻转
while (cur) {
ListNode* next1 = cur->next;
cur->next = prev;
prev = cur;
cur = next1;
}
// prev 为翻转后的第一个节点
// 头节点指向下一次翻转的结果
head->next = reverseKGroup(next, k);
return prev;
}else {
return head;
}
}
};

复杂链表

剑指 Offer 35. 复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。Link

  • 哈希存储
  • 建立原节点、新节点的哈希映射,通过哈希映射构造复制后的链表
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return head;
unordered_map<Node*, Node*> hash;
Node* cur = head;
while (cur) {
hash[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
while (cur) {
hash[cur]->next = hash[cur->next];
// 如果 cur -> random 为空,哈希映射找不到,此时复制节点的 random 链接默认为空
hash[cur]->random = hash[cur->random];
cur = cur->next;
}
return hash[head];
}
};

  • 拼接、拆分
  • 将创建的新节点跟在原节点的后面
  • 创建指针 cur 遍历原节点,如果原节点的 random 非空,则令 cur->next->random = cur->random->next;
  • 对拼接链接进行拆分:
    • pre = head, cur = head->next;
    • pre->next = pre->next->next; cur->next = cur->next->next
    • pre 要在 cur 前操作,否则 pre 找不到正确的下一节点,即应该先断开黄色节点与蓝色节点的链接
    • cur->next 为空时,说明到达了链表的结尾,可以停止
      在这里插入图片描述
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return head;
// 1. 复制新节点跟在原节点后面
Node *cur = head;
while (cur) {
Node* temp = new Node(cur->val);
temp->next = cur->next;
cur->next = temp;
cur = temp->next;
}
// 2. 调整新节点的 random 指针
cur = head;
while (cur) {
// 如果原节点 random 非空的话,复制的节点自然在 random 节点的下一个
if (cur->random) {
cur->next->random = cur->random->next;
}
cur = cur->next->next;
}
// 3. 拆分链表,返回复制链表的头节点
cur = head->next;
Node *pre = head, *res = head->next;
while (cur->next) {
// pre 要在 cur 之前,这很重要
pre->next = pre->next->next;
cur->next = cur->next->next;
// 如果 cur 在前的话, 将会阻止 pre 找到其下一个节点
// 1 - 1‘ - 2 - 2’
// 1' - 2'
pre = pre->next;
cur = cur->next;
}
pre->next = nullptr;
return res;
}
};

本文作者:GreyWang

本文链接:https://www.cnblogs.com/GreyWang/p/17124736.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   GreyWang  阅读(16)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起