[链表 双指针] 141. 环形链表(快慢指针找环)142. 环形链表 II(双指针求倒数第k个节点)143. 重排链表(快慢指针找中位点+原地逆序链表)
141. 环形链表(快慢指针判断是否有环,不需要找环的起点)
题目链接:https://leetcode-cn.com/problems/linked-list-cycle/
分类:链表、双指针(快慢指针找环)
这题只需要判断有没有环即可。
思路1:map存放链表节点 (空间复杂度O(N))
比较简单,不再赘述。
思路2:双指针法(空间复杂度为O(1))
定义两个指针,同时从链表头节点出发,一个指针slow一次走一步,另一个指针fast一次走两步:
- 如果走得快的指针追上了走得慢的指针,说明链表包含环
- 如果走得快的指针走到了链表末尾到达null时,都没有追上慢指针,说明没有环。
为什么快慢指针能找到链表中的环?(路程差不断+1)
这个问题等价于“为什么快慢指针能够在链表的环路中相遇?”
慢指针每次移动一个节点,设速度为s,快指针每次移动两个节点,设速度为f,则f=2s,因为它们所走的时间是相同的,所以各自所走的路程中fast是slow的两倍,可以发现两个指针的路程差从1 →2→3→4…,即每过一个单位时间,两个指针的路程差就+1。
在一个环中,如果两个指针的路程差不断+1,最终路程差一定会等于环路的长度,此时两个指针就相遇了,快慢指针能够相遇,说明链表存在环路。
实现代码:
public class Solution {
public boolean hasCycle(ListNode head) {
//鲁棒性
if(head == null || head.next == null)
return false;
ListNode fast = head, slow = head;
//当fast=null或两指针相遇退出循环
while(fast != null){
if(fast.next != null) {//易遗漏,关键判断:fast还未指向尾节点时指针正常工作
fast = fast.next.next;//快指针一次走两步
slow = slow.next;//慢指针一次走一步
}
//若fast.next==null,则表示fast已经指向链表的最后一个节点,不可能存在环
else break;
//判断过程中出现两指针相遇就立即退出循环
if(fast == slow)
break;
}
//退出循环的情况有很多,return true的情况单独考虑,其余情况都return false
if(fast == slow){
return true;
}
//fast==null或fast.next.next=null的情况
return false;
}
}
142. 环形链表 II (寻找找出环的入口节点)
题目链接:https://leetcode-cn.com/problems/linked-list-cycle-ii/
分类:链表、双指针(快慢指针找环,同步指针找环的入口节点)
142比起141,如果链表有环,则需要找出环的入口节点。
- 判断链表是否有环,使用和141相同的方法:快慢指针;
- 寻找环的入口,先统计环内节点个数N,再设置两个同步指针,两指针相隔N个节点且同步移位,当两个指针相遇时就指向了环的入口。
思路1:快慢指针(找环)+同步指针(思路同找倒数第k个节点)
算法流程:
- 使用快慢指针判断是否有环 (141),当两指针指向同一点时说明有环,记录下这个节点meet;
- 开辟一个计数器,从meet开始遍历同时开始计数,直到再次遇到meet节点计数结束,得到的数就是环的节点数n;
- 再开辟两个指针formmer和latter,formmer先走n步后,latter才开始走,当latter到达环入口时,formmer正好也走完一整个环,回到环入口,此时formmer和latter指向同一个节点,这个节点就是环入口。(这个方法和 寻找链表的倒数第k个节点相同)
实现代码:
public class Solution {
public ListNode detectCycle(ListNode head) {
//鲁棒性判断:
if(head == null || head.next == null )
return null;
ListNode fast = head, slow = head;
int count=1;//计算环的节点个数,count的初始值受第二个while条件的影响
//1.判断是否有环
//先做一次指针移位,否则一开始fast=slow导致进入不了下面的while
fast = fast.next.next;
slow = slow.next;
while(fast != slow && (fast != null && fast.next != null)){
fast = fast.next.next;
slow = slow.next;
}
//不存在环则返回null
if( fast == null || fast.next == null ){
return null;
}
//存在环则返回fast和slow的相遇节点
ListNode meet = fast;
//2.计算环的节点个数
//注意链表只有一个节点,next指向自己,也是一个环
while( fast.next != meet){
count++;
fast = fast.next;
}
//3.找出环入口
//先将fast,slow拿来重用,重新置回head
fast = head;
slow = head;
//fast指针先开始工作,移动count步
while(count > 0){
fast = fast.next;
count--;
}
//fast走了count步后,fast和slow开始同步移动,直到两指针相遇
while(fast != slow){
slow = slow.next;
fast = fast.next;
}
//退出循环时,两个指针指向的同一个节点就是环入口
return fast;
}
}
143. 重排链表(快慢指针找中位点 + 原地逆序链表)
题目链接:https://leetcode-cn.com/problems/reorder-list/
分类:链表、双指针(快慢指针找中位点、辅助节点用于原地逆序)
题目分析
链表的一个缺点是不能随机访问,所以这题每一轮交换都需要从当前节点遍历到倒数的节点,才能找到待交换的节点,但这样一来时间复杂度很高。
这里有两个思路:
思路1:使用辅助空间记录链表节点,让链表能够随机访问,空间复杂度为O(N);
思路2:找到链表中位点,然后对链表后半部分逆序处理,再将前后部分链表节点交替组合,空间复杂度为O(1)
思路1:辅助空间 (空间复杂度为O(N))
使用一个列表存放每个节点,这样通过O(1)的时间就能找到待交换节点。
设链表长度为n,对于第i个节点,它的next要指向第n-i-1个节点。(节点序号从0开始计)
- 当i == n-i-1 时,说明到达算法结尾。
实现代码:
class Solution {
public void reorderList(ListNode head) {
if(head == null || head.next == null) return;
List<ListNode> list = new ArrayList<>();
ListNode work = head;//工作节点
//将链表每个节点依次存入列表中
while(work != null){
list.add(work);
work = work.next;
}
int n = list.size();
//开始交换节点
for(int i = 0; i < n/2; i++){//只需遍历到链表的中位点即可
ListNode temp = head.next;//记录节点原来的next
head.next = list.get(n - i - 1);
head.next.next = temp;
head = temp;
}
head.next = null;//将最后一个节点的next置为空,避免出现环路
}
}
思路2:后半链表原地逆序 + 前后部分节点交替组合(空间复杂度O(1))
算法流程:
- 先使用快慢指针找到链表的中位点:
fast指针一次移动两个节点,slow指针一次移动一个节点。
但要注意,链表长度的奇偶性会对快慢指针的终止条件和寻找到的中位点有所影响:
- 链表长度为偶数:1->2->3->4,则找到的中位点时fast.next.next==null,slow指向2,则2就是中位点;
- 链表长度为奇数:1->2->3->4->5,则找到中位点时fast.next==null,slow指向3,则3就是中位点。
为了统一处理,头结点~中位点作为前半部分,取中位点的下一个节点~尾结点作为后半部分。
-
对链表的后半部分做原地逆序处理。
-
将链表的前半部分和后半部分交替选取一个节点组合,构成新链表。
按1的设置,最终划分的前后部分的节点数量:前半部分>=后半部分,所以以后半部分为空作为算法的终止条件,当后半部分到达末尾时,前半部分的节点保持原状即可。
实现代码:
class Solution {
public void reorderList(ListNode head) {
if(head == null || head.next == null) return;
//快慢指针寻找链表中位点
ListNode fast = head, slow = head;
while(fast.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
}
//退出循环时slow指向的就是中位点
ListNode tail = null;//tail用于原地反转链表
ListNode cur = slow.next;//取中位点的下一个节点作为后半部分的头结点
slow.next = null;//将前半部分的末尾置null,将链表截断成独立的两部分,避免出现环路
//原地翻转链表的后半部分
while(cur != null){
ListNode temp = cur.next;
cur.next = tail;
tail = cur;
cur = temp;
}
//退出循环后tail指向翻转后的头结点
ListNode pre = head, post = tail;
//前、后半部分节点交替组合
while(post != null){
ListNode tempPre = pre.next;
ListNode tempPost = post.next;
pre.next = post;
post.next = tempPre;
pre = tempPre;
post = tempPost;
}
}
}