25. K组一个翻转链表
题目描述
给你一个链表,每 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]
示例3:
输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]
翻转链表的模板
ListNode* reverseList(ListNode* head) {
ListNode* now = NULL;
ListNode* next = NULL;
while(head!=NULL)
{
next = head->next;
head->next = now;
now = head;
head = next;
}
return now;
}
值得记录的是,链表相关的题目中常用的一个技巧是设置一个空的表头,就像上面代码中的now,设置一个这样的表头能够免去一些繁琐的边界判断。
迭代法求解
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *now, *prev, *last, *tmp;
int cnt = 0;
ListNode* dummy = new ListNode;
dummy -> next = head;
now = last = dummy;
while(now) {
++cnt;
now = now -> next;
if(cnt == k && now) {
prev = last -> next;
while(--cnt) {
tmp = last -> next;
last -> next = tmp -> next;
tmp -> next = now -> next;
now -> next = tmp;
}
now = last = prev;
}
}
return dummy -> next;
}
同样,我们需要设置一个头结点以免去边界判断,在代码中体现为dummy。由于涉及到每K个翻转以及每一段的拼接问题,我们不像模板中那样处理新建的头结点,而保持dummy指针不动,作为最后返回答案的一个标记位置。
同时,用now标记每一节链表翻转后的头(即翻转前的尾,其实上面的last和now互换后更容易理解,这个看个人习惯),last标记上一轮翻转后的尾结点(包括dummy),prev提前记录这一轮翻转后的尾结点(可看做是下一轮的last)。
迭代法翻转的核心思路是 —— last(在链表中的)位置不动,以now为基点,依次将各个结点插入到now的后面。
依次翻转图示如下:
由于每一次翻转while的判断条件是--cnt,因此每一次只需操作k-1次。
容易得到k-1次操作过程中,图中翻转区结点值的变化:1->2->3->4 —— 2->3->4->1 —— 3->4->2->1 —— 4->3->2->1
空间复杂度:用到5个辅助指针,空间复杂度为O(1)
时间复杂度:仅需观察now结点和小while循环次数。now结点经历了完整的一次遍历链表,而while循环共进行(k-1)*(n/k) ≈ n次。因此相当于遍历了2n次,空间复杂度为O(n)。
递归法求解
ListNode* reverseKGroup(ListNode* head, int k) {
if(head == NULL || head->next == NULL || k==1)return head;
ListNode* tail = head;
int x = k;
while(x) {
tail = tail -> next;
--x;
if(!tail && x>0) return head;
}
ListNode* prev = head, *now = head -> next;
ListNode *tmp = NULL;
while(now && now != tail) {
tmp = now -> next;
now -> next = prev;
prev = now;
now = tmp;
}
head -> next = reverseKGroup(tail, k);
return prev;
}
递归思路即只关心K个结点的翻转,而不关心超过K个的其他结点。在每次翻转前,先判断结点数是否够K个,若足够,则进行一次翻转,若不足K个(当前结点tial为空结点,但计数未到K——!tail&&x>0),则不需翻转,直接返回头结点。
真正做翻转操作时与翻转模板差异不大,知识尾结点变为了tail而不是NULL。
递归调用时,直接令head->next = 下一轮递归结果,而返回prev(翻转后头结点)。
可以看到,递归法省去手动拼接过程,思路较为清晰。
空间复杂度:由于递归使用了栈,大概进行了n/k次递归,每次递归O(1)复杂度,空间复杂度可表示为O(n/k)
时间复杂度:与迭代法相同,进行了至多两轮遍历,复杂度为O(n)
返回倒数第K个结点
题目描述
题目链接:https://leetcode-cn.com/probl...
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
示例:
输入: 1->2->3->4->5 和 k = 2
输出: 4
快慢指针法
链表经典题,快慢指针,只需一次遍历。时间复杂度O(n),空间复杂度O(1)
int kthToLast(ListNode* head, int k) {
ListNode* fast = head, *low = head;
while(k--) {
fast = fast -> next;
}
while(fast) {
fast = fast -> next;
low = low -> next;
}
return low -> val;
}
160. 相交链表
题目描述
题目链接:https://leetcode-cn.com/probl...
编写一个程序,找到两个单链表相交的起始节点。
示例:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
注:skipA和skipB仅做理解用,并不作为接口的参数
快慢指针
思路:由题目输入所给的提示可以看出,如果两个链表有相同的结点,那么在公共结点之前,两个链表分别有自己的子链表,而这两条子链表的长度差正好是两条链表的长度差(因为从公共结点开始两个链表都是相同的)。知道了这一点,就有了一个思路——假设两个链表长度差为diff,那么让指向较长链表的指针先走diff步,然后两个指针同时走,再消除了长度差后,两个指针必然同时走到公共结点(如果有的话)。
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB) return NULL;
int len1 = 0, len2 = 0;
ListNode *node1 = headA, *node2 = headB;
while(node1) {
node1 = node1 -> next;
++len1;
}
while(node2) {
node2 = node2 -> next;
++len2;
}
int diff = len1 - len2;
node1 = headA; node2 = headB;
if(diff > 0) {
while(diff--) {
node1 = node1 -> next;
}
}
else {
while(diff++ < 0) {
node2 = node2 -> next;
}
}
while(node1 != node2) {
node1 = node1 -> next;
node2 = node2 -> next;
}
return node1;
}
时间复杂度:显然,两个链表都只遍历了两次,时间复杂度为O(m+n)
空间复杂度:O(1)
巧解法
思路:巧解法思路的精髓在于,若两个链表长度为a,b,那么a+b=b+a。这就意味着,让两个链表的末尾都指向对方的开头,形成的两条新链表的长度是一致的。并且,如果两个链表有公共结点,那么形成的两条长链表的结尾必然有公共部分(因为两个原始链表本来就有公共部分,只是原来两个链表的长度可能不同,不好比较,但两条长链表)。画出两个长链表一目了然。基于这个思路,可以得到如下简洁代码:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB) return NULL;
ListNode *node1 = headA, *node2 = headB;
while(node1 != node2) {
node1 = node1 ? node1 -> next : headB;
node2 = node2 ? node2 -> next : headA;
}
return node1;
}
找到链表环入口
题目描述
题目链接:https://leetcode-cn.com/probl...
给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
剑指offer经典题,快慢指针。
快慢指针法
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr)
{
slow = slow->next;
// 无环的情况
if (fast->next == nullptr)
{
return nullptr;
}
fast = fast->next->next;
// 找到相交点
if (fast == slow)
{
// 作为新的起点继续走
ListNode* curr = head;
while (curr != fast)
{
fast = fast->next;
curr = curr->next;
}
return curr;
}
}
return nullptr;
}
时间复杂度O(n), 空间复杂度O(1)。
附快慢指针为什么能成功的数学证明:https://leetcode-cn.com/probl...
130. 复制带随机指针的链表
题目描述
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
朴素解法
链表的复制并不复杂,麻烦在random的处理,random指向的随机性,没有办法一次性解决复制(因为新链表中,前面指针的random指向的结点可能还没生成)。
朴素解法的思路:
- 先做一遍复制,对于val以及next做深拷贝,而对于random只做浅拷贝
- 在复制的同时,用一个map记录新节点和源节点的对应关系。
做第二遍遍历,根据map中的对应关系,替换random指向的结点。
class Solution { public: Node* copyRandomList(Node* head) { if(!head) return head; unordered_map<Node*, Node*> mp; Node* newhead = new Node(head->val); newhead->random = head->random; Node *last = newhead; mp[head] = newhead; while(head) { head = head->next; if(!head) last->next = nullptr; else { Node* newNode = new Node(head->val); newNode->random = head->random; last->next = newNode; last = last->next; mp[head] = newNode; } } last = newhead; while(last) { last->random = mp[last->random]; last = last->next; } return newhead; } };
递归法
递归法思路简洁,对于一个结点的拷贝,直接拷贝val,然后递归地创建next结点和random结点。如果next结点和random结点已存在,则直接返回,如果不存在,则继续进行递归创建。为了记录新结点的创建情况,需要用一个map记录。
PS:用递归法,结点的拷贝是相互独立且不按序的,一般来说,最先被创建的会是最后一个结点class Solution { public: unordered_map<Node*, Node*> mp; Node* copyRandomList(Node* head) { if(!head) return nullptr; if(!mp.count(head)) { Node* node = new Node(head->val); mp[head] = node; node->next = copyRandomList(head->next); node->random = copyRandomList(head->random); } return mp[head]; } };
结点拆分法
前两种方法在时间复杂度上都达到了O(n)标准,但在空间上都有瓶颈。两种方法都用一个map去记录新旧结点的对应关系,但实际上这个关系并不复杂,而是非常直接的对应。因此,可以用一个技巧去解决这个对应关系——即直接将新的拷贝结点放在源节点之后,形成一条2n长的链表,然后处理random和next指针。
- 第一次循环,建立拷贝结点,放在旧结点之后
- 处理新节点的random指针,此时只需根据旧结点的random做一次偏移即可
- 分离新旧结点,处理next指针
class Solution {
public:
Node* copyRandomList(Node* head) {
if(!head) return head;
Node* node = head;
while(node) {
Node* newNode = new Node(node->val);
newNode->next = node->next;
node->next = newNode;
node = node->next->next;
}
node = head;
while(node) {
if(!node->random) node->next->random = nullptr;
else node->next->random = node->random->next;
node = node->next->next;
}
Node* newhead = head->next;
node = head;
while(node) {
Node* tmp = node->next;
node->next = node->next->next;
if(!tmp->next) tmp->next = nullptr;
else tmp->next = tmp->next->next;
node = node->next;
}
return newhead;
}
};
148.排序链表
题目描述
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表。
思路
快排、堆排没法用,想要时间复杂度最低,只能用归并。
链表的归并细节很多,主要有:
- 每一次分组的所使用sublen,应从1开始,每一轮归并后乘2,直到sublen >= 链表长度
- 对于每一轮每一组链表,都应拆分成两个子链表,用两个表头head1和head2表示
- 对于每一轮每一组链表,它的长度很可能小于sublen,甚至小于sublen/2,则head2很可能为nullptr,甚至head1表示的链表也可能只有很短的长度。因此在用sublen取子链表时,对head1要判断now->next是否为nullptr,对head2要判断now和now->next是否为nullptr
- 设置dummy使得代码更简洁,但在返回前需要释放,否则会内存泄漏
代码
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head || !head->next) return head;
int len = 0;
ListNode *now = head;
while(now) {
++len;
now = now->next;
}
ListNode *dummy = new ListNode(0, head);
ListNode *prev = dummy;
for(int sublen = 1; sublen < len; sublen *= 2) {
prev = dummy;
now = dummy->next;
while(now) {
ListNode *head1 = now;
for(int i=0; i<sublen-1 && now->next; ++i) {
now = now->next;
}
ListNode *head2 = now->next;
now->next = nullptr;
now = head2;
for(int i=0; i<sublen-1 && now && now->next; ++i) {
now = now->next;
}
ListNode *next = now ? now->next : nullptr;
if(now) now->next = nullptr;
prev->next = merge(head1, head2);
while(prev->next) {
prev = prev->next;
}
now = next;
}
}
head = dummy->next;
delete dummy;
return head;
}
ListNode* merge(ListNode* head1, ListNode* head2) {
if(!head1) return head2;
if(!head2) return head1;
ListNode* dummy = new ListNode;
ListNode* now = dummy;
while(head1 && head2) {
if(head1->val < head2->val) {
now->next = head1;
head1 = head1->next;
}
else {
now->next = head2;
head2 = head2->next;
}
now = now->next;
}
if(head1) now->next = head1;
if(head2) now->next = head2;
head1 = dummy->next;
delete dummy;
return head1;
}
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。