总结
链表相关的题目整体上难度不大
主要是对指针的指向修改容易出错
- 赋值顺序错误导致丢失了某个节点之后的所有结点
- 访问了空指针
- 计数次数与预期相差1
链表的特性
无法随机访问,只能从头开始单向前进
初始时链表的长度未知
(单链表)从某个节点无法获取其前一个节点
双指针
使用多个指针同时前进,实现到达链表中的某个偏移位置
快慢指针:以不同的速度前进,快指针每次前进两个结点,慢指针每次前进一个结点,快指针到末尾时慢指针到达链表中点
以一定的间隔同时前进,A指针先前进k步,之后AB指针同时前进,A指针到末尾时,B指针在倒数第k个节点
prev指针
对与需要删除或移动某个节点的情况,需要修改该节点前一个结点的next指针
如果循环时迭代的变量视为当前节点,则需要额外维护一个prev变量
当然,也可以直接将循环时迭代的变量视为前一个节点,使用该节点->next->val,这样可以省去prev变量
dummy结点
通常以一个不存储实际数值的哑结点作为头结点,以统一操作,避免在循环中进行额外的判断(是否为head)
如果使用上面的prev变量,初始时prev就指向dummy
注意点
对循环边界条件的判断
- 链表是否结束,注意判断是用
p!=nullptr
还是p->next!=nullptr
- 前进指定的次数
对于需要计数的情况
尽可能不要在循环的判断条件中对计数变量使用
++
和--
后缀运算符,特别是与结点指针p != nullptr
的判断进行逻辑与&&
运算时因为后缀运算的副作用是否执行与执行位置不明显,带来思维上的负担,容易出现边界情况计算错误
好的做法应该是在循环体中修改计数变量(同时修改结点指针)
对于指针指向的修改
- 需要额外的变量暂存某个指针
- 以一定的顺序赋值,避免错误覆盖而丢失指针
- 每次循环结束后某个指针变量的指向是明确的(“语义”不变的)
关于递归实现
- 递归虽然更简洁,但边界条件也容易出错,并且栈开销较大,尽量不使用
具体题目
简单题
2. 两数相加
以链表存储的两个数,低位在前
直接相加即可,注意最后的进位
445. 两数相加 II
以链表存储的两个数,高位在前
先入栈或先翻转链表
19. 删除链表的倒数第 N 个结点
经典双指针,注意删除的有可能是首结点,需要dummy
A从head先前进N,然后B从dummy出发与A一起前进,A==null时,B为倒数第N的前一个结点
21. 合并两个有序链表
简单归并,注意最后接上未遍历完的那个链表
83. 删除排序链表中的重复元素
题意:删除已排序链表中所有重复数字的节点,使每个元素只出现一次
对每个结点,不断删除相同值的下一个结点
82. 删除排序链表中的重复元素 II
题意:删除已排序链表中所有重复数字的节点,只留下原先只出现一次的数字
这题有可能删除头结点,需要dummy
设置prev和curr,对每个结点curr,不断删除相同值的下一个结点,最后删除curr
141. 环形链表
快慢指针判断环,注意循环的边界条件
如果fast
遇到nullptr
,说明没有环
203. 移除链表元素
判断下一个元素的值即可,若相等,删除下一个元素
206. 反转链表
经典方法,前中后三个指针遍历
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* next = nullptr;
while (curr) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
还有一种奇技淫巧,
ListNode *p;
for (p = nullptr; head; )
tie(p, head, head->next) = tuple<ListNode*, ListNode*, ListNode*>(head, head->next, p);
return p;
237. 删除链表中的节点
比较有意思的题目,由于不知道当前节点的前一个结点,无法直接删除,我们可以把下一个结点(必须存在)的值放到当前节点存储,然后删除下一个结点
725. 分隔链表
题意:把链表按顺序平均切成k份,任意两部分的长度差距不能超过 1,排在前面的部分的长度应该大于或等于排在后面的长度。
先计算长度n,求每份基本长度n/k,n%k余数r,前r段每段长度n/k+1,剩下的每段长度为n/k,对两种长度分别循环切分
876. 链表的中间结点
经典的快慢指针
1290. 二进制链表转整数
遍历,移位,累加
1669. 合并两个链表
题意:给你两个链表,它们包含的元素分别为 n
个和 m
个。请你将 list1
中下标从 a
到 b
的全部节点都删除,并将list2
接在被删除节点的位置。
注意前进次数,前进a-1次获得删除区间的前一个结点,再前进b+1次,获得删除区间的后一个结点
1721. 交换链表中的节点
题意:交换 链表正数第 k
个节点和倒数第 k
个节点的值
双指针,A先前进k-1次,记录A的位置获得第k
A+1(注意这里),之后AB一起,A==null时,B就是倒数k
2058. 找出临界点之间的最小和最大距离
题意:找出链表的两个严格极(大或小)值之间的最大和最小距离
前中后三个指针,依次找到所有极值的位置,更新最小间距
2095. 删除链表的中间节点
快慢指针找中点的前一个结点,注意输入和循环的边界条件
或者多用一个pre存放上一次的慢指针,比较清晰不会混乱
中等题
24. 两两交换链表中的节点
递归方法:简单
迭代方法:
交换之前的节点关系是 temp -> node1 -> node2
,交换之后的节点关系要变成 temp -> node2 -> node1
,因此需要进行如下操作。
temp.next = node2
node1.next = node2.next
node2.next = node1
完成上述操作之后,节点关系即变成 temp -> node2 -> node1
。再令 temp = node1
,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
61. 旋转链表
题意:将链表每个节点向右移动 k
_ _个位置
先求长度n,k=k%n,k==0时无需处理
思路:将链表连接成环,然后在指定位置断开
具体:尾部下一个节点连到头成环,然后找到新链表的最后一个节点(即原链表的第n - 1 - k个节点),将当前闭合为环的链表断开
86. 分隔链表
题意:给一个特定值x,调整链表使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前,应当 保留 两个分区中每个节点的初始相对位置。
效果相当于快排里的划分函数。
但链表不能从后往前迭代。因此,开一个dummy存放遍历时遇到的所有>=x的结点,原head的链表保留<x的结点,最后链接两个链表
92. 反转链表 II
题意:反转链表中的一个区间[left,right],下标从1开始
注意设置dummy,因为left可能是1,这样我们总是可以从dummy先前进left-1次获得left的前一个结点p,然后常规翻转right-left+1次
最后设置原p->next的next为翻转后剩下结点的首节点,p->next设为翻转后的首节点
142. 环形链表 II
题意:找出环形链表的入口点
快慢指针,直到快为null(说明无环,返回)或快==慢
此时slow走n步,fast走2n步,fast比slow多走一圈,则圈长为n
设不在圈中的部分长度为k,则slow在圈中已走n-k步,说明再走k步就会回到入口点(进圈的第一个结点)
让fast回head,与slow同速度前进,直到相等,则此时slow走n+k步,比fast多1圈,则fast走了k步,两者都在入口点
143. 重排链表
147. 对链表进行插入排序
设置一个指针p指向已排序的末尾节点,遍历链表,若当前节点大于末尾,则p++,否则取出当前节点从头开始寻找插入点
160. 相交链表
题意:两个链表可能在某个结点相交(之后的结点是一样的)。返回相交的其实结点,不存在返回Null
简单的方法:先算长度,然后重新遍历,让长的链表先走长度差,之后同时遍历两个链表,判断节点是否一样
巧妙的方法:同时遍历两个链表,各自到末尾时开始遍历另一个链表,直到二者相等(包括都为null的情况,说明不相交)
234. 回文链表
题意:判断链表的数字是否回文
方法一:快慢指针 + 反转链表,不额外使用存储的方法。先快慢指针找到中点,将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后将链表恢复原样。
方法二:快慢指针找中点,中点后的结点存入一个栈中
方法三:巧妙的递归方法,需要使用全局变量
ListNode* frontPointer;
bool recursivelyCheck(ListNode* currentNode) {
if (currentNode != nullptr) {
if (!recursivelyCheck(currentNode->next)) {
return false;
}
if (currentNode->val != frontPointer->val) {
return false;
}
frontPointer = frontPointer->next;
}
return true;
}
bool isPalindrome(ListNode* head) {
frontPointer = head;
return recursivelyCheck(head);
}
328. 奇偶链表
题意:奇数下标的结点连在一起,偶数下标的结点连在一起作为后半部分
开另一个dummy结点存储取出的偶结点,最后接到原链表后
817. 链表组件
题意:计算链表中 元素连续出现在某个数组中 的区间数量
先把数组元素存入集合,方便查找,然后遍历链表即可
2130. 链表最大孪生和
题意:在一个大小为 n
且 n
为 偶数 的链表中,第 i
个节点(下标从 0 开始)的孪生节点为第 (n-1-i)
个节点 ,求最大的孪生结点元素和
类似回文链表
138. 复制带随机指针的链表
题意:给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。返回这个链表的 __深拷贝__。复制链表中的指针都不应指向原链表中的节点 。
简单方法:使用哈希表记录新旧结点关系,按序构建新链表,然后再次遍历,根据hash表重建random指针
巧妙方法:
首先将该链表中每一个节点拆分为两个相连的节点,例如对于链表 ,我们可以将其拆分为 。对于任意一个原节点 ,其拷贝节点 即为其后继节点。
然后,重建random指针,对于一个原链表节点执行p->next->random = p->random->next
最后,将两个链表拆分开。
困难题
23. 合并K个升序链表
朴素方法:k-1次循环,每次取一个链表合并到第一个链表,O(nk^2)
进阶方法:分治合并,logk次,O(nklogk)
更好的方法:维护当前每个链表没有被合并的元素的最前面一个, 个链表就最多有 个满足这样条件的元素,每次在这些元素里面选取 val
属性最小的元素合并到答案中。在选取最小元素的时候,用优先队列来优化这个过程。优先队列中的元素不超过 个,最多有 个点,插入删除各一次,故总的时间代价即渐进时间复杂度为 。
25. K 个一组翻转链表
细节题
实现一个函数,输入一个子链表的头和尾节点,翻转,并且返回新的头与尾
148. 排序链表
题意:归并排序链表
430. 扁平化多级双向链表
类似二叉树的先序遍历
相似题目 114. 二叉树展开为链表
1171. 从链表中删去总和值为零的连续节点
朴素方法:每个位置都执行向后遍历,删除和为0的区间,直到不能再删,换下一个位置,O(n^2)
两次遍历法:第一次求前缀和,维护前缀和到节点(保留最后一个)的哈希表;第二次遍历,若当前节点处前缀和在哈希表出现了
,则表明两结点之间所有节点和为0,直接删除区间所有节点
2074. 反转偶数长度组的节点
细节题