代码随想录——链表

目录

一、链表理论基础

1、链表的定义、类型

2、链表的存储方式

3、链表的定义代码

4、链表的操作

二、移除链表元素

1、题目:203. 移除链表元素 - 力扣(LeetCode)

2、思路

1)移除的不是头结点

2)移除的是头结点

方法1:直接使用原来的链表进行删除操作。

方法2:设置一个虚拟头结点进行删除操作——可以把头结点和中间结点一起处理。

3、代码

1)使用原链表

2)添加虚拟头结点

4、复杂度分析

三、设计链表

1、题目:707. 设计链表 - 力扣(LeetCode)

2、思路

3、代码

1)单链表

2)拓展:双链表

4、复杂度分析

四、反转链表

1、题目:206. 反转链表 - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

五、两两交换链表中的结点

1、题目:24. 两两交换链表中的节点 - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

六、删除链表的倒数第N个节点

1、题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

七、链表相交

1、题目:面试题 02.07. 链表相交 - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析

八、环形链表2

1、题目:142. 环形链表 II - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析


一、链表理论基础

1、链表的定义、类型

  • 定义:链表是一种通过指针串联在一起的线性结构(这里的指针只是一个概念,跟JAVA里面有没有指针无关)
  • 每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针)。
  • 有几种类型的链表:
  • 1、单链表:如下图。

  • 2、双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点
    • 既可以向前查询,也可以向后查询。

  • 3、循环链表:顾名思义,就是链表首尾相连。

2、链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

  • 链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

3、链表的定义代码

public class ListNode {  // 这个类有下面3个方法
    int val;   // 结点的值
    ListNode next;  // // 下一个结点

    // 节点的构造函数(无参)
    public ListNode() {
    }
    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }
    // 节点的构造函数(有两个参数) (当前节点的值、下一个节点),一般是这个方法
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

4、链表的操作

  • 1、删除节点:只要将C节点的next指针 指向E节点就可以了。(JAVA有自己的内存回收机制,可以释放节点D的内存)

  • 2、添加节点:

  • 性能分析:
    • 插入和删除:数组的时间复杂度是O(n),链表是O(1)(链表增删的时候,不需要移动其他元素,只需要修改指针。因为内存也不是连续的)
    • 查询:数组的时间复杂度是O(1),链表是O(n)  (相当于数组知道索引可以直接访问,但是链表没有索引,每次都要重新开始)

二、移除链表元素

视频课:手把手带你学会操作链表 | LeetCode:203.移除链表元素_哔哩哔哩_bilibili

1、题目:203. 移除链表元素 - 力扣(LeetCode)

2、思路

1)移除的不是头结点

直接删除,next重新链接就好了。

2)移除的是头结点

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。

方法1:直接使用原来的链表进行删除操作。

只用将头结点向后移动一位即可。(然后java会自动释放内存,但是c++里面还要手动将原头结点从内存中删掉。)

但是这样的话,对头结点要单独处理,不能和普通结点统一处理。

方法2:设置一个虚拟头结点进行删除操作——可以把头结点和中间结点一起处理。

给链表添加一个虚拟头结点为新的头结点。在程序最后别忘了还要指回新的头结点。

3、代码

注意:链表用ListNode head来表示,也就是仅仅用一个头结点来进行索引,表示整个链表

1)使用原链表

public ListNode removeElements(ListNode head, int val) { // 只需要直到头结点,即可表示整个链表
    while (head != null && head.val == val) {
        head = head.next;  
        //当头结点的数值就是目标值,就用.next指向后一个结点(直接更改head头结点的位置)
        // 这是一个循环,直到头结点不再为目标值为止
    }
    // 已经为null,提前退出
    if (head == null) {
        return head;
    }
    // 已确定当前head.val != val
    ListNode pre = head;  // pre表示上一结点(比较数值结点的上一个)
    ListNode cur = head.next;  // 当前比较结点
    while (cur != null) {
        if (cur.val == val) {  // 但凡head.next下一结点等于目标值
            pre.next = cur.next;  // 直接上一结点的next指向再往后一个结点
        } else { 
            pre = cur;  // 如果不是目标值,就把当前结点置为pre
        }  
        cur = cur.next;  // 比较结点都要往后移
    }
    return head;  // 返回这个链表head
}

