LeetCode [24. 两两交换链表中的节点]
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
-
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3] -
示例 2:
输入:head = []
输出:[] -
示例 3:
输入:head = [1]
输出:[1]
思路:
-
虚拟头结点法
-
递归法
递归解题首先要明确的是递推公式的含义,在这里递推公式swapPairs()的含义是:将给定的链表中的相邻节点两两交换后返回,返回的是交换完成的链表的头节点。
不要试图去理解递归的每一个步骤,只要知道递推公式处理后的结果是什么就可以了。
在这道题目里,递归终止条件是节点为null或当前节点的下一个节点为null,这意味着没有节点需要交换。
//虚拟头结点法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
//初始化一个虚拟点,初始赋值为0,并且dummyNode的下一个next指针指向head
ListNode dummyNode = new ListNode(0,head);
//让cur来表达当前的节点,首先从虚拟节点开始
ListNode cur = dummyNode;
//如果cur的后面没有节点或者只有一个节点,则没有更多的节点需要交换,将此作为判定循环结束的依据
//否则,获得cur后面的两个节点nodex和nodey,通过更新节点的指针关系实现两两交换节点。
//整体形式是:cur->nodex->nodey,转变为cur->nodey->nodex,再另cur = nodex,
//即cur从第一个点(dummyNode)变为第三个点(第一轮的nodex)
//对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
while(cur.next != null && cur.next.next != null){
ListNode nodex = cur.next;
ListNode nodey = cur.next.next;
cur.next = nodey;
nodex.next = nodey.next;
nodey.next = nodex;
cur = nodex;
}
return dummyNode.next;
}
}
//递归法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 如果当前结点为null或当前结点下一个结点为null,则递归终止
if(head == null || head.next == null){
return head;
}
// subResult是head.next.next之后的结点两两交换后的头结点
ListNode subResult = swapPairs(head.next.next);
ListNode headNext = head.next;
headNext.next = head;
head.next = subResult;
return headNext;
}
}
扩展:递归;new ListNode(0)常见用法
-
递归
关心的主要有三点:- 返回值
- 调用单元做了什么
- 终止条件
-
new ListNode(0)常见用法
- 初始化一个空节点,没有赋值,指针指向为list
ListNode list = new ListNode();
- 初始化一个空节点,初始赋值为0,指针指向为list
ListNode list = new ListNode(0);
- 初始化一个空节点,初始赋值为0,并且list的下一个next指针指向head,指针指向为list
ListNode list = new ListNode(0,head);
- 定义一个空链表
ListNode list=null;
- 通常定义一个空节点还需要有节点的next指针指向,否则只是定义一个空节点
ListNode list = new ListNode(0,head);
or
ListNode list = new ListNode(0);
list.next=head;
LeetCode [19. 删除链表的倒数第 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个,设链表元素个数为size,正数就是第size - n + 1个,但是如何判断是第几个,就需要计算链表的长度,因为没有类似数组的.length方法,所以需要自己来手写一个方法getSize()。
-
栈方法
通过先进后出原则,先将链表元素压栈,弹出的第n个节点就是题目要求删除的节点。且此时的栈顶元素就是删除节点的前驱节点。
-
双指针法
不预处理出链表的长度,以及使用常数空间的前提下解决本题。
由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针 fast 和 slow 同时对链表进行遍历,并且 fast 比 second 超前 n个节点。当 fast 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。
具体地,初始时fast和slow均指向头节点。我们首先使用fast对链表进行遍历,遍历的次数为n。此时,fast和slow之间间隔了 n-1个节点,即fast比slow超前了n个节点。
在这之后,同时使用fast和slow对链表进行遍历。当fast遍历到链表的末尾(即fast为空指针)时,slow 恰好指向倒数第 n 个节点。
根据方法一和方法二,如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将slow指向dummyNode,其余的操作步骤不变。这样一来,当fast遍历到链表的末尾时,slow的下一个节点就是我们需要删除的节点。
//计算链表长度法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0,head);
ListNode cur = dummyNode;
int size = getSize(head);
//从头节点开始对链表进行一次遍历,当遍历到第Size-n+1个节点时,它就是我们需要删除的节点。
//为了方便删除操作,从虚拟节点dummyNode开始遍历Size−n+1个节点。
//当遍历到第Size-n+1个节点时,它的下一个节点就是需要删除的节点,这样只需要修改一次指针,就能完成删除操作。
for(int i = 1;i < size - n + 1;i++){
cur = cur.next;
}
cur.next = cur.next.next;
return dummyNode.next;
}
//从头节点开始对链表进行一次遍历,得到链表的长度
public int getSize(ListNode head){
int size = 0;
while(head != null){
++size;
head = head.next;
}
return size;
}
}
//栈方法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
Deque<ListNode> stack = new LinkedList<ListNode>();
ListNode dummyNode = new ListNode(0,head);
ListNode cur = dummyNode;
while(cur != null){
stack.push(cur);
cur = cur.next;
}
for(int i = 0;i < n;i++){
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return dummyNode.next;
}
}
//双指针法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(0,head);
ListNode fast = head;
ListNode slow = dummyNode;
for(int i = 0;i < n;i++){
fast = fast.next;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummyNode.next;
}
}
扩展:Deque;LinkedList;ArrayDeque
-
双端队列Deque
- Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。
-
Deque有三种用途
-
普通队列(一端进另一端出)
Queue queue = new LinkedList()
or
Deque deque = new LinkedList()
-
双端队列(两端都可进出)
Deque deque = new LinkedList()
-
堆栈
Deque deque = new LinkedList()
Deque堆栈操作方法:push()、pop()、peek()。
-
-
Deque是一个线性collection,支持在两端插入和移除元素。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。
第一个元素 (头部) 第一个元素 (头部) 最后一个元素 (尾部) 最后一个元素 (尾部) 抛出异常 特殊值 抛出异常 特殊值 插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e) 删除 removeFirst() pollFirst() removeLast() pollLast() 检查 getFirst() peekFirst() getLast() peekLast() -
与Queue的联系
-
该接口扩展了Queue接口。
-
当deque被用作队列时,会产生FIFO (First-In-First-Out)行为。
-
在deque容器的末尾添加元素,并从开头删除元素。
也就是说Deque是Queue的子类,可以把它当作队列来使用。
但还记得上面的关键词吗?支持两端元素插入和移除的线性集合。
也就是Deque可以在两边都操作元素:新增、删除、访问元素等。
而Queque 只能在队首 对元素进行操作。
- 与堆栈的联系
- Deque也可以用作LIFO(后进先出)堆栈。
- 这个接口应该优先于旧的Stack类使用。
- 当一个队列被用作堆栈时,元素从队列的开始被推入和弹出。
- 堆栈方法完全等同于Deque方法。
- 所以以后需要使用Stack的情况时候,记得优先使用Deque。
-
与List的不同
与List接口不同,该接口不支持对元素的索引访问。 -
为什么不能插入null
因为实例后各个元素的默认值就是null。从而插入null的话会造成以下情况:下次在该位置插入元素时无法确定该位置是否存在元素。
-
Deque与堆栈的联系(详解)
Java中实际上提供了java.util.Stack来实现栈结构,但官方目前已不推荐使用,而是使用java.util.Deque双端队列来实现队列与栈的各种需求。如下图所示java.util.Deque的实现子类有java.util.LinkedList和java.util.ArrayDeque。顾名思义前者是基于链表,后者基于数据实现的双端队列。-
下表列出了Deque与Queue相对应的接口:
-
下表列出了Deque与Stack对应的接口:
两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(false或null)。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看。明白了这一点讲解起来就会非常简单。 -
双端队列(Deque),是Quene是一个子接口,双向队列是指该队列两端的元素既能入队(offer)也能出队(poll),如果将Deque限制为只能从一端入队(push)和出队(pop),则可限制栈的数据结构。对于栈而言,有入栈,遵循先进后出原则。
/* *add()\offer(e):将元素增加到队列的末尾,如果成功,返回true。 *remove()\poll():将元素从队列的队首删除。 *element()\peek():返回队首的元素,但不进行删除 *栈: *push(e):入栈,添加到队首 *pop(e):出栈,删除队首元素 *peek():返回栈首元素,但不进行删除 */ package com.edu.leetcode; import java.util.Deque; import java.util.LinkedList; public class DequeDemo { public static void dequeTest(){ Deque<String> deque = new LinkedList<String >(); deque.push("苹果"); deque.push("华为"); System.out.println("通过push(e)往队尾添加元素:"); System.out.println(deque); //获取栈首元素后,元素不会出栈 //peek()获取队首元素,不删除 String str = deque.peek(); System.out.println("获取队首元素peek()的返回值:"+str); System.out.println("通过peek()之后的:(只是获取,栈中还有这个元素):"+deque); //pop获取队首元素并删除 String pos = deque.pop(); System.out.println(pos); System.out.println("通过pop()之后的:(会把pop()的结果删掉):"+deque); //element获取队首元素,不删除 String ele = deque.element(); System.out.println("通过element()的返回值:"+ele); System.out.println("通过ele之后的栈:"+deque); //peek()获取队首元素,不删除 String peekRes = deque.peek(); System.out.println("通过pekk()的返回值:"+peekRes); System.out.println("通过peek()之后的栈:"+deque); } public static void main(String args[]){ DequeDemo.dequeTest(); } }
-
Quene是集合框架Collection的子接口,是一种常见的数据结构,Quene有一个直接子类PriorityQuene,队列Quene是一种常用的数据结构,可以将队列看作是一种特殊的线性表,该结构遵循的先进先出原则。Java中,LinkedList实现了Quene接口,因为LinkedList进行插入、删除操作效率较高。
//poll():将队首的元素删除,并返回该元素。 //peek():返回队首的元素,但不进行删除操作。 //offer():将元素添加到队尾,如果成功,则返回true。 package com.edu.leetcode; import java.util.*; public class QueneDemo { public static void testQuene(){ Queue<String> qu = new LinkedList<>(); qu.add("苹果"); qu.add("华为"); System.out.println("原始队列:"); System.out.println(qu); System.out.println("通过add往队尾添加元素:"); qu.add("OPPO"); System.out.println(qu); System.out.println("通过offer往队列尾添加元素:"); qu.offer("vivo"); System.out.println(qu); System.out.println("使用remove删除队列头元素:"); qu.remove(); System.out.println(qu); System.out.println("使用poll删除对列头元素:"); qu.poll(); System.out.println(qu); } public static void main(String[] args){ QueneDemo.testQuene(); } }
结果为
// An highlighted block 原始队列: [苹果, 华为] 通过add往队尾添加元素: [苹果, 华为, OPPO] 通过offer往队列尾添加元素: [苹果, 华为, OPPO, vivo] 使用remove删除队列头元素: [华为, OPPO, vivo] 使用poll删除对列头元素: [OPPO, vivo]
-
-
LinkedList
LinkedList
实现了Deque
接口,因此其具备双端队列的特性,由于其是链表结构,因此不像ArrayDeque
要考虑越界问题,容量问题,那么对应操作就很简单了,另外当需要使用栈和队列是官方推荐的是ArrayDeque
。 -
ArrayDeque
从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。
head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。
-
addFirst()
针对首端插入实际需要考虑:空间是否够用,以及下标是否越界的问题。上图中,如果head为0之后接着调用addFirst(),虽然空余空间还够用,但head为-1,下标越界了。下列代码很好的解决了这两个问题。
public void addFirst(E e) { if (e == null) throw new NullPointerException(); //下标越界问题解决方案 elements[head = (head - 1) & (elements.length - 1)] = e; //容量问题解决方案 if (head == tail) doubleCapacity(); }
空间问题是在插入之后解决的,因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。
下标越界的处理解决起来非常简单,head = (head - 1) & (elements.length - 1)就可以了,这段代码相当于取余,同时解决了head为负值的情况。因为elements.length必需是2的指数倍(构造函数初始化逻辑保证),elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用,如果head - 1为负数(其实只可能是-1),则相当于对其取相对于elements.length的补码。
扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
图中我们看到,复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素。
private void doubleCapacity() { assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; }
-
addLast()
addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由于tail总是指向下一个可以插入的空位,因此只需要elements[tail] = e;即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapacity()进行扩容。与first比较类似。
-
LeetCode [面试题 02.07. 链表相交]
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
-
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 -
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at ‘2’
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。 -
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
思路:
-
双指针法 参考链接
设「第一个公共节点」为 node ,「链表 headA」的节点数量为 a ,「链表 headB」的节点数量为 b ,「两链表的公共尾部」的节点数量为 c ,则有:
头节点 headA 到 node 前,共有 a - c 个节点;
头节点 headB 到 node 前,共有 b - c 个节点;考虑构建两个节点指针 A , B 分别指向两链表头节点 headA , headB ,做如下操作:
指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:
a + (b - c)指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:
b + (a - c)如下式所示,此时指针 A , B 重合,并有两种情况:
a + (b - c) = b + (a - c)
若两链表 有 公共尾部 (即 c > 0 ) :指针 A , B 同时指向「第一个公共节点」node 。
若两链表 无 公共尾部 (即 c = 0 ) :指针 A , B 同时指向 null 。
因此返回 A 即可。 -
哈希法
首先遍历链表headA,并将链表headA 中的每个节点加入哈希集合中。然后遍历链表headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:
如果当前节点不在哈希集合中,则继续遍历下一个节点;
如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
如果链表headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。
//双指针法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA;
ListNode B = headB;
while(A!= B){
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return B;
}
}
//哈希法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> node = new HashSet<ListNode>();
ListNode cur = headA;
while(cur != null){
node.add(cur);
cur = cur.next;
}
cur = headB;
while(cur != null){
if(node.contains(cur)){
return cur;
}
cur = cur.next;
}
return null;
}
}
LeetCode [142. 环形链表 II]
题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
-
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。 -
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。 -
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
思路:
-
哈希法
遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。
-
双指针法 参考链接
-
双指针第一次相遇: 设两指针
fast
,slow
指向链表头部head
,fast
每轮走 22 步,slow
每轮走 11 步;- 第一种结果:
fast
指针走过链表末端,说明链表无环,直接返回null
; 若有环,两指针一定会相遇。因为每走 1 轮,fast
与slow
的间距 +1,fast
终会追上slow
; - 第二种结果: 当
fast == slow
时, 两指针在环中 第一次相遇 。下面分析此时fast
与slow
走过的 步数关系 :- 设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点),链表环 有 b 个节点(这里需要注意,a 和 b 是未知数);两指针分别走了 f,s 步,则有:
-
fast
走的步数是slow
步数的 2 倍,即 f=2s;(解析:fast
每轮走 2 步) -
fast
比slow
多走了 n 个环的长度,即 f=s+n b;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时fast
比slow
多走 环的长度整数倍 );
-
- 以上两式相减得:f=2n b,s=n b,即
fast
和slow
指针分别走了 2n,n 个 环的周长 (注意: n 是未知数,不同链表的情况不同)。
- 设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点),链表环 有 b 个节点(这里需要注意,a 和 b 是未知数);两指针分别走了 f,s 步,则有:
- 第一种结果:
-
目前情况分析:
- 如果让指针从链表头部一直向前走并统计步数
k
,那么所有 走到链表入口节点时的步数 是:k=a+nb
(先走 a 步到入口节点,之后每绕 1 圈环( b 步)都会再次到入口节点)。 - 而目前,
slow
指针走过的步数为 n b 步。因此,我们只要想办法让slow
再走 a 步停下来,就可以到环的入口。 - 但是我们不知道 a 的值,该怎么办?依然是使用双指针法。我们构建一个指针,此指针需要有以下性质:此指针和
slow
一起向前走a
步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头部head
。
- 如果让指针从链表头部一直向前走并统计步数
-
双指针第二次相遇:
slow
指针 位置不变 ,将cur
指针 指向链表头部节点 ;slow
和cur
同时每轮向前走 1 步;此时 c=0,s = nb ;- 当
cur
指针走到c = a 步时,slow
指针走到步s = a+nb,此时 两指针重合,并同时指向链表环入口 。
//哈希法
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> nodeSet = new HashSet<ListNode>();
ListNode cur = head;
while(cur != null){
if(nodeSet.contains(cur)){
return cur;
}else{
nodeSet.add(cur);
}
cur = cur.next;
}
return null;
}
}
//双指针法
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(true){
if(fast == null ||fast.next == null){
return null;
}
fast = fast.next.next;
slow = slow.next;
if(slow == fast){
break;
}
}
ListNode cur = head;
while(slow != cur){
slow = slow.next;
cur = cur.next;
}
return slow;
}
}
总结
- 题量上去了以后确实还挺吃力的,最近还有笔试和面试,应接不暇
- 练习了链表的经典题目,学到了不少内容。特别是设计链表,这个还是需要再多练习练习的。刷了这些题目,一刷真的是细节到把各个知识点都进行了补充,还有集合类的内容没有补充完善,等这几天面试结束了再看吧。