前言
记录 LeetCode 刷题中遇到的链表相关题目,第二篇
142. 环形链表 II
我们使用快慢双指针来完成这道题
假设链表有环,如上图所示,绿色部分即为环,从链表头 head 到环入口的距离为 a。fast 跟 slow 指针从 head 出发,fast 每次向后移动两个节点,slow 每次向后移动一个节点。那么 fast 跟 slow 肯定会相遇,假设相遇的节点离环的入口节点距离为 b,到环的最后一个节点距离为 c。那么环的长度为 b + c
那么相遇的时候,fast 走过的路程为 a + n(b + c) + b,n 表示 fast 走过了多少个完整的环,至于 n 为多大,跟 a 有关,a 越大,相遇之前 fast 在环中走的时间就越久,n 就越大。这不影响我们解题;
而 slow 走过的路程就为 a + b,即 slow一个完整的环都没走完时,就会与 fast 相遇。当然,让 slow 跟 fast 无休止地走下去的话,它们会相遇很多次,但我们只需要它们第一次相遇的数据就够了,
那么根据 fast 的 “速度” 是 slow 的两倍,就有 a + n(b + c) + b = 2(a + b),化简可得
a = ( n − 1 ) ( b + c ) + c a = (n - 1)(b + c) + c a=(n−1)(b+c)+c
等式左边就表示从 head 到环入口的路程,等式右边可以看成 slow 把当前走的环剩下的路程 c 走完后,再走 n - 1 个环的路程,可以看出来走到最后 slow 就会在环入口的位置
所以让一个新的指针指向 head,然后在 head 跟 slow 未相遇时,两个指针都每次向前走一步,直到两个指针相等,所在的节点就是环入口节点
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null || head.next.next == null) return null;
ListNode fast = head.next.next,slow = head.next;
while(fast != slow){
if(fast.next == null || fast.next.next == null) return null;
fast = fast.next.next;
slow = slow.next;
}
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
234. 回文链表
可以借助反转链表的操作,使用快慢指针找到链表中点,然后将前半部分或后半部分反转,如果把前半部分反转,就比较反转后的前半部分以及原来的后半部分是否相同即可
下面给出的是递归的代码,先找到后半部分的起始节点,然后递归遍历后半部分,递归的效果是后半部分的节点会被逆序访问,而没有直接地去反转链表
class Solution {
ListNode prv; //prv指向前半部分链表
boolean rec(ListNode n){
if(n.next != null){
if(!rec(n.next)) return false; //如果后面的节点已经出现了不相等的情况则直接返回false
}
if(n.val == prv.val){ //否则就比较当前节点跟对应的前半部分中的节点是否相等,同时更新prv
prv = prv.next;
return true;
}
return false;
}
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
ListNode fast = head,slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
//找到后半部分的起始节点,模拟几个例子可以发现,当fast不能继续往前走两步时
//如果fast此时就是null,那么slow的位置就是后半部分的起始节点
//不然的话就是fast的next是null,那么slow应该再走一步才是后半部分的起始节点
if(fast != null) slow = slow.next;
prv = head;
return rec(slow);
}
}
86. 分割链表
快慢双指针:保证慢指针往前的 (包括慢指针本身) 都是小于 x 的节点,慢指针的下一个就是大于等于 x 的节点;由快指针去找到小于 x 的节点,然后将其插入到慢指针的后面。直到快指针指向 null,算法结束
注意点:
- 为了避免插入到头节点之前的情况,设置一个 dummy 节点作为临时头节点
- 由于要把快指针的节点插到慢指针之后,那么就要把插入前快指针的前驱节点跟其后继节点相连,由于是单向链表,所以要维护快指针的前驱节点 fastPrv
public ListNode partition(ListNode head, int x) {
ListNode dummy = new ListNode(-1,head);
ListNode slow = dummy;
//让 slow 指向第一个大于等于 x 的节点的之前一个节点
while(slow.next != null && slow.next.val < x) slow = slow.next;
ListNode fast = head,fastPrv = dummy;
//让 fast 指向第一个大于等于 x 的节点
while(fast != null && fast.val < x){
fast = fast.next;
fastPrv = fastPrv.next;
}
while(true){
//快指针向后查找小于等于 x 的节点
while(fast != null && fast.val >= x){
fast = fast.next;
fastPrv = fastPrv.next;
}
if(fast == null) break; //注意及时判断是否遇到 null 及时 break
//下面就是将fast所指节点插入到slow的后面,同时移动fast 跟 slow
fastPrv.next = fast.next;
fast.next = slow.next;
slow.next = fast;
slow = slow.next;
fast = fastPrv.next;
}
return dummy.next;
}
61.旋转链表
将链表尾接到链表头上形成 循环链表,然后再根据 k 的大小,找到旋转后最后一个节点,将其与其下一个节点的链接断开,断开前先记录最后一个节点的下一个节点,也就是旋转后链表的头节点,断开后返回这个新头节点即可
public ListNode rotateRight(ListNode head, int k) {
if(head == null || head.next == null || k == 0) return head;
ListNode tmp = head;
int len = 1;
//计算链表长度
while(tmp.next != null){
tmp = tmp.next;
len++;
}
//k可能比原链表长度大,实际需要旋转的只有k%len个单位
//如果k恰好为链表长度的倍数那就不用旋转 (旋转后跟原来链表是一样的)
int m = k % len;
if(m == 0) return head;
//连成循环链表
tmp.next = head;
//查找旋转后链表最后一个节点,让tmp指向它
m = len - m;
while(m-- > 0){
tmp = tmp.next;
}
//断开链接
ListNode res = tmp.next;
tmp.next = null;
return res;
}
2. 两数相加
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode p1 = l1;
ListNode p2 = l2;
ListNode dummy = new ListNode(); //哑节点
ListNode tmp = dummy;
int extra = 0;
while(p1 != null && p2 != null){
tmp.next = new ListNode();
tmp = tmp.next;
int val = p1.val + p2.val + extra;
extra = val / 10;
tmp.val = val % 10;
p1 = p1.next;
p2 = p2.next;
}
while(p1 != null){
tmp.next = new ListNode();
tmp = tmp.next;
int val = p1.val + extra;
extra = val / 10;
tmp.val = val % 10;
p1 = p1.next;
}
while(p2 != null){
tmp.next = new ListNode();
tmp = tmp.next;
int val = p2.val + extra;
extra = val / 10;
tmp.val = val % 10;
p2 = p2.next;
}
if(extra != 0){
tmp.next = new ListNode(extra);
tmp.next.next = null;
}else {
tmp.next = null;
}
return dummy.next;
}
445. 两数相加 II
跟上面的 链表求和 不一样的地方在于这里的链表从头到尾是从高位到低位,所以利用栈将两个链表中的值反转过来就可以跟 链表求和 一样从低位到高位直接相加了
那怎么做到还是从头到尾只遍历一遍完就得到结果:
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int count = 0; //用于计算长度
ListNode head, last;
for(head = l1; head != null; head = head.next) count++;
for(head = l2; head != null; head = head.next) count--;
//让l1指向较长的链。如果count小于0,说明l2更长,那就交换。相加后的值存于l1中
if(count < 0) {
ListNode t = l1;
l1 = l2;
l2 = t;
}
//head用于记录相加后链表的头节点
//last用于记录上一个值小于9的节点,这样当某一位相加后大于9,就让last节点值增1,
//然后让last节点与当前节点之间的所有节点 (值都为9) 的值都置为0,完成进位操作
//在最前面加一个值为0的节点作为初始的last节点,防止最高位也产生进位,如果最终该节点值仍为0则删除该节点
last = head = new ListNode(0);
head.next = l1; //相加后的值存于l1中
for(int i = Math.abs(count); i != 0; i--){ //取l1中后面的与l2等长的部分与l2相加
if(l1.val != 9) last = l1;
l1 = l1.next;
}
int tmp;
while(l1 != null){
tmp = l1.val + l2.val;
//发生进位,则更新last到l1之间所有数位的值,同时l1会成为新的last
if(tmp > 9){
tmp -= 10;
last.val += 1;
last = last.next;
while(last != l1){
last.val = 0;
last = last.next;
}
}
//没有发生进位且tmp小于9的话,last就应该更新,指向l1;如果tmp就等于9,那last就保持不变
else if(tmp != 9) last = l1;
l1.val = tmp;
l1 = l1.next;
l2 = l2.next;
}
return head.val == 1 ? head : head.next; //head等于1说明原来两个链表的最高位发生了进位,返回head
}
上面的做法应该算是时间上最优解了
剑指 Offer II 023. 两个链表的第一个重合节点
假设有链表 A 以及链表 B,它们重合部分长度为 l,链表A中除了重合部分外前面的部分长度为 l1,链表 B 中除了重合部分外前面的部分长度为 l2
让两个指针 p1,p2 同时开始遍历 A 和 B,且遍历完再去遍历另外一个链表。即对于 p1 来说,它先去遍历 A,遍历完再去遍历 B,当遍历 B 遍历到第一个重合节点时,其走过的路程为 l1 + l + l2;同理对于 p2,当它遍历完 B 再去遍历 A 且遍历到第一个重合节点时,它走过的路程为 l2 + l + l1,可以发现两个指针走过的路程都是相等的,也就是说它们会在第一个重合节点相遇,所以相遇时直接返回它们所在节点即可
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode p1 = headA;
ListNode p2 = headB;
while(p1 != p2){
p1 = p1 == null ? headB : p1.next;
p2 = p2 == null ? headA : p2.next;
}
return p1;
}