2)添加虚拟头结点

用统一的规则来删除,这样代码简单一些(虽然时间复杂度是一样的)

要删元素,就要定位到该元素的前一个结点。

public ListNode removeElements(ListNode head, int val) {
    if (head == null) {
        return head;  // 如果头结点都为null,表示整个链表为空,直接返回就好
    }
    // 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
    ListNode dummy = new ListNode(-1, head);  //在原始head链表前加一个-1虚拟头结点
    ListNode pre = dummy;  // 初始把虚拟结点设为pre
    ListNode cur = head;  // 把head设为cur  然后就可以直接比较,不用把head单拎出来了
    while (cur != null) {
        if (cur.val == val) {
            pre.next = cur.next;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return dummy.next;  // 注意,此处返回的是虚拟头结点的next,就是真正的头结点
    // 为什么不return head,因为head可能已经被删掉了
}

4、复杂度分析

  • 使用原来的链表:时间复杂度: O(n);空间复杂度: O(1)。
  • 使用虚拟头结点:时间复杂度: O(n);空间复杂度: O(1)。

三、设计链表

视频课:帮你把链表操作学个通透!LeetCode:707.设计链表_哔哩哔哩_bilibili

1、题目:707. 设计链表 - 力扣(LeetCode)

设计5个方法:实现对单链表结点的增删改查。

2、思路

这道题目设计链表的五个函数

统一采用虚拟头结点的方式!!

  • 获取链表第index个节点的数值
    • index是从0开始的,所以如果index=0,就是要获取头结点。(0 ~ size-1)
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
    • current.next为空了,就证明current指向了尾部结点
  • 在链表第index个节点前面插入一个节点
    • 找到前一个结点,所以其实current.next才是第index个结点。
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目。

3、代码

1)单链表

其实链表的宗旨,就是要操作哪个结点,都先定位到该结点的前一个结点。

其实增删改查操作都不难。主要是要清晰链表是怎么一个个链接起来的,并且在写代码的时候要清晰index对应到什么位置,才好写for循环。然后一般知道了index,都是会索引到操作节点的前一个。

下面的代码中的定义了一个虚拟头结点的。

// 这是定义了结点的class
class ListNode {
    int val;  // 结点有一个值val
    ListNode next;  // 结点还有一个next结点
    ListNode(){}
    ListNode(int val) {
        this.val=val;  // 可以用xx.val得到结点值
    }
}
// 下面是一个包含这5个方法的一个链表类
class MyLinkedList {   
    int size;  //size存储链表元素的个数
    ListNode head;  //虚拟头结点

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);   // 这定义的是一个虚拟头结点
        // head 被初始化为一个新的 ListNode 对象,其值为0,这表明它是一个虚拟节点
    }

    // 1)获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        if (index < 0 || index >= size) { //如果index非法,返回-1
            return -1;
        }
        ListNode currentNode = head;    
        for (int i = 0; i <= index; i++) {  // 包含一个虚拟头节点,所以查找第 index+1 个节点
            currentNode = currentNode.next; // 一直向后遍历,得到index+1位置的元素
        }
        return currentNode.val;  //返回目前索引结点的值即可
    }

    //在链表最前面插入一个节点,等价于在第0个元素前添加(可以用方法4的函数)
    public void addAtHead(int val) {
        addAtIndex(0, val);  
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加(可以用方法4的函数)
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    
    public void addAtIndex(int index, int val) {
        if (index > size) {   
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;  // 都是增加了1个结点,链表长度+1
        //找到要插入节点的前驱
        ListNode pred = head;  // 从虚拟头结点开始往后索引
        for (int i = 0; i < index; i++) {
            pred = pred.next;  // 一直索引到index的前一个
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;  // 然后直接把toAdd结点插入进去就行
    }

    //删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        if (index == 0) {
            head = head.next;  // 相当于删除头结点
	    return;
        }
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;   // 索引到要删除的结点前面一个
        }
        pred.next = pred.next.next;  //建立新的链接
    }
}

