【代码随想录】链表篇 总结

本文围绕链表展开,介绍了链表理论基础,包括类型、存储方式、操作等。还针对多个链表相关题目,如移除元素、设计链表、反转链表等,给出思路和代码实现,涉及虚拟头结点、双指针法、递归法等方法,同时提及实现中遇到的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码随想录:https://www.programmercarl.com/

导图 - 链表篇

在这里插入图片描述


2.1 链表理论基础

  • 链表的类型:单、双、循环(可解决约瑟夫环问题)

  • 链表的存储方式:通过指针连接,散布在内存中

  • 链表的操作:增删改查

  • 链表的定义

    // C++
    // 单链表
    struct ListNode {
        int val;  // 节点上存储的元素
        ListNode *next;  // 指向下一个节点的指针
        ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
    };
    
    // 虚拟头结点:创建了一个val为0,next为NULL的结点
    ListNode* dummy_head = new ListNode(0);
    
    // 无虚拟头结点
    ListNode* head = NULL;
    
    # Python
    class ListNode:
        def __init__(self, val, next=None):
            self.val = val
            self.next = next
    
    # 虚拟头结点:创建了一个val为0,next为None的结点
    dummy_head = ListNode(0)
    
    # 无虚拟头结点
    head = None
    
  • 与数组对比
    在这里插入图片描述


2.2 203.移除链表元素

思路

  • 在链表中如何删除一个元素?(C/C++还要从内存中删除移除的节点,Java、Python不用手动管理内存)

    • 除头结点以外的其他结点的删除(C/C++还要从内存中删除移除的节点)
      在这里插入图片描述

    • 头结点的删除:只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点(C/C++还要将原头结点从内存中删除)
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

  • 如何处理头结点和其他结点删除操作不同的情况?

    1. 直接用原来的链表进行删除,只是头结点需要特殊处理

    2. 用虚拟头结点,统一删除操作:删除当前结点,找到其前一个结点即可

上面的图片均出自代码随想录(203.移除链表元素)

代码实现(这里只实现有虚拟头结点的代码)

无虚拟头结点的代码实现时要注意

  • 删除头结点时要用循环:因为头结点是不断更新的,即删掉一个,后面的结点自动成为新的头结点。新的头结点也需要特殊处理,所以干脆直接用循环
  • 删除其他结点时的循环条件为while (cur && cur->next):删完头结点后进入循环删除其他结点时,循环的条件不仅仅是cur->next != NULL,还要加一个cur != NULL,因为删除完头结点后,cur = head,但是head此时可能为空;
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 有虚拟头结点

        ListNode* dummy_head = new ListNode(0, head);
        ListNode* cur = dummy_head; 

        while (cur->next) {                     // 若头结点为空,cur->next为空,不进入循环;若cur为最后一个结点,一样
            if (cur->next->val == val) {        // 删除当前结点,要找到其前一个结点;cur指向要删除结点的前一个结点
                ListNode* temp = cur->next;     // 手动释放结点
                cur->next = cur->next->next;    // 删除操作
                delete temp;                    
            }
            else {
                cur = cur->next;
            }
        }
        head = dummy_head->next;                // 头结点head可能已被删除,直接用dummy_head->next表示新头结点即可
        delete dummy_head;
        return head;                
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)
  • 第1次写的时候,忘记手动释放内存了

遍历链表时为什么要定义一个current指针来遍历?or 为什么不用头结点来遍历?

因为大多数时候,我们操作完链表以后,要返回头结点
如果直接操作头结点或虚拟头结点的话,头结点的值都改了或删了,如何返回链表的头结点呢?
所以定义一个临时指针current,通过操作临时指针current来操作链表即可
最终返回 headdummy_head.next

虚拟头结点 小结

  • 作用:不用对头结点进行特殊判断,而可以采用统一的方式对每个结点进行操作。方便增、删操作
  • 初始化:创建一个val为0(即空、无意义),nextNULLNone(或指向头结点)的结点(普通结点的valnext都有意义)
  • 在链表的操作中,注意传入的结点是头结点还是虚拟头结点(自己瞎加的,应该不用太担心,一般都是头结点)

