代码随想录算法训练营第04天 | LeetCode 24. 两两交换链表中的节点,19.删除链表的倒数第N个节点,面试题 02.07. 链表相交,142.环形链表II

LeetCode [24. 两两交换链表中的节点]

题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

  • 示例 1:

    输入:head = [1,2,3,4]
    输出:[2,1,4,3]

  • 示例 2:

    输入:head = []
    输出:[]

  • 示例 3:

    输入:head = [1]
    输出:[1]

思路

  1. 虚拟头结点法

  2. 递归法

    递归解题首先要明确的是递推公式的含义,在这里递推公式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)常见用法

  • 递归
    关心的主要有三点:

    1. 返回值
    2. 调用单元做了什么
    3. 终止条件
  • new ListNode(0)常见用法

  1. 初始化一个空节点,没有赋值,指针指向为list
ListNode list = new ListNode();
  1. 初始化一个空节点,初始赋值为0,指针指向为list
ListNode list = new ListNode(0);
  1. 初始化一个空节点,初始赋值为0,并且list的下一个next指针指向head,指针指向为list
ListNode list = new ListNode(0,head);
  1. 定义一个空链表
ListNode list=null;
  1. 通常定义一个空节点还需要有节点的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]

思路

  1. 计算链表长度法:

    最开始的思路来源于移除链表元素,基本上核心思想一致,该题目删除的是倒数第n个,设链表元素个数为size,正数就是第size - n + 1个,但是如何判断是第几个,就需要计算链表的长度,因为没有类似数组的.length方法,所以需要自己来手写一个方法getSize()。

  2. 栈方法

    通过先进后出原则,先将链表元素压栈,弹出的第n个节点就是题目要求删除的节点。且此时的栈顶元素就是删除节点的前驱节点。

  3. 双指针法

    不预处理出链表的长度,以及使用常数空间的前提下解决本题。

    由于我们需要找到倒数第 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

    1. Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。
    • Deque有三种用途

      1. 普通队列(一端进另一端出)

        Queue queue = new LinkedList()
        

        or

        Deque deque = new LinkedList()
        
      2. 双端队列(两端都可进出)

        Deque deque = new LinkedList()
        
      3. 堆栈

        Deque deque = new LinkedList()
        

        Deque堆栈操作方法:push()、pop()、peek()。

    1. Deque是一个线性collection,支持在两端插入和移除元素。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。

      第一个元素 (头部)第一个元素 (头部)最后一个元素 (尾部)最后一个元素 (尾部)
      抛出异常特殊值抛出异常特殊值
      插入addFirst(e)offerFirst(e)addLast(e)offerLast(e)
      删除removeFirst()pollFirst()removeLast()pollLast()
      检查getFirst()peekFirst()getLast()peekLast()
    2. 与Queue的联系

    • 该接口扩展了Queue接口。

    • 当deque被用作队列时,会产生FIFO (First-In-First-Out)行为。

    • 在deque容器的末尾添加元素,并从开头删除元素。

      也就是说DequeQueue子类,可以把它当作队列来使用。
      但还记得上面的关键词吗?支持两端元素插入和移除的线性集合
      也就是Deque可以在两边都操作元素:新增、删除、访问元素等。
      而Queque 只能在队首 对元素进行操作。

    1. 与堆栈的联系
    • Deque也可以用作LIFO(后进先出)堆栈。
    • 这个接口应该优先于旧的Stack类使用。
    • 当一个队列被用作堆栈时,元素从队列的开始被推入和弹出。
    • 堆栈方法完全等同于Deque方法。
    • 所以以后需要使用Stack的情况时候,记得优先使用Deque
    1. 与List的不同
      与List接口不同,该接口不支持对元素的索引访问。

    2. 为什么不能插入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 。

思路

  1. 双指针法 参考链接

    设「第一个公共节点」为 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 即可。

  2. 哈希法

    首先遍历链表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
    解释:链表中没有环。

思路

  1. 哈希法

    遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。

  2. 双指针法 参考链接

  • 双指针第一次相遇: 设两指针 fastslow 指向链表头部 headfast 每轮走 22 步,slow 每轮走 11 步;

    1. 第一种结果: fast 指针走过链表末端,说明链表无环,直接返回 null; 若有环,两指针一定会相遇。因为每走 1 轮,fastslow 的间距 +1,fast 终会追上 slow
    2. 第二种结果:fast == slow时, 两指针在环中 第一次相遇 。下面分析此时fastslow走过的 步数关系
      • 设链表共有 a+b 个节点,其中 链表头部到链表入口a 个节点(不计链表入口节点),链表环b 个节点(这里需要注意,ab 是未知数);两指针分别走了 fs 步,则有:
        1. fast 走的步数是slow步数的 2 倍,即 f=2s;(解析: fast 每轮走 2 步)

        2. fastslow多走了 n 个环的长度,即 f=s+n b;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fastslow 多走 环的长度整数倍 );

      • 以上两式相减得:f=2n bs=n b,即fastslow 指针分别走了 2nn环的周长 (注意: n 是未知数,不同链表的情况不同)。
  • 目前情况分析:

    • 如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:k=a+nb(先走 a 步到入口节点,之后每绕 1 圈环( b 步)都会再次到入口节点)。
    • 而目前,slow 指针走过的步数为 n b 步。因此,我们只要想办法让 slow 再走 a 步停下来,就可以到环的入口。
    • 但是我们不知道 a 的值,该怎么办?依然是使用双指针法。我们构建一个指针,此指针需要有以下性质:此指针和slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头部head
  • 双指针第二次相遇:

    • slow指针 位置不变 ,将cur指针 指向链表头部节点slowcur同时每轮向前走 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;
    }
}

总结

  • 题量上去了以后确实还挺吃力的,最近还有笔试和面试,应接不暇
  • 练习了链表的经典题目,学到了不少内容。特别是设计链表,这个还是需要再多练习练习的。刷了这些题目,一刷真的是细节到把各个知识点都进行了补充,还有集合类的内容没有补充完善,等这几天面试结束了再看吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值