【LeetCode 热题 100】链表 系列
📁206. 反转链表
对于每一个节点cur,都是将next节点指向cur,cur节点指向上一个节点head。因此可以采用递归的策略,从后往前进行上述操作,期间记录最后一个节点并返回。
我是将递归分为3类:
1. 前序递归:在递归之前进行处理。
2. 中序递归:在递归中进行处理。
3. 后续递归:在递归后处理。
本地就是一个后续递归的操作,地递归到最后一层,从后往前进行处理。
ListNode* reverseList(ListNode* head) {if(head == nullptr || head->next == nullptr)return head;ListNode* newHead = reverseList(head->next);ListNode* next = head->next;head->next = next->next;next->next = head;return newHead;}
📁160. 相交链表
我们假设A链表总结点个数是a,B链表总结点个数是b,如果存在公共节点node,那么我们假设公共节点后面所有节点个数是c。
指针 cur1 先遍历完链表 headA
,再开始遍历链表 headB
,当走到 node
时,共走步数为:
a+(b−c)
指针 cur2 先遍历完链表 headB
,再开始遍历链表 headA
,当走到 node
时,共走步数为:
b+(a−c)
那么可以列式,并有两种情况:
a+(b−c)=b+(a−c)
1. 如果两链表有公共节点(c > 0):节点 cur1 和 cur2 指向公共节点node
2. 如果不存在公共节点(c = 0):节点 cur1 和 cur2 都指向nullptr
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {ListNode* cur1 = headA , *cur2 = headB;while(cur1 != cur2){cur1 = cur1 != nullptr ? cur1->next : headB;cur2 = cur2 != nullptr ? cur2->next : headA;}return cur1;}
📁234. 回文链表
一个简单的思路就是取出链表中的元素,放到数组中,使用双指针判断数组是否是回文。
bool isPalindrome(ListNode* head) {vector<int> ret;while(head){ret.push_back(head->val);head = head->next;}int right = ret.size() - 1 , left = 0;while(left < right){if(ret[left++] != ret[right--])return false;}return true;}
📁141. 环形链表
我们来假设一种情况,在一条无限长的跑道上,小明每秒走x步,小妹每秒走x+1步,并且小美在小明后面追赶,如果情况成立小美一定能追上小明,因为他们之间的距离每次都1。
对于该题目,采用双指针思路,如果链表有环,那么slow指针进入换后就一定能被fast指针追上。
bool hasCycle(ListNode *head) {ListNode* fast = head;ListNode* slow = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast)return true;}return false;}
📁142. 环形链表 II
符号定义与初始条件
- 头节点(
head
):链表的起点。 - 环入口节点(
A
):链表开始形成环的节点。 - 相遇点(
B
):快慢指针第一次相遇的位置。 - a:头节点到环入口节点的距离。
- b:环入口节点到相遇点的距离。
- c:相遇点到环入口节点的剩余距离。
- 环周长:
n = b + c
(环的总长度)。 - 快指针速度:每次移动 2 步。
- 慢指针速度:每次移动 1 步。
Sfast=2⋅Sslow⟹a+k(b+c)+b=2(a+b)
将上述方程化简:
a+k(b+c)+b=2a+2b⟹k(b+c)=a+b⟹ a=(k−1)(b+c)+c
- 等于 c 加上
(k - 1)
圈环的周长。 - 若
k = 1
,则a = c
,即从头节点到环入口的距离等于相遇点到环入口的距离。
ListNode *detectCycle(ListNode *head) {ListNode* fast = head;ListNode* slow = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast){while(slow != head){slow = slow->next;head = head->next;}return head;}}return nullptr;}
📁2. 两数相加
本题就是一道类似高精度的问题,但是本题相较简单,我们只需要从后往前遍历即可,每次取出两个个位数相加,对结果进行模除操作结果放到新节点中即可。
此外,需要注意的是,最后需要判断最后一位数相加是否还需要进位。
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode* newHead = new ListNode();ListNode* tail = newHead;int tmp = 0;while(l1 != nullptr || l2 != nullptr){if(l1){tmp += l1->val;l1 = l1->next;}if(l2){tmp += l2->val;l2 = l2->next;}tail->next = new ListNode(tmp % 10);tmp /= 10;tail = tail->next;}if(tmp)tail->next = new ListNode(tmp);tail = newHead->next;delete newHead;return tail;}
📁 21. 合并两个有序链表
最简单的一个思路就是创建一个新的链表,然后遍历两个链表,将较小值的节点尾插到新节点中。
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {ListNode* cur1 = list1 , *cur2 = list2;ListNode* newHead = new ListNode();ListNode* tail = newHead;while(cur1 && cur2){if(cur1->val < cur2->val){tail->next = cur1;tail = tail->next;cur1 = cur1->next;}else{tail->next = cur2;tail = tail->next;cur2 = cur2->next;}}while(cur1){tail->next = cur1;tail = tail->next;cur1 = cur1->next;}while(cur2){tail->next = cur2;tail = tail->next;cur2 = cur2->next;}tail = newHead->next;delete newHead;return tail;}
也可以采用递归的方法。
1. 递归函数的返回值:返回合并后链表的头结点。
2. 递归函数结束条件:如果有一个链接节点为空,返回另一个节点。
3. 递归过程:比较两个链表的节点,取出较小值作为当前轮次的头结点,它的next指向递归的返回值,即两个链表剩下节点合并后链表的头结点。
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {if(!list1) return list2;if(!list2) return list1;if(list1->val < list2->val)list1->next = mergeTwoLists(list1->next , list2);else list2->next = mergeTwoLists(list1 , list2->next);return list1->val < list2->val ? list1 : list2;}
📁 19. 删除链表的倒数第 N 个结点
从头节点开始对链表进行一次遍历,得到链表的长度 L。随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L− n + 1 个节点时,它就是我们需要删除的节点。
ListNode* removeNthFromEnd(ListNode* head, int n) {int sz = 0;ListNode* cur = head;while(cur){++sz;cur = cur->next;}//创建新的头结点方便处理只有一个节点的特殊情况ListNode* newhead = new ListNode( 0 , head);cur = newhead;for(int i = 0 ; i < sz - n; ++i){cur = cur->next;}cur->next = cur->next->next;return newhead->next;}
📁 24. 两两交换链表中的节点
本题我们可以看成反转链表的一种变种,只不过是变成两两交换了,第一个节点head , 第二个节点next,head节点next指向剩余链表中交换后的链表的头结点,next节点的next指向head节点。
ListNode* swapPairs(ListNode* head) {if(!head || !head->next)return head;ListNode* next = head->next;head->next = swapPairs(next->next);next->next = head;return next;}
📁 25. K 个一组翻转链表
就是将 n / k 个组内的节点进行头插,然后将剩下的节点在链入链表中即可。
ListNode* reverseKGroup(ListNode* head, int k) {ListNode* newHead = new ListNode();ListNode* tail = newHead; //组内最后一个节点ListNode* cur = head;int n = 0;while(cur){++n , cur = cur->next;}n /= k;cur = head;for(int i = 0 ; i < n ; ++i){//每组第一个节点经过翻转后变成最后一节点 ListNode* tmp = cur;for(int j = 0 ; j < k ; ++j){ ListNode* next = cur->next;cur->next = tail->next;tail->next = cur;cur = next;}tail = tmp;}if(cur)tail->next = cur;return newHead->next;}
📁 138. 随机链表的复制
对于本题最苦难的地方就在于随机指针的指向问题了,因为random可能指向一个还没有创建的节点node,如果我们直接创建,并使得random指向node,那么node之前的节点的next指针怎么指向node,因此我们需要一个东西来记录下来。
我们可以使用unordered_map来记录原节点和新节点的对应关系,这样random指向一个没有创建的节点node时,node可以直接创建,并且node节点之前的节点prev的next指针可以通过hash找到node节点,并成功指向。
class Solution {
public:unordered_map<Node* , Node*> hash;Node* copyRandomList(Node* head) {if(head == nullptr)return head;if(!hash.count(head)){Node* newNode = new Node(head->val);hash[head] = newNode;newNode->next = copyRandomList(head->next);newNode->random = copyRandomList(head->random);}return hash[head];}
};
📁 148. 排序链表
最简单的方法就是节点排序,然后创建新的链表,按值大小从后往前链接节点。我们使用map记录值和节点直接的映射关系,因为底层是红黑树,即平衡二叉搜索树,所以遍历时是从小到大遍历的,multimap可以保证我们存在相同值。
ListNode* sortList(ListNode* head) {if(head == nullptr || head->next == nullptr)return head;multimap<int , ListNode*> hash;ListNode* cur = head;while(cur){ hash.insert(make_pair(cur->val , cur));cur = cur->next;}ListNode* newHead = new ListNode();ListNode* tail = newHead;for(auto& [key , node] : hash){tail->next = node;tail = tail->next;}tail->next = nullptr;return newHead->next;}
📁 23. 合并 K 个升序链表
对于链表,我可以将翻转看成将每个节点从后往前进行头插。那么本题就简单了,我们只需要头插节点即可。
"如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序",可知我们需要翻转组数目 = n / k。
此外,进行头插后,我们还需要记录下来每组第一个节点,每组进行反转后第一个节点变为了最后一个节点,它的next节点应该指向下一组翻转节点。
struct cmp{bool operator()(const ListNode* l1 , const ListNode* l2){return l1->val > l2->val;}};ListNode* mergeKLists(vector<ListNode*>& lists) {std::priority_queue<ListNode* , std::vector<ListNode*> , cmp> heap;for(ListNode* node : lists)if(node != nullptr)heap.push(node);ListNode* newHead = new ListNode();ListNode* tail = newHead;while(!heap.empty()){ListNode* node = heap.top(); heap.pop();tail->next = node;tail = tail->next;if(node->next != nullptr)heap.push(node->next);}return newHead->next; }
📁 146. LRU 缓存
本题就是将将新插入或刚被调用的元素插入到链表头部,当容量满时淘汰掉链表尾部元素,即最近最少被使用的元素。
为什么使用unorder_map,因为哈希系列查找速度是O(1),而链表的查找速度是O(N),为了提高效率我们使用哈希来查找存储 <key,val>元素的链表节点的iterator。
class LRUCache {
public:typedef list<pair<int,int>>::iterator it;LRUCache(int capacity) {_capacity = capacity;}//判断是否缓存//找到了 将其剪切到链表的头部位置int get(int key) {auto kv = _hash.find(key);if(kv != _hash.end()){//push过 已被缓存_list.splice(_list.begin() , _list , kv->second);return kv->second->second;}return -1;}//1. 首先查找LRU中是否缓存了//2. 如果存在, 更新val 并且放到链表的最前方//3. 如果不存在 在判断链表是否未满//i. 如果链表满了 尾删//ii. 其次 头插并记录映射void put(int key, int value) {auto kv = _hash.find(key);if(kv != _hash.end()){//更新kv->second->second = value;_list.splice(_list.begin() , _list , kv->second);}else{if(_list.size() == _capacity){//尾删一个元素_hash.erase(_list.back().first);_list.pop_back();}//头插新元素_list.emplace_front(key , value); _hash[key] = _list.begin();}}private:int _capacity = 0;unordered_map<int , it> _hash;list<pair<int , int>> _list;
};