2)拓展:双链表

class ListNode{
    int val;
    ListNode next,prev;  // 这是双链表
    ListNode() {};
    ListNode(int val){
        this.val = val;
    }
}


class MyLinkedList {  
    int size;    //记录链表中元素的数量
    ListNode head,tail;   //记录链表的虚拟头结点和虚拟尾结点
    
    public MyLinkedList() {
        //初始化操作
        this.size = 0;
        this.head = new ListNode(0);
        this.tail = new ListNode(0);   // 这里相当于设计了一个虚拟头结点和尾结点
        //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
        head.next=tail;
        tail.prev=head;  // 头尾相连(实际上是实现了循环链表)
    }
    
    // 索引查找方法
    public int get(int index) {  
        //判断index是否有效
        if(index<0 || index>=size){
            return -1;
        }
        ListNode cur = this.head;  // 
        //判断是哪一边遍历时间更短
        if(index >= size / 2){
            //tail开始
            cur = tail;  //从尾巴开始向前一个个遍历
            for(int i=0; i< size-index; i++){
                cur = cur.prev;  // 一个个向前索引
            }
        }else{
            for(int i=0; i<= index; i++){
                cur = cur.next;  //注意是有虚拟头节点/尾结点的,所以都要比实际多索引一个
            }
        }
        return cur.val;
    }
    
    public void addAtHead(int val) {
        //等价于在第0个元素前添加
        addAtIndex(0,val);
    }
    
    public void addAtTail(int val) {
        //等价于在最后一个元素(null)前添加
        addAtIndex(size,val);
    }
    
    // 插入元素(在index的前面插入)
    public void addAtIndex(int index, int val) {
        if(index>size){
            return;    //index大于链表长度
        }
        if(index<0){
            index = 0;
        }
        size++;
        //目标是找到插入点的前驱
        ListNode pre = this.head;  // 这时候还是虚拟头节点
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        //新建结点
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;   //后一个结点的pre要改变
        newNode.prev = pre;  //当前结点的next和pre要链接
        pre.next = newNode;  //前一个结点的next要改变
        
    }

    // 删除结点
    public void deleteAtIndex(int index) {
        //判断索引是否有效
        if(index<0 || index>=size){
            return;
        }
        //删除操作
        size--;
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;  // 这是找到删除结点的前一个
        }
        pre.next.next.prev = pre;  // 删除节点的后一结点的pre要改变
        pre.next = pre.next.next;  // 删除节点的前一个结点的next要改变
    }
}

4、复杂度分析

  • 时间复杂度: 涉及 index 的相关操作为 O(index), 其余为 O(1)
  • 空间复杂度: O(n)

四、反转链表

1、题目:206. 反转链表 - 力扣(LeetCode)

反转一个单链表,输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

2、思路

视频课:帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法_哔哩哔哩_bilibili

如果再定义一个新的链表来一个个遍历建立连接,会很浪费空间。其实就是要改变原来这个链表的所有指针的指向,原本指向next,现在指向pre。然后尾部结点变成头结点。

  1. 双指针法:定义prev和cur指针,表示前一结点(cur修改后应链接的)和当前结点(应修改.next的指针)。所以prev初始是一个空,cur初始为头结点。然后两个指针一起向后移动,一个个修改链接方向。最后返回的是尾结点,也就是prev所在位置。

  1. 递归法:其实和双指针法是一样的逻辑,稍微抽象一点。其实是用递归来代替循环(方法中传入prev,cur,然后每次修改链接方向完成后,应该是要向前移动prev和cur指针。这里就直接递归调用方法,传入(cur,temp),达到了令prev=cur;cur=temp的作用)

3、代码

1)双指针法

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null; // prev初始为空
        ListNode cur = head;  // cur初始在头结点
        ListNode temp = null; 
        while (cur != null) {  // 但凡当前结点不为空,就要反转链接prev
            temp = cur.next;  // 保存下一个节点(因为下一步修改cur.next后就无法指向下一个节点了)
            cur.next = prev;  // 修改当前cur指针的链接方向,链接前一个结点pre
            prev = cur; // prev前移
            cur = temp;  // cur前移
        }
        return prev;  // 此时prev在尾部结点,也就是新链表的head
    }
}
// 先保存下一个结点,再修改当前节点指向,再移动pre和cur位置。