2.3 707.设计链表

思路

通过题目要求实现的操作发现,就是以下各个操作的集合

  1. 判断n的合法性(索引为n or 第n个):0表长sizen的关系,链表的操作不同,其关系也不同
  2. 删除cur的下一个结点:cur->next = cur->next->next(删除完后要size--
  3. cur后新增一个结点:(构造函数为LinkedNode(int val):val(val), next(nullptr){})
    ListNode* newNode = new ListNode(val);
    newNode->next = cur->next;
    cur->next = newNode
    (添加完结点后不要忘记size++
  4. 找到头结点、头结点的前一个结点、尾结点、尾结点的前一个结点
    • 头结点:headdummy_head->next
    • 头结点的前一个结点:dummy_head
    • 尾结点:while(cur->next) cur = cur->next
    • 尾结点的前一个结点:while(cur->next->next) cur = cur->next
  5. 找到第n个、下标为n的结点或其前一个结点(如何分别用forwhile循环实现,三步走
    ① 确定cur 起始位置结束位置 的下标
    (下标:统一从head为0开始算,dummy_head为-1)
    ② 确定 循环次数每次循环的跨度(即一次循环路过几个结点)
    cur结束位置下标 - cur起始位置下标 = 循环次数/每次循环的跨度
    如下图,2-(-1) = (3/1)在这里插入图片描述

代码实现(这里只给出了单链表,且带虚拟头结点的实现)

单链表

class MyLinkedList {
public:
    struct ListNode {
        int val;
        ListNode* next;
        ListNode(int x) : val(x), next(nullptr) {}
    };

    MyLinkedList() {
        // 初始化一个有虚拟头结点的单链表
        dummy_head = new ListNode(0);
        size = 0;
    }
    
    int get(int index) {
        // 获取链表中下标为 index 的节点的值
        // index的合法性判断 0-size-1
        if (index < 0 || index > size-1)
            return -1;

        ListNode* cur = dummy_head->next;

        // 起始0,终止index,循环次数 index
        for (int i = 0; i < index; i++) {
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        // 在表头插入值为val的结点
        ListNode* newNode = new ListNode(val);
        newNode->next = dummy_head->next;
        dummy_head->next = newNode;
        size++;
    }
    
    void addAtTail(int val) {
        // 在表尾插入值为val的结点
        ListNode* newNode = new ListNode(val);
        ListNode* cur = dummy_head;
        while (cur->next) {
            cur = cur->next;
        }
        cur->next = newNode;
        size++;
    }
    
    void addAtIndex(int index, int val) {
        // 在下标为index结点之前插入一个值为val的结点
        // index的合法性判断 0, size
        if (index < 0 || index > size)
            return;

        ListNode* cur = dummy_head;

        // 起始-1, 终止index-1, 循环次数index
        while (index) {
            cur = cur->next;
            index--;
        }

        ListNode* newNode = new ListNode(val);
        newNode->next = cur->next;
        cur->next = newNode;
        size++;
    }
    
    void deleteAtIndex(int index) {
        // 删除下标为index的结点,要找到其前一个结点
        // 判断index合法性 0,size-1
        if (index < 0 || index > size-1)
            return;
        
        ListNode* cur = dummy_head;

        // 起始-1,终止index-1,循环次数index
        while (index--) {               // 先index执行循环体,循环体执行完再--
            cur = cur->next;
        }
        ListNode* temp = cur->next;     // 手动释放内存
        cur->next = cur->next->next;
        delete temp;
        size--;
    }

private:
    ListNode* dummy_head;
    int size;
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */

 // 时间复杂度:在尾部删除和有index参数的为O(n),其他为O(1)
 // 空间复杂度:O(1)
遇到的问题
  • 单链表的定义,不太懂构造函数的语法
  • 初始化部分,要在最后加private,这里的语法也不太懂
  • 在添加或删除结点后总是忘记操作size变量,导致get(index)函数空指针异常
  • 申请一个新的结点的时候的语法为:ListNode* newNode = new ListNode(n),经常忘记new
  • 看代码最后的对象调用方法的语法不太懂 obj->get(index)
  • 删除结点操作,忘记手动释放内存

双链表

  • 双链表的实现主要是空间换时间,所以前一半数据和后一半数据的处理思路是不同的
  • 双链表似乎不是很需要虚拟头结点

2.4 206.反转链表

思路

只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表
curpre,挨个后移,依次把指针指向反转即可
代码随想录
![在这里插入图片描述](https://img-blog.csdnimg.cn/05dc4179f606483eaa3fef3750f66b63.png

代码实现:双指针法、递归法

双指针法

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 双指针法
        ListNode* cur = head;
        ListNode* pre = nullptr;

        while (cur) {
            ListNode* temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)

递归法

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverse(ListNode* pre, ListNode*cur) {
        // 递归三要素:1.确定参数和返回值;2.确定终止条件;3.确定单层递归逻辑
        // 1.参数和返回值
        // 2.确定终止条件
        if (cur == nullptr) 
            return pre;

        // 3.单层递归逻辑
        ListNode* temp = cur->next;
        cur->next = pre;
        return reverse(cur, temp);
    }

    ListNode* reverseList(ListNode* head) {
        // 递归法
        return reverse(nullptr, head);
    }
};
// 时间复杂度:O(n) 要递归处理链表的每个节点
// 空间复杂度:O(n) 递归调用了 n 层栈空间

还有一种从后往前的递归,见代码随想录参考代码(206.反转链表)


2.5 24.两两交换链表中的节点

思路

这种题一定要画图帮助理解

  • cur要指向要交换的两个结点之前的一个结点,然后进行交换结点的操作,循环往复,直至cur->nextcur->next->nextnullptr
  • 设虚拟头结点来交换第1、2个结点,更方便操作在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

图片均出自代码随想录(24.两两交换链表中的节点)

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummy_head = new ListNode(0, head);
        ListNode* cur = dummy_head;

        while (cur->next != nullptr && cur->next->next != nullptr) {    // 这里是&&不是||
            // cur->next == nullptr 说明cur后无结点需要交换了
            // cur->next->next == nullptr 说明cur后只剩一个结点,也无需交换

            // 假设cur所在结点编号为0,后面仨结点依次为1、2、3,最终交换完成顺序为,0213
            ListNode* temp1 = cur->next;                // 存一下1
            ListNode* temp2 = cur->next->next->next;    // 存一下3

            cur->next = cur->next->next;    // 步骤1
            cur->next->next = temp1;        // 步骤2
            temp1->next = temp2;            // 步骤3

            cur = temp1;                    // 交换后顺序为0213,cur移到1的位置,准备交换34
        }
        head = dummy_head->next;
        delete dummy_head;
        return head;
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)

2.6 19.删除链表的倒数第N个节点

思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fastslow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了

  • 本题可能需要删除头结点,所以设虚拟头结点来统一操作

  • fastslow都从虚拟头结点开始,fast先走n+1步,slow再走,这样fast==nullptr时,slow刚好指向要删除结点的前一个结点(例如,删除倒数第n个结点,即头结点,fast

    • fastn+1次循环,起始-1,终止size

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 设置头结点
        ListNode* dummy_head = new ListNode(0, head);
        ListNode* slow = dummy_head;
        ListNode* fast = dummy_head;

        // 可以再整个n的合法性判断
        for (int i = 0; i < n+1; i++) {
            fast = fast->next;
        }
        while (fast != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        ListNode* temp = slow->next;
        slow->next = slow->next->next;
        delete temp;

        head = dummy_head->next;
        delete dummy_head;
        return head;
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)

2.7 面试题 02.07.链表相交

160.链表相交

视频参考:睡不醒的鲤鱼 (强烈推荐!)

思路

  1. 尾部对齐,同步后移判等(代码随想录 的思路)
  2. 数学关系,遍历两次判等(睡不醒的鲤鱼 的思路,代码更简洁,直观)
    图片截图自

    图片截图自 睡不醒的鲤鱼

代码实现

尾部对齐,同步后移判等

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 尾部对齐,找相交
        int sizeA = 0, sizeB = 0, mov = 0;  // mov是记录较长的那个链表的curA后移长度
        ListNode* curA = headA;
        ListNode* curB = headB;

        // 求表A和表B的表长:起始0,终止size,循环次数sizeA,即表长
        while (curA) {
            curA = curA->next;
            sizeA++;
        }
        while (curB) {
            curB = curB->next;
            sizeB++;
        }

        // 求sizeA和sizeB的差值mov,是差的结点个数,只要长的一方后移mov次,尾部就能对齐
        if (sizeA > sizeB) {        // A长,移curA
            mov = sizeA - sizeB;
            curA = headA;
            while (mov--) {
                curA = curA->next;
            }
            curB = headB;
        }
        else if (sizeA < sizeB) {   // B长,移curB
            mov = sizeB - sizeA;
            curB = headB;
            while (mov--) {
                curB = curB->next;
            }
            curA = headA;
        }
        else {                      // 一样长,不用移动
            curA = headA;
            curB = headB;
        }

        // curA和curB若最后指向nullptr,是同步的,所以只用判断一个即可
        while (curA != nullptr && curA != curB) {
            curA = curA->next;
            curB = curB->next;
        }

        // 若为空,说明没有相交结点;不为空,curA、curB指的是同一结点,随便返回一个
        if (curA == nullptr)
            return nullptr;
        else
            return curA;
    }
};
// 时间复杂度:O(m+n)  两个链表,各遍历了两次
// 空间复杂度:O(1)

以上是我自己写的,代码随想录参考代码更简洁一些些

  • 最后的循环不用有 && curA != curB
  • 并且可以直接return curA 因为curA和curB无论是不是nullptr都是同步的

但是下面这个方法更简洁

数学关系,遍历两次判等

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        // 数学关系,同步后移判等
        ListNode* curA = headA;
        ListNode* curB = headB;

        while (curA != curB) {
            curA = curA == nullptr ? headB : curA->next;    //     max= a>b ? a : b;
            curB = curB == nullptr ? headA : curB->next;
        }
        return curA;    // 最后curA,curB要么一起指向同一个结点,要么一起指向nullptr,一定是同步的
    }
};
// 时间复杂度:O(m+n)  最坏情况,两个链表,各遍历了两次,也没找到
// 空间复杂度:O(1)


2.8 142.环形链表Ⅱ

同样推荐视频:睡不醒的鲤鱼

思路:双指针法

假设slow每次走1步,fast每次走2步
若有环,fast先入环,slow后入环,最终在一点相遇,分析各自的路程得下图和等式

  • fastx+y+n(y+z)
  • slow2(x+y);因为fast的速度是slow的2倍

化简等式后,得 slow从表头出发,fast从相遇结点出发,都以1步的速度出发,将在环的入口结点相遇
最终得到等式x=z
在这里插入图片描述

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 双指针法
        ListNode* slow = head;
        ListNode* fast = head;

        // 先找到相遇结点
        while (fast && fast->next) {    // fast head为空,直接退出;fast->next为空,则无环
            fast = fast->next->next;    // fast走两步
            slow = slow->next;          // slow走一步

            if (fast == slow) {         // 此时fast处于相遇结点上,fast从相遇结点出发
                slow = head;            // slow从表头出发

                while (slow != fast) {  // 本循环结束,即找到环的入口结点
                    slow = slow->next;      
                    fast = fast->next;
                }
                return slow;            
            }
        }
        return nullptr;                 // 没找到返回空
    }
};
// 时间复杂度:O(n)  slow在找相遇结点和环的入口结点的时都不会超过链表长度n,所以总体为O(n)
// 空间复杂度:O(1)

遇到的问题

在找环的入口结点时,slowfast要以相同的速度前进,而不是原先的速度

我第1次写的时候是以原本的速度,slow1,fast2,我模拟了一下,这样只会让它们依然在相遇结点再次相遇,而不是在环的入口结点相遇

从路程上分析也是,slow仍然走的是x+y的距离,fast仍然走的是z+n(y+z)的距离,和他俩从起点出发得到的等式是一样的,所以只会在相遇结点再次相遇

所以只有slowfast要以相同的速度前进,才能找到环的入口结点

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值