指针操作小技巧:
1、一旦出现类似 nxt.next
这种操作,就要条件反射地想到,先判断 nxt
是否为 null,否则容易出现空指针异常。
2、注意循环的终止条件。你要知道循环终止时,各个指针的位置,这样才能保返回正确的答案。如果你觉得有点复杂想不清楚,那就动手画一个最简单的场景跑一下算法,比如这道题就可以画一个只有两个节点的单链表 1->2
,然后就能确定循环终止后各个指针的位置了。
1.1合并两个有序链表
创建结果链表的哨兵节点 sentinel ;
然后用两个指针遍历两个链表 l1 和 l2,每次将较小的节点添加到结果链表中,直到其中一个链表为空跳出循环。
最后,将剩余的链表直接接到结果链表的尾部,返回 sentinel.next 即可。
速度击败100%
class Solution {
ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode sentinel = new ListNode(-1, null);
ListNode l = sentinel;
while(l1 != null && l2 != null){//
if(l1.val < l2.val){
l.next = l1;
l1 = l1.next;
l = l.next;
}
else{
l.next = l2;
l2 = l2.next;
l = l.next;
}
}
if(l1 != null){
l.next = l1;
}
if(l2 != null){
l.next = l2;
}
return sentinel.next;
}
}
1.2合并K个升序链表
创建结果链表的sentienl,若K == 0则直接返回null,防止创建优先级队列的时候出错;
把这K个升序链表非空链表的头节点放入优先级队列;
循环弹出队列头即当前结点中最小的结点,连接到结果链表上,若该结点不是尾结点,则把它的后一个结点加入优先级队列,直到优先级队列为空。返回sentinel.next;
class Solution {
public ListNode mergeKLists(ListNode[] lists){
if(lists.length == 0)//必须有,否则创建优先级队列的时候会出错
return null;
ListNode sentinel = new ListNode(-1, null);
PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length, (a, b)->(a.val - b.val));
for(int i = 0; i < lists.length; i++){
if(lists[i] != null)
pq.add(lists[i]);
}
ListNode l = sentinel;
while(!pq.isEmpty()){
l.next = pq.poll();
l = l.next;
if(l.next != null){
pq.add(l.next);
}
}
return sentinel.next;
}
}
1.3根据特定值分割链表
创建两个哨兵节点 sentinelSmall 和 sentinelBig,分别用于连接小于 x 和大于等于 x 的节点。
遍历链表,将小于 x 的节点添加到 sentinelSmall 链表中,其他节点添加到 sentinelBig 链表中。
把sentinelBig链表的结尾置为null,防止出现环,最后再将 sentinelBig 链表链接到 sentinelSmall 链表后,返回 sentinelSmall.next 作为结果。
/* 创建两个senitnel,分别连接比x小的和比x大的结点,最后把这两个链表连接起来
注意:必须把大的那个链表结尾置为null,防止出现环!*/
class Solution {
public ListNode partition(ListNode head, int x){
ListNode sentinelSmall = new ListNode(-1, null);
ListNode sentinelBig = new ListNode(-1, null);
ListNode lS = sentinelSmall, lB = sentinelBig;
while(head != null){
if(head.val < x){
lS.next = head;
head = head.next;
lS = lS.next;
}
else{
lB.next = head;
head = head.next;
lB = lB.next;
}
}
lB.next = null;//注意:
lS.next = sentinelBig.next;
return sentinelSmall.next;
}
}
二、快慢指针
2.1删除倒数第k个结点
要删除倒数第n个结点,就要找到倒数第n+1个结点:
用两个指针遍历,让第一个指针先走n+1步后,另一个再开始同步遍历,直到第一个指针到走null 就找到了倒数第N+1个结点
需要注意的是:删除结点必须要有sentinel结点,因为有可能要删除的是头节点!!所以应将两个指针初始化为sentinel,最后返回sentinel.next;
/*要删除倒数第n个结点,就要找到倒数第n+1个结点:
用两个指针遍历,第一个指针先走n+1步后,另一个再开始同步遍历,直到第一个指针到null
注意:删除结点必须要有sentinel,因为有可能要删除的是头节点!!*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int k){
ListNode sentinel = new ListNode(-1, head);
ListNode l1 = sentinel;
ListNode l2 = sentinel;
for(int i = 0; i < k+1; i++){
l1 = l1.next;
}
while(l1 != null){
l1 = l1.next;
l2 = l2.next;
}
l2.next = l2.next.next;
//此时因为倒数第n个结点没有了reference,所以会被java清理掉
return sentinel.next;
}
}
2.2返回单链表的中间结点(若有两个中间结点返回第二个)
法1:使用sentinel结点统一
快慢指针同时同地开始遍历,一次fast走两步/一步,slow走一步,直到fast走到null,快慢指针走的次数应相同。注❗️:这里的一次指的是代码1中的一次循环,该写法只要fast不为null就会进入循环即会至少走一步。
若没有虚拟结点sentienl,那么:
[1, 2, 3, 4, 5, null]: fast应走5步到null(即3次),slow应走2步(即2次)
[1, 2, 3, 4, 5, 6, null]: fast应走6步到null(即3次),slow应该走3步(即3次)
第一种偶数个情况的快慢指针走的次数不同,所以不能同时从头节点出发,若加上sentienl,就是:
第一种就是fast走6步->3次,slow走3次->3步,就对了
第二种就是fast走7步->4次, slow走4次->4步,也对了
所以该题只需添加sentinel结点,快慢指针同时从sentinel结点开始遍历就能统一奇偶数的情况。
若想要返回第一个中间结点:
[ 1, 2, 3, 4, 5, null]:fast应走5步到null(即3次),slow应走2步(即2次)
[1, 2, 3, 4, 5, 6, null]: fast应走6步(即3次),slow应该走2步(即2次)
那么就不需sentinel结点,快指针先走一次2步,然后慢指针从头节点出发再一起遍历。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode sentinel = new ListNode(-1, head);
ListNode fast = sentinel;
ListNode slow = sentinel;
while(fast != null){
fast = fast.next;
if(fast != null)
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
法2 可判断结点是奇数/偶数个
但循环也有另一种写法:这种就不需要sentinel结点,且可以判断该链表的结点是奇数个还是偶数个!
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}//此循环执行一次fast必走2步;。
if(fast != null)//说明该链表有奇数个结点,slow,fast还需继续往后走一步;
{ slow = slow.next;
fast = fast.next;
}
2.3找到链表的环入口点
一个链表最多只有一个环
快慢指针同时从头节点出发,设到达相遇点时,慢指针走了k步,那么快指针就多走了k步,再设相遇点到环入口点为x步,那么从快指针的角度,可以得到从相遇点走k-x步就到了环入口点,因为:
快指针比慢指针多走的步数就是在环里打转,也即k步是环长度的整数倍,因此从相遇点走k-x步就是走环的整数倍再减去x步,也就是走到了环入口点。
同时显然,若到达相遇点后慢指针回到头节点,走k-x步,也是到了入口点。
因此,到达相遇点后,慢指针回到头节点,快指针依然在相遇点,然后两个指针同时同速度出发,再次相遇时就是环入口点(都走了k-x步)。7.
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null)
return null;
ListNode fast = head;
ListNode slow = head;
while(fast != null){
fast = fast.next;
if(fast == null)//防止接下来的fast.next出现nullpointerError
return null;
fast = fast.next;//
slow = slow.next;
if(slow == fast)
break;
}
if(fast == null)
return null;
slow = head;
while(fast != slow){
slow = slow.next;
fast = fast.next;
}
return slow;//or fast;
}
}
2.4判断两个链表是否有相交结点并返回
把A链表的尾结点与B链表的头结点相连,就变成了判断是否有环和若有环则寻找环入口结点的问题,即若连接后不存在环说明没有相交结点。但要特别注意以下三点:
1.fast每走一步前都要先判断是否为尾结点(fast.next == null),若是则需先连接另一个链表;
2.必须保证两链表只能连接一次,否则当两链表没有相交结点的情况下,会因为连接两次出现lastA->headB->lastB->headB链表B整个变成环的情况,导致无法判断两链表没有相交的情况。
那么如何判断没有相交结点:即两链表已经连接过一次,但fast依然出现了为null的情况(不存在环),
因此额外设置一个hasLinked变量标记是否已经连接过链表;
3.题目要求返回结果不改变原链表的结构,所以返回前要还原链表!那就额外用一个变量指向尾结点,在返回前把尾结点.next设置回null;
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode fast = headA;
ListNode slow = headA;
ListNode lastNode = null;
boolean hasLinked = false;//
while(fast != null){
//若当前fast是尾结点且两链表还未连接过(即当前fast是A链表的尾结点)
if(fast.next == null && !hasLinked){
lastNode = fast;
lastNode.next = headB;
hasLinked = true;
}
fast = fast.next;
//若已经连接过一次,但又出现fast为null则说明两链表没有相交结点!
if(fast == null && hasLinked){
lastNode.next = null;
return null;
}
//若当前fast是尾结点且两链表还未连接过(即当前fast是A链表的尾结点)
if(fast.next == null && !hasLinked){
lastNode = fast;
lastNode.next = headB;
hasLinked = true;
}
fast = fast.next;
slow = slow.next;
if(slow == fast)
break;
}
//必须要有这个判断,因为循环结束有两种可能,要么fast == null要么slow == fast !=null
if(fast == null && hasLinked){
lastNode.next = null;
return null;
}
slow = headA;
while(fast != slow){
slow = slow.next;
fast = fast.next;
}
lastNode.next = null;
return slow;//or fast;
}
}
三、反转链表相关
3.1基础:反转整个链表:
3.1.1三指针迭代法
初始化三个指针:pre为null, cur为head, nxr为head.next,所以要先判断head(以及head.next)是否为null,若是则直接返回head.
利用while循环,把cur.next指向pre, pre, cur, nxt都往后移一个,注意要先判断nxt不为null,才能将nxt往后移(即若出现nxt.next,必须先判断nxt不为null,否则会出现nullpointerError),直到cur为null结束循环,最后返回pre;
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre, cur, nxt;
pre = null;
cur = head;
nxt = head.next;
while(cur != null){
cur.next = pre;
pre = cur;
cur = nxt;
if(nxt != null){
nxt = nxt.next;
}
}
return pre;
}
}
3.1.2 递归法
对于1->2->3->4->null这样的单链表,递归函数应该做两件事:
1.先递归反转除第一个结点组成的子链表,得到1->reverseList(2->3->4->null),注意head.next依然指向原来的第二个结点,如图:
2.再把第一个结点改连接到最后:
base case:当前需要处理的链表为null或者只有一个结点;
必须先反转子链表,再改连接第一个结点,因为最终连接的顺序是2->1
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) //
return head;
ListNode List = reverseList(head.next);
head.next.next = head;
head.next = null;
return List;
}
}
3.2 反转固定区间【left, right]的结点
3.2.1总思路:
将第left个结点作为头节点组成子链表,反转它的前right-left+1个结点,所以我们可以写一个辅助方法reverseN即反转链表的前n个结点method并返回反转后的头结点,
最后再把left结点的前一个结点.next指向返回的头结点, 所以需要记录第left-1个结点,也因此当left是头结点即left==1时要记录它的前一个结点,就需要有sentinel结点了 , 最后返回sentinel.next。
3.2.2基础进阶:反转链表的前n个结点 reverseN(ListNode head, int n)
数据保证了n<=链表长度
使用三指针迭代法:当cur处理到第n+1个结点时跳出循环,再把head.next指向cur结点,同时把head指向pre结点 ,最后返回head。
使用递归法:
class Solution {
/* 反转链表的前n个结点 数据保证了n<=链表长度
使用三指针迭代法,当cur处理到第n+1个结点时跳出循环,再把head.next指向cur结点,同时把head指向pre结点 ,最后返回head。 */
private ListNode reverseN(ListNode head, int n){
if(head == null || head.next == null)
return head;
ListNode pre = null, cur = head, nxt = head.next;
int cnt = 1;
while(cnt <= n){//cnt==n时进入循环处理的是第n个结点
cur.next = pre;
pre = cur;
cur = nxt;
if(nxt != null){
nxt = nxt.next;
}
cnt ++;
}
head.next = cur;
head = pre;
return head;
}
/* 将第left个结点作为头节点组成子链表,反转它的前right-left+1个结点,所以我们可以写一个辅助方法即反转链表的前n个结点method并返回反转后的头结点,
最后再把left结点的前一个结点.next指向返回的头结点, 所以需要记录left的前一个结点,
也因此当left是头结点的,这时就需要有sentinel结点了 .最后返回sentinel.next. */
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode sentinel = new ListNode(-1, head);
ListNode preLeft = sentinel;
for(int i = 1; i <= left-1; i++){
preLeft = preLeft.next;
}
preLeft.next = reverseN(preLeft.next, right-left+1);
return sentinel.next;
}
}
3.3 以K个一组依次反转链表
若最后一组个数少于k则不予改变
从头节点开始,利用reverseBetween(head, int left, int right),套一层for循环,初始化left = 1, right = left+k-1 ;循环步长为k, left, right同时增加;当right > 链表长度时结束,所以要先遍历计算长度:
/*从头节点开始,利用reverseBetween(head, int left, int right),套一层for循环,初始化left = 1, right = left+k-1 ;循环步长为k, left, right同时增加;当right > 链表长度时结束,所以要先遍历计算长度*/
public ListNode reverseKGroup(ListNode head, int k) {
ListNode p = head;
int n = 0;
while(p != null){
n ++;
p = p.next;
}
for(int left = 1, right = left+k-1; right <= n; left+= k, right += k){
head = reverseBetween(head, left, right);
}
return head;
}