2)递归法

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转
        // 其实上面这些步骤和双指针方法都是一样的
        return reverse(cur, temp);   // 其实这里递归的目的是更新prev、cur位置
        // 也就是实现了prev = cur; cur = temp;的效果  (这里递归也就相当于循环了)
    }
}

4、复杂度分析

两种方法复杂度一样

  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间

五、两两交换链表中的结点

1、题目:24. 两两交换链表中的节点 - 力扣(LeetCode)

给一个单链表。交换两个相邻结点的位置(如果是奇数个结点,最后一个就不处理)。

2、思路

视频课:帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点_哔哩哔哩_bilibili

仍然使用虚拟头结点的方法,不然还是不好对head结点进行操作。

步骤1:cur指向结点2,也就是把cur指向要右交换节点。

步骤2:结点2指向结点1,也就是右交换点指向左交换点。

步骤3:结点1指向结点3,也就是左交换点指向两个交换点的下一个。

每次都让cur处于要交换的两个结点的前一个结点

  • 遍历终止条件:如果有奇数个结点,那么遍历到cur是倒数第二个就行了,也就是cur.next.next为空的时候就停止,如果是偶数个结点,那么遍历到cur在尾结点,即cur.next为空就行了。所以要保证这两个都不为空

3、代码

//按照前面所讲的步骤,一步步来
class Solution {
  public ListNode swapPairs(ListNode head) {
        ListNode dumyhead = new ListNode(-1); // 设置一个虚拟头结点
        dumyhead.next = head; // 初始虚拟头结点指向head
        ListNode cur = dumyhead;  // cur初始在虚拟头
        ListNode temp; // 临时节点,保存两个节点后面的节点
        ListNode firstnode; // 临时节点,保存交换的两个节点之中的第一个节点
        ListNode secondnode; // 临时节点,保存交换的两个节点之中的第二个节点
        while (cur.next != null && cur.next.next != null) {  // 确保两个交换的结点不为空
            temp = cur.next.next.next;  // 两个交换结点的下一个,最开始对应结点3
            firstnode = cur.next;  // 交换的左边结点
            secondnode = cur.next.next;  // 交换的右边结点
            // 下面开启正式的交换操作(相当于cur要定位到交换结点的前一个)
            cur.next = secondnode;       // 步骤一:cur指向右交换点(cur指向结点2)
            secondnode.next = firstnode; // 步骤二:右交换点指向左交换点(结点2指向结点1)
            firstnode.next = temp;      // 步骤三:左交换点指向两个交换的下一个点(结点1指向结点)
            // 然后就移动cur的位置,要移动到下一次交换的前一个,也就是交换后的右边这个点
            cur = firstnode; // cur移动,准备下一轮交换
        }
        return dumyhead.next;  // 最后返回虚拟头结点的下一个点,也就是原始的head
    }
}

4、复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

六、删除链表的倒数第N个节点

1、题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

2、思路

视频课:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili

步骤1:首先fast和slow都指向虚拟头结点。然后fast向后移动n+1步。(让fast和slow之间隔n个)

步骤2:fast和slow同时向后移动,直到fast指向null。

步骤2:此时slow已经指向待删除结点的前一个了,直接修改next进行删除即可。(其实目标就是让指针指向操作结点的前一个结点)

3、代码

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode = new ListNode(0);
        dummyNode.next = head;  //新建一个虚拟头节点指向head
        // 初始快慢指针指向虚拟头节点
        ListNode fastIndex = dummyNode;
        ListNode slowIndex = dummyNode;
        // 只要快慢指针相差 n 个结点即可
        for (int i = 0; i <= n; i++) {
            fastIndex = fastIndex.next;  // fast向后移动n+1个
        }
        // 然后同时向后移动fast和slow,直到fast指向null
        while (fastIndex != null) {
            fastIndex = fastIndex.next;
            slowIndex = slowIndex.next;
        }
        // 此时 slowIndex 的位置就是待删除元素的前一个位置。
        if (slowIndex.next != null) {  // 检查 slowIndex.next 是否为 null,以避免空指针异常
            slowIndex.next = slowIndex.next.next;  // 直接删除
        }
        return dummyNode.next;  // 返回head
    }
}

