目录
[十、环形链表 II](#十、环形链表 II)
常用技巧
1.画图 --- 直观,便于理解,代码不容易出错
2.引入虚拟头节点(带哨兵位的头节点) --- 便于处理边界情况,方便对链表操作
3.不要吝啬空间,大胆去定义变量
如上图所示,要将cur插入到两个节点之间,那么①与②的顺序就不能颠倒,但是如果定义了一个指针变量next,就完全不用考虑链接顺序了!
①②③④可以任意颠倒~
4.快慢双指针~
比如判环,找链表中环的入口,找链表倒数第n个节点
常用操作
1.创建一个新节点
2.尾插
3.头插
leetcode/牛客题目
一、移除链表元素
203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/description/
1.题目解析
删除链表中值为val的所有节点
2.算法分析
法一: 把所有值不等于val的节点进行尾插
法二: 在原链表进行删除操作
3.算法代码
法一: 把所有值不等于val的节点进行尾插
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val)
{
ListNode* newHead = new ListNode(); //虚拟头节点
ListNode* cur = head, *tail = newHead;
while(cur)
{
if(cur->val != val)
{
tail->next = cur;
tail = tail->next;
cur = cur->next;
}
else
{
ListNode* next = cur->next;
delete cur;
cur = next;
}
}
tail->next = nullptr;
return newHead->next;
}
};
法二: 在原链表进行删除操作
cpp
class Solution {
public:
ListNode* removeElements(ListNode* head, int val)
{
ListNode* newHead = new ListNode(); //虚拟头节点
newHead->next = head;
ListNode* cur = head, *prev = newHead; //prev用于记录当前节点的前一个节点
while(cur)
{
if(cur->val == val)
{
prev->next = cur->next;
delete cur;
cur = prev->next;
}
else
{
prev = cur;
cur = cur->next;
}
}
return newHead->next;
}
};
二、反转链表
206. 反转链表 - 力扣(LeetCode)https://leetcode.cn/problems/reverse-linked-list/1.题目解析
反转链表
2.算法分析
法一:头插法
法二:三指针反转链表
3.算法代码
法一:头插法
cpp
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
ListNode* newHead = new ListNode();
ListNode* cur = head;
while(cur)
{
ListNode* next = cur->next;
cur->next = newHead->next;
newHead->next = cur;
cur = next;
}
ListNode* ret = newHead->next;
delete newHead;
return ret;
}
};
法二:三指针反转链表
cpp
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
if(head == nullptr) return head;
ListNode* n1 = nullptr;
ListNode* n2 = head;
ListNode* n3 = head->next;
while(n2)
{
//1.改变指针指向
n2->next = n1;
//2.循环迭代
n1 = n2;
n2 = n3;
if(n3)
n3 = n3->next;
}
return n1;
}
};
三、链表的中间结点
876. 链表的中间结点 - 力扣(LeetCode)https://leetcode.cn/problems/middle-of-the-linked-list/1.题目解析
返回链表的中间节点,如果有两个中间节点,返回第二个中间节点
2.算法分析
这类题目是典型的双指针解法,定义快慢指针,快指针每次走两步,慢指针每次走一步,当快指针走到空或者走到最后一个节点,慢指针指向的就是中间节点
3.算法代码
cpp
class Solution {
public:
ListNode* middleNode(ListNode* head)
{
ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
};
四、返回倒数第k个节点
面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/1.题目解析
返回链表倒数第k个节点中的值
2.算法分析
法一:先遍历一遍链表,求出节点总个数n, 再从头遍历, 循环n-k次,求出倒数第k个节点的值
法二:快慢双指针策略
fast和slow指针开始都在起始位置,fast先走k步,然后slow和fast再一起走,每次都走一步,当fast走到空时,slow的位置就是倒数第k个节点
3.算法代码
法一:
cpp
class Solution {
public:
int kthToLast(ListNode* head, int k)
{
ListNode* cur = head;
int n = 0;
while(cur)
{
cur = cur->next;
n++;
}
cur = head;
for(int i = 0; i < n-k; i++)
{
cur = cur->next;
}
return cur->val;
}
};
法二:
cpp
class Solution {
public:
int kthToLast(ListNode* head, int k)
{
ListNode* slow = head, *fast = head;
while(k--)
fast = fast->next;
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow->val;
}
};
五、合并两个有序链表
21. 合并两个有序链表 - 力扣(LeetCode)https://leetcode.cn/problems/merge-two-sorted-lists/description/1.题目解析
将两个升序链表合并成1个升序的链表
2.算法分析
本题和合并两个有序数组非常相似,采用的方法都是遍历两个链表(数组), 每次取小的进行尾插
3.算法代码
cpp
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
{
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
ListNode* newHead = new ListNode(), *tail = newHead;
ListNode* cur1 = list1, *cur2 = list2;
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;
}
}
if(cur1) tail->next = cur1;
if(cur2) tail->next = cur2;
ListNode* ret = newHead->next;
delete newHead;
return ret;
}
};
六、链表分割
给定一个值x, 将所有值小于x的节点排在其余节点之前
2.算法分析
我们只需要遍历一遍原始链表,将值小于x的节点尾插到一个新的链表中,将值>=x的节点尾插到另一个新的链表中,然后将前一个新的链表和后一个新的链表链接起来即可,但是为了方便处理,我们仍然给两个新的链表都添加了虚拟头节点
3.算法代码
cpp
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
ListNode* greaterHead = new ListNode(-1), *greatertail = greaterHead;
ListNode* lessHead = new ListNode(-1), *lesstail = lessHead;
ListNode* cur = pHead;
while(cur)
{
if(cur->val < x)
{
lesstail->next = cur;
lesstail = lesstail->next;
cur = cur->next;
}
else
{
greatertail->next = cur;
greatertail = greatertail->next;
cur = cur->next;
}
}
lesstail->next = greaterHead->next;
greatertail->next = nullptr;
return lessHead->next;
}
};
七、链表的回文结构
判断链表是否是回文链表
2.算法分析
本题可以拆解成以下几个问题
1.求链表的中间节点
2.翻转中间节点以后的部分
3.判断前半部分链表和翻转后的后半部分链表是否相等
3.算法代码
cpp
class PalindromeList {
public:
bool chkPalindrome(ListNode* A)
{
//1.找链表的中间节点
ListNode* fast = A, *slow = A;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
//2.翻转后半部分
ListNode* newHead = new ListNode(-1);
ListNode* cur = slow;
while(cur)
{
ListNode* next = cur->next;
cur->next = newHead->next;
newHead->next = cur;
cur = next;
}
//3.判断两个链表是否相等
ListNode* B = newHead->next;
while(B)
{
if(A->val != B->val)
return false;
A = A->next;
B = B->next;
}
return true;
}
};
八、相交链表
160. 相交链表 - 力扣(LeetCode)https://leetcode.cn/problems/intersection-of-two-linked-lists/1.题目解析
求两个相交链表的相交的起始节点
2.算法分析
解法一:
1.分别遍历两个链表,求出两个链表的长度(节点的个数), 并求出长度差 gap
2.定义快慢指针,让fast指针指向较长链表的第一个节点,先走gap步
3.快慢指针同时向后走,当快慢指针相遇时,就是相交链表的起始节点
解法二:
3.算法代码
解法一:
cpp
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode* cur1 = headA, *cur2 = headB;
int lenA = 0, lenB = 0;
while(cur1)
{
cur1 = cur1->next;
lenA++;
}
while(cur2)
{
cur2 = cur2->next;
lenB++;
}
ListNode* fast = headA, *slow = headB;
if(lenA < lenB)
{
fast = headB;
slow = headA;
}
int gap = abs(lenA - lenB);
while(gap--)
fast = fast->next;
while(slow != fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
解法二:
cpp
class Solution {
public:
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;
}
};
九、环形链表
141. 环形链表 - 力扣(LeetCode)https://leetcode.cn/problems/linked-list-cycle/1.题目解析
判断链表中是否有环
2.算法分析
定义快慢指针,快指针每次走两步,慢指针每次走一步,如果快慢指针可以相遇,说明链表中有环;如果fast指针走到了空/fast的下一个节点就是空,说明链表中不带环
简单证明: 如果有环,fast必然先进入环,slow后进入环,所以当slow即将入环时,假设相距x, 由于fast每次走两步,slow每次走一步,所以fast和slow之间的距离每次缩小1,因此最后距离总会减小到0,也就是相遇!
拓展: fast每次走3步是不可以的,因为如果fast每次走3步,slow走1步,距离每次缩小2,如果x是偶数,那么x会减小到0,slow和fast相遇,但是如果x是奇数,那么距离会减小到1, -1 ...., 就会错过距离0, 因此本轮slow和fast没有相遇; 下一轮slow和fast相距N-1( N代表环的长度 ), 如果N是奇数,那么N-1是偶数,则本轮可以相遇;若N是偶数,N-1是奇数,则fast和slow永远都相遇不了!
3 .算法代码
cpp
class Solution {
public:
bool hasCycle(ListNode *head)
{
ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow) return true;
}
return false;
}
};
十、环形链表 II
142.环形链表 IIhttps://leetcode.cn/problems/linked-list-cycle-ii/description/1.题目解析
如果链表不带环,返回nullptr, 如果链表带环,返回环形链表的第一个入环节点的指针
2.算法分析
如何判断链表是否带环,题目九已经讲解过了,因此我们接下来的关键就是求入环节点
法一: 快慢指针
如果有环, slow和fast必在环内相遇,此时再定义一个指针cur2, cur2从fast和slow相遇位置开始走,与此同时,cur1(原先链表的head)从头开始走,两个指针每次都走一步,当cur1和cur2相遇时,相遇的地方就是入环的第一个节点
简单证明:
法二: 转化成题目八 ---> 相交链表
如果有环, slow和fast必在环内相遇,此时可以将环断开,形成两个链表,此时问题就转化成了相交链表的问题了~
3.算法代码
法一:
cpp
class Solution {
public:
ListNode *detectCycle(ListNode *head)
{
ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast) //一定有环
{
ListNode* cur1 = head, *cur2 = fast;
while(cur1 != cur2)
{
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}
}
return nullptr;
}
};
法二:
cpp
class Solution {
public:
//求相交链表的第一个交点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode* cur1 = headA, *cur2 = headB;
int lenA = 0, lenB = 0;
while(cur1)
{
cur1 = cur1->next;
lenA++;
}
while(cur2)
{
cur2 = cur2->next;
lenB++;
}
ListNode* fast = headA, *slow = headB;
if(lenA < lenB)
{
fast = headB;
slow = headA;
}
int gap = abs(lenA - lenB);
while(gap--)
fast = fast->next;
while(slow != fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
ListNode *detectCycle(ListNode *head)
{
ListNode* fast = head, *slow = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast) //一定有环
{
ListNode* head2 = fast->next;
fast->next = nullptr; //断开,形成两个链表
return getIntersectionNode(head, head2);
}
}
return nullptr;
}
};
十一、随机链表的复制
138. 随机链表的复制 - 力扣(LeetCode)https://leetcode.cn/problems/copy-list-with-random-pointer/description/
1.题目解析
题目给定一个链表,链表中除了有指向下一个节点的指针之外,还有指向链表中随机一个节点的指针random, 题目要求深拷贝原链表,并返回
2.算法分析
1.将所有的新节点链接到原节点后面
2.控制所有复制节点中的random指向
3.解开新链表和原链表绑定,恢复原链表
3.算法代码
cpp
class Solution {
public:
Node* copyRandomList(Node* head)
{
//1.将所有的新节点链接到原节点后面
Node* cur = head;
while(cur)
{
Node* next = cur->next; //先保存下一个节点
Node* copy = new Node(cur->val); //开辟复制节点
cur->next = copy; //将复制节点链接到原链表对应节点后面
copy->next = next; //将下一个节点链接到复制节点的后面
cur = next; //更新cur到下一个节点
}
//2.控制所有复制节点中的random指向
cur = head;
while(cur)
{
Node* copy = cur->next; //找到复制节点
if(cur->random == nullptr) //原节点的random为空,则复制节点的random为空
{
copy->random = nullptr;
}
else //原节点的random不为空,则复制节点的random指向cur->random->next
{
copy->random = cur->random->next;
}
cur = copy->next; //更新cur指针
}
//3.解开原链表和新链表的绑定,恢复原链表
cur = head;
Node* copyHead = NULL, *copyTail = NULL; //copyHead记录复制链表的开始, copyTail记录复制链表的结尾
while(cur)
{
Node* copy = cur->next; //记录复制节点
Node* next = copy->next; //记录原链表下一个节点
if(copyTail == nullptr) //开始时copyTail为空
{
copyHead = copyTail = copy;
}
else
{
copyTail->next = copy; //链接复制链表两个相邻节点
copyTail = copyTail->next; //更新copyTail指针
}
cur->next = next; //链接原链表的相邻两个节点
cur = next; //更新cur指针
}
return copyHead;
}
};
十二、两数相加
2. 两数相加 - 力扣(LeetCode)https://leetcode.cn/problems/add-two-numbers/description/1.题目解析
两个单链表,每个链表存储了一个整数(逆序存储), 返回两数相加之后的结果(依旧是链表)
2.算法分析
题目中的逆序存储是为了方便我们操作,我们只需要从头开始向后遍历链表相加即可
解法就是模拟两数相加即可
3.算法代码
cpp
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
ListNode* head = new ListNode(); //创建虚拟头节点
ListNode* cur1 = l1, *cur2 = l2, *tail = head;
int t = 0; //记录当前位置相加结果
while(cur1 || cur2 || t) //有可能当cur1和cur2都为空时,t中还保留了一个进位
{
if(cur1)
{
t += cur1->val;
cur1 = cur1->next;
}
if(cur2)
{
t += cur2->val;
cur2 = cur2->next;
}
//链接新节点
tail->next = new ListNode(t % 10);
tail = tail->next;
t /= 10; //进位情况
}
//释放虚拟头节点
tail = head->next;
delete head;
return tail;
}
};
十三、两两交换链表中的节点
24. 两两交换链表中的节点 - 力扣(LeetCode)https://leetcode.cn/problems/swap-nodes-in-pairs/description/1.题目解析
两两交换链表中的节点, 但是不能直接交换节点中的值, 而是要改变指针指向
2.算法分析
2.1增加虚拟头节点,统一了交换1,2节点和交换后面两两节点的操作
2.2采用循环的方式完成整个过程
2.3 循环结束的条件是 cur为空或者 next为空
2.2所给图示是next为空就结束了,但是还有可能是cur为空结束~
3.算法代码
cpp
class Solution {
public:
ListNode* swapPairs(ListNode* head)
{
if(head == nullptr || head->next == nullptr) return head;
ListNode* newHead = new ListNode();
newHead->next = head;
ListNode* prev = newHead, *cur = head, *next = head->next, *nnext = next->next;
while(cur && next)
{
//1.交换节点
prev->next = next;
next->next = cur;
cur->next = nnext;
//2.修改指针(注意顺序)
prev = cur;
cur = nnext;
if(cur) next = cur->next;
if(next) nnext = next->next;
}
cur = newHead->next;
delete newHead;
return cur;
}
};
十四、重排链表
143. 重排链表 - 力扣(LeetCode)https://leetcode.cn/problems/reorder-list/1.题目解析
按题目要求将链表节点重新排列顺序
2.算法分析
这题目可以拆成以下3个子问题~
1.找到链表的中间节点 --- 双指针
2.把后面的部分逆序 --- 头插法
3.合并两个链表 --- 双指针
注意:找到中间节点之后,我们可以逆序连同中间节点之后的部分,也可以逆序不包含中间节点之后的部分
但是我们逆序完之后需要形成两个链表,如果采用头插法逆序包含中间节点及以后的部分,那么链表就无法断开,因为我们没有办法得到mid的前一个节点地址prev,从而无法将prev的next置成空指针,而如果采用头插法逆序中间节点以后的部分, 断开链表只需要将mid->next置成空指针即可
如果一定要逆序包含mid及以后的部分,可以添加虚拟头节点,此时slow的落点就是上面的prev了!
3.算法代码
cpp
class Solution {
public:
void reorderList(ListNode* head)
{
//特殊处理,不需要重排
if(head == nullptr || head->next == nullptr || head->next->next == nullptr) return;
//1.找到链表的中间节点
ListNode* slow = head, *fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//2.把slow后面的部分逆序
ListNode* head2 = new ListNode();
ListNode* cur = slow->next;
slow->next = nullptr; //断开链表
while(cur)
{
ListNode* next = cur->next;
cur->next = head2->next;
head2->next = cur;
cur = next;
}
//3.合并两个链表
ListNode* ret = new ListNode(), *tail = ret;
ListNode* cur1 = head, *cur2 = head2->next;
while(cur1) //由于第一个链表一定比第二个链表长, 因此判断cur1即可
{
//尾插第一个链表
tail->next = cur1;
cur1 = cur1->next;
tail = tail->next;
//尾插第二个链表
if(cur2)
{
tail->next = cur2;
cur2 = cur2->next;
tail = tail->next;
}
}
}
};
十五、合并K个升序链表
23. 合并 K 个升序链表 - 力扣(LeetCode)https://leetcode.cn/problems/merge-k-sorted-lists/description/1.题目解析
将所有的升序链表合并成一个升序链表,并返回
2.算法分析
法一:暴力解法:链表两两合并,直至所有链表合并完成, 时间复杂度: 假设每个链表长度为n, 则时间复杂度为 O(n*(k-1) + n*(k-2) + ... +) = O(n*k^2)
法二:利用优先级队列优化暴力枚举策略
我们可以创建一个小堆,开始时将所有的链表头节点指针都放进去,然后创建一个新的虚拟头节点,出堆顶元素尾插,然后让堆顶元素指针的下一个节点指针进入堆,依次类推, 时间复杂度为O(n*k*logk)
法三:分治-递归
从中间劈开,如果左右部分已经合并完成,那么最后只需要合并左右两个链表即可,而左右链表如何合并的呢???和原问题是相同的,依旧从中间劈开,让左右部分合并完成即可
3.算法代码
法二:优先级队列
cpp
class Solution
{
public:
//仿函数
struct cmp
{
bool operator()(const ListNode* l1, const ListNode* l2)
{
return l1->val > l2->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists)
{
//创建小根堆
priority_queue<ListNode*, vector<ListNode*>, cmp> heap; //小堆
for(auto& l : lists)
if(l) heap.push(l);
//合并所有链表
ListNode* newHead = new ListNode(), *tail = newHead;
while(!heap.empty())
{
ListNode* top = heap.top();
heap.pop();
tail->next = top;
tail = tail->next;
if(top->next) heap.push(top->next);
}
return newHead->next;
}
};
法三:分治-递归
cpp
class Solution
{
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
return merge_sort(lists, 0, lists.size()-1);
}
ListNode* merge_sort(vector<ListNode*>& lists, int left, int right)
{
if(left > right) return nullptr;
if(left == right) return lists[left];
//1.求中间节点
int mid = (left + right) >> 1;
//2.递归左右区间
ListNode* l1 = merge_sort(lists, left, mid);
ListNode* l2 = merge_sort(lists, mid+1, right);
//3.合并两个有序链表
return merge_two_lists(l1, l2);
}
ListNode* merge_two_lists(ListNode* l1, ListNode* l2)
{
if(l1 == nullptr) return l2;
if(l2 == nullptr) return l1;
ListNode* cur1 = l1, *cur2 = l2;
ListNode* newHead = new 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;
}
}
if(cur1) tail->next = cur1;
if(cur2) tail->next = cur2;
return newHead->next;
}
};
十六、K个一组翻转链表
25. K 个一组翻转链表 - 力扣(LeetCode)https://leetcode.cn/problems/reverse-nodes-in-k-group/1.题目解析
K个一组翻转链表,最后不够K个节点,就不用再翻转了~
2.算法分析
1.求出需要翻转多少组:n ---> 链表节点总个数 / k 即可
2.重复n次, 长度为k的链表的翻转即可
3.算法代码
cpp
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
//1.求需要翻转多少组
int n = 0;
ListNode* cur = head;
while(cur)
{
cur = cur->next;
n++;
}
n /= k;
//2.重复n次,长度为k的链表的翻转
ListNode* newHead = new ListNode(), *prev = newHead;
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 = prev->next;
prev->next = cur;
cur = next;
}
prev = tmp;
}
//把不需要翻转的节点接上
prev->next = cur;
return newHead->next;
}
};