leetcode 203.移除链表元素
删除链表中等于给定值 val 的所有节点,并返回新的头节点。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
删除非头节点让节点next指针直接指向下下一个节点
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。将头结点向后移动一位就可以从链表中移除一个头结点
当我们遍历到链表的最后一个节点时,cur.next 为空节点,如果不加以判断,访问cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表
需要单独写一段逻辑来处理移除头结点的情况
迭代写法:
时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。
空间复杂度:O(1)。
class Solution {
public ListNode removeElements(ListNode head, int val) {
while (head!=null&&head.val==val) {//删除头节点
head = head.next;
}
ListNode temp = head;//删除非头节点
while (temp!=null&&temp.next!=null) {
if (temp.next.val==val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
return head;
}
}
递归写法:
时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。
空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
head.next = removeElements(head.next, val);
return head.val == val ? head.next : head;
}
}
给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1,就可以使用和移除链表其他节点的方式统一
dummyNode->next
是移除后的新头节点
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
//设置虚节点,统一操作
ListNode dummy = new ListNode(-1,head);
ListNode pre = dummy;
ListNode cur = head;
while (cur!=null) {
if (cur.val==val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
}
leetcode 83.删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素
当我们遍历到链表的最后一个节点时,cur.next 为空节点,如果不加以判断,访问cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点,而不需要遍历完整个链表
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head==null) {
return head;
}
ListNode cur = head;
while (cur.next!=null) {
if (cur.val==cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
leetcode 19.删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1 输出:[]
示例 3:
输入:head = [1,2], n = 1 输出:[1]
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
删除slow指向的下一个节点,如图:
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode fast = dummyHead,slow = dummyHead;
while (n--!=0) {
fast = fast.next;
}
fast = fast.next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while(fast!=null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;//删除操作
return dummyHead.next;
}
}
leetcode 707.设计链表
单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的
在链表类中实现这些功能:
设计链表的五个接口:
class ListNode { //单链表
int val;
ListNode next;
ListNode() {}
ListNode(int val) {
this.val = val;
}
}
class MyLinkedList {
int size;
ListNode dummyHead;//虚拟头节点
public MyLinkedList() {
size = 0;
dummyHead = new ListNode(0);
}
public int get(int index) { //index从0开始,0表示头节点
if (index<0||index>=size) {
return -1;
}
ListNode cur = dummyHead;
for (int i=0;i<=index;i++) {//包含一个虚拟头节点,所以查找第index+1个
cur = cur.next;
}
return cur.val;
}
//在链表最前面插入一个节点
public void addAtHead(int val) {
addAtIndex(0,val);
}
//在链表的最后插入一个节点
public void addAtTail(int val) {
addAtIndex(size,val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index>size) {
return;
}
if (index<0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pre = dummyHead;
for (int i=0;i<index;i++) {
pre = pre.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pre.next;
pre.next = toAdd;
}
public void deleteAtIndex(int index) {
if (index<0||index>=size) {
return;
}
size--;
ListNode pre = dummyHead;
for (int i=0;i<index;i++) {
pre = pre.next;
}
pre.next = pre.next.next;
}
}
class ListNode { //双链表
int val;
ListNode next,prev;
ListNode(int val) {
this.val = val;
}
}
class MyLinkedList {
int size;
ListNode dummyHead,tail;//虚拟头节点
public MyLinkedList() {
size = 0;
dummyHead = new ListNode(0);
tail = new ListNode(0);
dummyHead.next = tail;
tail.prev = dummyHead;
}
public int get(int index) {
if (index<0||index>=size) {
return -1;
}
ListNode cur = dummyHead;
// 通过判断 index < (size - 1) / 2 来决定是从头结点还是尾节点遍历,提高效率
if(index < (size - 1) / 2){
for(int i = 0; i <= index; i++){
cur = cur.next;
}
}else{
cur = tail;
for(int i = 0; i <= size - index - 1; i++){
cur = cur.prev;
}
}
return cur.val;
}
//在链表最前面插入一个节点
public void addAtHead(int val) {
addAtIndex(0,val);
}
//在链表的最后插入一个节点
public void addAtTail(int val) {
addAtIndex(size,val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index>size) {
return;
}
if (index<0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode cur = dummyHead;
for (int i=0;i<index;i++) {
cur = cur.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = cur.next;
cur.next.prev = toAdd;
toAdd.prev = cur;
cur.next = toAdd;
}
public void deleteAtIndex(int index) {
if (index<0||index>=size) {
return;
}
size--;
ListNode pre = dummyHead;
for (int i=0;i<index;i++) {
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
插入头尾节点可单独写:
public void addAtHead(int val) {
ListNode cur = head;
ListNode newNode = new ListNode(val);
newNode.next = cur.next;
cur.next.prev = newNode;
cur.next = newNode;
newNode.prev = cur;
size++;
}
public void addAtTail(int val) {
ListNode cur = tail;
ListNode newNode = new ListNode(val);
newNode.next = tail;
newNode.prev = cur.prev;
cur.prev.next = newNode;
cur.prev = newNode;
size++;
}
leetcode 206.反转链表
反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用temp指针保存,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
继续移动pre和cur指针。最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时return pre指针就可以了,pre指针就指向了新的头结点。
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur!=null) {
ListNode temp = cur.next;
cur.next = pre;//反转操作
pre = cur;//更新pre和cur
cur = temp;
}
return pre;
}
}
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
空间复杂度: O ( n ) O(n) O(n)
从前往后翻转指针指向:
class Solution {
public ListNode reverse(ListNode pre,ListNode cur) {
if (cur == null) return pre;
ListNode temp = cur.next;
cur.next = pre;
//如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
public ListNode reverseList(ListNode head) {
//初始化pre=null,cur=head
return reverse(null,head);
}
}
上面的递归写法和双指针法实质上都是从前往后翻转指针指向,还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向
class Solution {
public ListNode reverseList(ListNode head) {
// 边缘条件判断 base case
if (head == null) return null;
if (head.next == null) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}
leetcode 92.反转链表 II
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表 。
ListNode successor = null; // 后驱节点
// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
// 记录第 n + 1 个节点
successor = head.next;
return head;
}
// 以 head.next 为起点,需要反转前 n - 1 个节点
ListNode last = reverseN(head.next, n - 1);
//反转指针
head.next.next = head;
// 让反转之后的 head 节点和后面的节点连起来
head.next = successor;
return last;
}
与完全反转的区别:
递归反转链表的一部分:
给一个索引区间 [m,n](索引从 1 开始),仅仅反转区间中的链表元素
ListNode reverseBetween(ListNode head, int m, int n) {
// base case
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
// 初始化指针
ListNode g = dummyHead;
ListNode p = dummyHead.next;
// 将指针移到相应的位置
for(int i = 0; i < m - 1; i++) {
g = g.next; p = p.next;
}
// 头插法插入节点
for (int j = 0; j < n - m; j++) {
ListNode removed = p.next;
p.next = p.next.next;
removed.next = g.next;
g.next = removed;
}
return dummyHead.next;
}
}
leetcode 25.K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
1、先反转以 head 开头的 k 个元素。
2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数。
3、将上述两个过程的结果连接起来。
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode left = head,right = head;
for (int i=0;i<k;i++) {
// 不足 k 个,不需要反转,base case
if (right==null) return head;
right = right.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(left,right);
// 递归反转后续链表并连接起来
left.next = reverseKGroup(right,k);
return newHead;
}
// 迭代反转区间 [left, right) 的元素,注意是左闭右开
ListNode reverse(ListNode left,ListNode right) {
ListNode pre=null,cur = left;
ListNode next;
while (cur!=right) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;//返回反转后的头节点
}
}
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy=new ListNode(0);
dummy.next=head;
//pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
ListNode pre=dummy;
ListNode end=dummy;
while(end.next!=null){
//循环k次,找到需要翻转的链表的结尾,这里每次循环要判断end是否等于空,因为如果为空,end.next会报空指针异常。
for(int i=0;i<k&&end != null;i++){
end=end.next;
}
//如果end==null,即需要翻转的链表的节点数小于k,不执行翻转。
if(end==null){
break;
}
//先记录下end.next,方便后面链接链表
ListNode next=end.next;
//然后断开链表
end.next=null;
//记录下要翻转链表的头节点
ListNode start=pre.next;
//翻转链表,pre.next指向翻转后的链表。
pre.next=reverse(start);
//翻转后头节点变到最后。通过.next把断开的链表重新链接。
start.next=next;
//将pre换成下次要翻转的链表的头结点的上一个节点。即start
pre=start;
//翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start
end=start;
}
return dummy.next;
}
private ListNode reverse(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
}
leetcode 234.回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
寻找回文串的核心思想是从中心向两端扩展
判断一个字符串是不是回文串简单很多,不需要考虑奇偶情况,只需要用双指针,从两端向中间逼近即可。
但单链表无法倒着遍历,无法使用双指针技巧。
最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同
借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表
链表兼具递归结构,树结构不过是链表的衍生。那么,链表其实也可以有前序遍历和后序遍历
void traverse(ListNode head) {
// 前序遍历代码位置
traverse(head.next);
// 后序遍历代码位置
}
如果想正序打印链表中的 val 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作
模仿双指针实现回文遍历:
时间复杂度: O ( n ) O(n) O(n),n为链表长度
空间复杂度: O ( n ) O(n) O(n)
class Solution {
ListNode left;
public boolean isPalindrome(ListNode head) {
left = head;
return traverse(head);
}
boolean traverse(ListNode right) {
if (right == null) return true;
boolean res = traverse(right.next);
res = res && (right.val==left.val);
left = left.next;
return res;
}
}
核心逻辑实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已
1、先通过 双指针技巧 中的快慢指针来找到链表的中点
2、如果fast指针没有指向null,说明链表长度为奇数,slow还要再前进一步
3、从slow开始反转后面的链表,就可以开始比较回文串了
空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode slow = head,fast = head;
while (fast!=null&&fast.next!=null) {
slow = slow.next;//用双指针找到链表中点
fast = fast.next.next;
}
//若fast没有指向null,说明链表长度为奇数,slow还要再前进一步
if (fast!=null)
slow = slow.next;
//反转slow后的链表
ListNode left = head;
ListNode right = reverse(slow);
//开始比较
while(right!=null) {
if (right.val!=left.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
//反转完整链表的方法
ListNode reverse(ListNode head) {
ListNode pre = null,cur = head;
while(cur!=null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
leetcode 24.两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
画图分析,注意操作的先后顺序,用临时节点temp保存后续有用但在之后操作会被覆盖的信息
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode cur = dummyHead;
while (cur.next!=null&&cur.next.next!=null) {
ListNode temp = cur.next;// 记录临时节点
ListNode temp1 = cur.next.next.next;// 记录临时节点
cur.next = cur.next.next;// 步骤一
cur.next.next = temp;// 步骤二
cur.next.next.next = temp1;// 步骤三
cur = cur.next.next;// cur移动两位,准备下一轮交换
//while循环内另一种写法,原理相同
/*
ListNode temp = head.next.next;
cur.next = head.next; // 步骤一
head.next.next = head; // 步骤二
head.next = temp; // 步骤三
cur = head; // 步进1位
head = head.next; // 步进1位*/
}
return dummyHead.next;
}
}
时间复杂度: O ( n ) O(n) O(n) ,n是链表的长度
空间复杂度: O ( n ) O(n) O(n)
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
// 获取当前节点的下一个节点
ListNode cur = head.next;
// 进行递归
ListNode newNode = swapPairs(cur.next);
// 这里进行交换
cur.next = head;
head.next = newNode;
return cur;
}
}
leetcode 21.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
解法:使用虚拟头节点
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(-1),p = dummy;//虚拟头节点
ListNode cur1 = list1,cur2 = list2;
while (cur1!=null&&cur2!=null) {
//while循环内比较cur1,cur2值大小,小的接入p内
if (cur1.val > cur2.val) {
p.next = cur2;
cur2 = cur2.next;
} else {
p.next = cur1;
cur1 = cur1.next;
}
p = p.next;
}
//将链表中剩余元素全部并入p
if (cur1!=null) p.next = cur1;
if (cur2!=null) p.next = cur2;
return dummy.next;
}
}
用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。实现了把k个有序链表的合并化为每次两个有序链表的合并,合并多次。
时间复杂度: O ( k 2 n ) O(k^2n) O(k2n)
假设每个链表的最长长度是 n。在第一次合并后,ans 的长度为 n;第二次合并后,ans 的长度为 2×n,第 i 次合并后,ans 的长度为 i×n。第 i 次合并的时间代价是 O(n+(i−1)×n)=O(i×n),那么总的时间代价为
O(∑ki=1(i×n)) = O ( ( 1 + k ) ⋅ k / 2 × n ) O( (1+k)⋅k/2 ×n) O((1+k)⋅k/2×n)= O ( k 2 n ) O(k^2n) O(k2n)
空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode ans = null;
for (int i=0;i<lists.length;i++) {
ans = mergeTwoLists(ans,lists[i]);
}
return ans;
}
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//if (list1 == null || list2 == null) {
// return list1 != null ? list1 : list2;
//} //这一段可有可无
ListNode dummy = new ListNode(-1),p = dummy;
ListNode cur1 = list1,cur2 = list2;
while (cur1!=null&&cur2!=null) {
if (cur1.val > cur2.val) {
p.next = cur2;
cur2 = cur2.next;
} else {
p.next = cur1;
cur1 = cur1.next;
}
p = p.next;
}
if (cur1!=null) p.next = cur1;
if (cur2!=null) p.next = cur2;
return dummy.next;
}
}
合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上
这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点
leetcode 876.链表的中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
无法直接得到单链表的长度 n,常规方法就是先遍历链表计算 n,再遍历一次得到第 n / 2 个节点,也就是中间节点。
让两个指针 slow 和 fast 分别指向链表头结点 head。
每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样,当 fast 走到链表末尾时,slow 就指向了链表中点。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head,fast = head;
while(fast!=null&&fast.next!=null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
leetcode 141.环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
每当慢指针 slow 前进一步,快指针 fast 就前进两步。
如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head,fast = head;
while(fast!=null && fast.next!=null) {
slow = slow.next;
fast = fast.next.next;
if (slow==fast) {
return true;
}
}
return false;
}
}
leetcode 142.环形链表 II
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
假设从头结点到环形入口节点的节点数为x。 环形入口节点到 fast 指针与slow指针相遇节点节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:
相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针,(y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2,化简得x + y = n (y + z)
因为要找环形的入口,那么要求的是x(头结点到环形入口节点的的距离)
x = n (y + z) - y
再从n(y+z)中提出一个(y+z)来,整理公式之后为如下公式:
x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
当 n为1的时候,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了,公式就化解为 x = z
结论:从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候就是环形入口的节点
n如果大于1,就是fast指针在环形转n圈之后才遇到 slow指针。这种情况和n为1的时候效果是一样的,一样可以通过这个方法找到环形的入口节点,只不过,index1 指针在环里多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head,fast = head;
while (fast!=null&&fast.next!=null) {
slow = slow.next;
fast = fast.next.next;
if (fast==slow) break;
}
//fast遇空指针,说明无环
if(fast==null||fast.next==null) {
return null;
}
slow = head;
while (slow!=fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
leetcode 160.相交链表
leetcode 面试题 02.07.链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
求两个链表交点节点的指针。 交点不是数值相等,而是指针相等。(引用完全相同,即:内存地址完全相同的交点)
难点在于,由于两条链表的长度可能不同,两条链表之间的节点无法对应
如果用两个指针 p1 和 p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1。
解决这个问题的关键是,通过某些方式,让 p1 和 p2 能够同时到达相交节点 c1。
因为链表自交点后都相等,所以如果有交点则有公共尾部
所以求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。
时间复杂度: O ( m + n ) O(m + n) O(m+n),m,n分别为两链表长度
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0,lenB = 0;
while(curA!=null) {// 求链表A的长度
lenA++;
curA = curA.next;
}
while(curB!=null) {// 求链表B的长度
lenB++;
curB = curB.next;
}
curA = headA;//求长度时改变了curA,curB
curB = headB;//此时要重新改回
// 保证curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
int temp = lenA;
lenA = lenB;
lenB = temp;
ListNode tmp = curA;
curA = curB;
curB = tmp;
}
int sub = lenA - lenB;// 求长度差
// 让curA和curB在同一起点上(末尾位置对齐)
while (sub-- > 0) {
curA = curA.next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA!=null) {
if (curA==curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
当链表 headA 和 headB 都不为空时,创建两个指针pA 和 pB,初始时分别指向两个链表的头节点 headA 和headB,然后将两个指针依次遍历两个链表的每个节点。这样相当于逻辑上两条链表接在了一起,就可以让 pA 和 pB 同时到达相交节点 c1。
情况一:两个链表相交
链表headA 和headB 的长度分别是 m 和 n。假设链表headA 的不相交部分有 a 个节点,链表headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m,b+c=n。
如果 a=b,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点;
如果 a!=b,则指针 pA 会遍历完链表 headA,指针 pB 会遍历完链表 headB,两个指针不会同时到达链表的尾节点,然后指针 pA 移到链表 headB 的头节点,指针 pB 移到链表 headA 的头节点,然后两个指针继续移动,在指针 pA 移动了 a+c+b 次、指针pB 移动了 b+c+a 次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。
情况二:两个链表不相交
链表 headA 和headB 的长度分别是 m 和 n。
如果 m=n,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 null,此时返回 null;
如果m!=n,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 pA 移动了 m+n 次、指针 pB 移动了n+m 次之后,两个指针会同时变成空值null,此时返回 null。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
PS:如果把两条链表首尾相连,那么「寻找两条链表的交点」的问题就转换成了前面的「寻找环起点」的问题
leetcode 146.LRU 缓存 (哈希表+双向链表实现)综合复杂问题
369