4、复杂度分析

  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

七、链表相交

1、题目:面试题 02.07. 链表相交 - 力扣(LeetCode)

给出listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3。(A链表相交前有2个结点)

输出Intersected at '8'

注意:这里的相交不是数值相等,而是指针相等。

2、思路

视频课:无

步骤1:curA和curB首先都指向head。然后计算两个链表的长度差值n,让curA向后移动n步,这样curA和curB就对齐了。

步骤2:再一次查看curA和curB是否相同,不相等则同时向右移动

3、代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA;  // 题目是给定了headA和headB的
        ListNode curB = headB;  // 初始curA和curB都是指向头结点的
        int lenA = 0, lenB = 0;
        while (curA != null) { // 求链表A的长度
            lenA++;
            curA = curA.next;
        }
        while (curB != null) { // 求链表B的长度
            lenB++;
            curB = curB.next;
        }
        curA = headA;  // 再重新指回头结点
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            int tmpLen = lenA; 
            lenA = lenB;
            lenB = tmpLen;  // 交换lenA和lenB
            ListNode tmpNode = curA;
            curA = curB;
            curB = tmpNode;  // 交换curA和curB
        }
        // 求长度差
        int gap = lenA - lenB;  // 保证了A肯定比B长
        while (gap-- > 0) {
            curA = curA.next;   // 向右移动curA,直到curA和curB在同一起点上
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != null) {  // 因为curA和curB位置相同,所以要是为null就都为,只用判断一个
            if (curA == curB) {
                return curA;  // 返回指针相同的结点
            }
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }
}

4、复杂度分析

  • 时间复杂度:O(n + m)
  • 空间复杂度:O(1)

八、环形链表2

1、题目:142. 环形链表 II - 力扣(LeetCode)

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

需要输出:pos。用从0开始的整数pos来表示环结束的位置,也就是链尾指向链中的地方)比如下面这个环,pos=1。

2、思路

视频课:把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili

1)首先判断有没有环

从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

因为fast移动2步、slow移动1步,这样相当于如果有环fast每次都更靠近1步slow,即在追赶

2)找环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。需要计算出x的表达式:

  • slow指针走过的节点数为: x+y, fast指针走过的节点数:x+y+n(y+z).
    • 即fast走了n小圈后遇到slow
  • fast指针走过的节点数 = slow指针走过的节点数 * 2:(x+y)*2 = x+y+n(y+z).
    • 化简这个式子后:x = n(y+z)-y = (n-1)(y+z)+z
    • n=1的时候,x=z。其实就是x的长度等于几个小圈,加上一个多余的z。

可见这道题主要是看推理能力。

其实x=(n-1)(y+z)+z。。在代码中,就是一个结点从头结点开始,一个结点从相遇节点开始。两个结点分别向后移动,直到相遇,相遇的位置就刚好是环开始的位置。

3、代码

代码的步骤就是先定义fast和slow结点判断有没有环。如果有环了,也就知道了相遇结点位置。再同时后移头结点和相遇结点,如果碰上了,那么此位置就刚好是环初始的位置。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;  //初始fast和slow都指向head
        while (fast != null && fast.next != null) {
            slow = slow.next;  // slow每次移动1步
            fast = fast.next.next;   // fast每次移动2步
            if (slow == fast) {  // 判断是否有环?但凡能相遇,就是有环
                ListNode index1 = fast;  // index1是相遇的结点
                ListNode index2 = head;  // index2是头结点
                // 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
                while (index1 != index2) {
                    index1 = index1.next;  // 也就是求X的过程  x= 几个小圈+z
                    index2 = index2.next;
                }
                return index1;
            }
        }
        return null;
    }
}

4、复杂度分析

  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n。
  • 空间复杂度: O(1)。
  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值