快速复习之数据结构篇——链表

链表

学习链表及以后的数据结构,最重要的就是 " 学会画图 !!!!"

3.1 链表的概念及结构

概念:链表是一种**物理存储结构上非连续、**非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

3.2 链表的分类

单向或双向+带头或不带头+循环或不循环,两两组合,共八种。

常用两种:无头单向不循环(单链表),带头双向循环(双链表)。

3.3 链表的实现

双向链表的实现

注意**:pre和next指针的修改顺序。**

3.4 链表面试题⭐️⭐️

1.移除链表元素

删除链表中等于给定值 val 的所有结点。

203. 移除链表元素 - 力扣(LeetCode)https://leetcode.cn/problems/remove-linked-list-elements/description/

本题的难点在于对于第一个元素就是val值的时候,pre指针会存在空指针解引用,所以,要加一个哨兵节点

cpp 复制代码
/**
 * 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) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    // 移除链表中所有值为val的节点,返回新的头节点
    ListNode* removeElements(ListNode* head, int val) {
        // 空链表直接返回
        if(head == NULL)
            return NULL;
      
        listnode* cur = head;       // cur:遍历链表的当前节点
        // 创建虚拟头节点pre,统一处理「头节点删除」和「中间节点删除」的逻辑
        listnode* pre = (listnode*)malloc(sizeof(listnode));
        pre->next = head;
        listnode* ret = pre->next;  // ret:最终要返回的新头节点
        
        // 先跳过所有值为val的头节点,找到第一个非val的节点作为新头
        while(ret != NULL && ret->val == val)
        {
            ret = ret->next;
        }
        
        // 遍历链表,删除所有值为val的节点
        while(cur != NULL)
        {
            if(cur->val == val)
            {
                // 当前节点值为val:让pre的next跳过cur,直接指向cur的下一个节点
                pre->next = cur->next;
                cur = pre->next;  // cur移动到下一个节点,继续判断
            }
            else 
            {
                // 当前节点不是val:pre和cur一起后移,继续遍历
                pre = cur;
                cur = cur->next;
            }
        }
        return ret;
    }
};

2. 反转单链表⭐️(可以说是一个模板)

206. 反转链表 - 力扣(LeetCode)https://leetcode.cn/problems/reverse-linked-list/description/

三指针法。非常巧妙的一种方法。

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode listnode;

struct ListNode* reverseList(struct ListNode* head) {
    if(head == NULL)
        return NULL; // 空链表直接返回

    // 三指针迭代反转:n1前驱、n2当前、n3后继(防止断链)
    listnode* n1 = NULL;    // 前驱节点,初始为NULL(反转后当前节点的next要指向它)
    listnode* n2 = head;    // 当前正在处理的节点,初始为头节点
    listnode* n3 = n2->next;// 后继节点,提前保存,避免修改n2->next后断链丢失下一个节点

    n2->next = n1; // 处理第一个节点:反转当前节点的指向

    while(n3 != NULL) // 还有后继节点时继续处理
    {
        n1 = n2;        // 前驱指针后移到当前节点
        n2 = n3;        // 当前节点后移到下一个节点
        n3 = n3->next;  // 后继指针后移,保存下一个待处理节点
        n2->next = n1;  // 反转当前节点的指向
    }

    return n2; // 循环结束后,n2指向原链表尾节点,即反转后的新头节点
}

3.链表的中间结点

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个 中间结点。

876. 链表的中间结点 - 力扣(LeetCode)https://leetcode.cn/problems/middle-of-the-linked-list/description/

快慢指针的一个算法。

cpp 复制代码
/**
 * 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) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        // 快慢指针法:快指针速度是慢指针的2倍,快指针到尾时慢指针刚好在中间
        listnode* fast = head;  // 快指针:每次走2步
        listnode* slow = head;  // 慢指针:每次走1步
        
        // 循环条件:fast和fast->next不为空,避免访问空指针,兼容奇偶长度链表
        while (fast && fast->next)
        {
            slow = slow->next;          // 慢指针走1步
            fast = fast->next;          // 快指针先走第1步
            if (fast) fast = fast->next;// 快指针再走第2步(凑够2步,避免空指针)
        }
        return slow; // 循环结束时,slow指向中间结点(偶数长度时返回第二个中间结点)
    }
};

4.返回倒数第 k 个节点

输入一个链表,输出该链表中倒数第k个结点。

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/description/

上一题的变式题。两个指针间隔k即可

cpp 复制代码
/**
 * 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) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        // 快慢指针法:快指针速度是慢指针的2倍,快指针到尾时慢指针刚好在中间
        listnode* fast = head;  // 快指针:每次走2步
        listnode* slow = head;  // 慢指针:每次走1步
        
        // 循环条件:fast和fast->next不为空,避免访问空指针,兼容奇偶长度链表
        while (fast && fast->next)
        {
            slow = slow->next;          // 慢指针走1步
            fast = fast->next;          // 快指针先走第1步
            if (fast) fast = fast->next;// 快指针再走第2步(凑够2步,避免空指针)
        }
        return slow; // 循环结束时,slow指向中间结点(偶数长度时返回第二个中间结点)
    }
};

5.合并两个有序链表

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成 的。

21. 合并两个有序链表 - 力扣(LeetCode)https://leetcode.cn/problems/merge-two-sorted-lists/description/

哨兵节点+注意提前判空

cpp 复制代码
/**
 * 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) {}
 * };
 */
typedef struct ListNode listnode;

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        // 边界处理:若一个链表为空,直接返回另一个
        if(list1 == NULL)
            return list2;
        if(list2 == NULL)
            return list1;

        listnode* cur1 = list1; // 遍历list1的指针
        listnode* cur2 = list2; // 遍历list2的指针
        // 虚拟头节点:统一处理链表头,避免单独判断第一个节点
        listnode* head = (listnode*)malloc(sizeof(listnode));
        listnode* cur = head;   // 新链表的当前节点指针,用于拼接

        // 双指针遍历,按升序拼接节点
        while(cur1 && cur2)
        {
            if(cur1->val < cur2->val)
            {
                cur->next = cur1;   // 把较小节点接到新链表
                cur = cur->next;    // 新链表指针后移
                cur1 = cur1->next;  // list1指针后移
            }else
            {
                cur->next = cur2;   // 把较小节点接到新链表
                cur2 = cur2->next;  // list2指针后移
                cur = cur->next;    // 新链表指针后移
            }
        }

        // 拼接剩余节点(未遍历完的链表直接接上即可)
        if(cur1) cur->next = cur1;
        if(cur2) cur->next = cur2;

        return head->next; // 返回虚拟头的下一个节点,即新链表的头
    }
};

6.链表分割

编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。

思路:分成两个链表,大链表和小链表。连接两个链表。

注意!!!大链表的最后一个指针要置空!!!(太阴了)

cpp 复制代码
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
typedef struct ListNode listnode;

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // 双虚拟头节点法:将链表分为「小于x」和「大于等于x」两部分,再拼接
        listnode* small = (listnode*)malloc(sizeof(listnode)); // 小于x的分区虚拟头
        listnode* big = (listnode*)malloc(sizeof(listnode));   // 大于等于x的分区虚拟头
        listnode* smallhead = small; // 保存小分区的虚拟头,用于最后返回
        listnode* bighead = big;     // 保存大分区的虚拟头,用于拼接
        listnode* cur = pHead;       // 遍历原链表的指针

        while(cur)
        {
            if(cur->val < x)
            {
                small->next = cur;    // 接到小分区链表
                cur = cur->next;
                small = small->next;  // 小分区指针后移
            } else {
                big->next = cur;      // 接到大分区链表
                big = big->next;      // 大分区指针后移
                cur = cur->next;
            }
        }

        big->next = NULL;             // 大分区尾节点置空,避免残留指针成环
        small->next = bighead->next;  // 小分区尾 拼接 大分区的有效头
        return smallhead->next;       // 返回新链表的头(跳过虚拟头)
    }
};

7. 链表的回文结构⭐️⭐️(基础题的大综合)

链表的回文结构_牛客题霸_牛客网https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-ranking

寻找中间节点+链表翻转

思路:把这个链表从中间分开成两个链表,翻转第二个链表,判断两个链表是否一样。

注意:分开两个链表后,两个链表可以不断开,因为,我们判断两个链表是否一样时,短的那个结束了,就判断结束了。

cpp 复制代码
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
typedef struct ListNode listnode;
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // 1. 快慢指针找中间节点(偶数时偏右)
        listnode* fast = A;
        listnode* slow = A;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }

        // 2. 反转从 slow 开始的链表(包含 slow)
        listnode* n1 = NULL;
        listnode* n2 = slow;
        listnode* n3 = n2->next;
        n2->next = NULL;          // 断开后半部分,slow 成为新尾部
        while (n3) {
            n1 = n2;
            n2 = n3;
            n3 = n3->next;
            n2->next = n1;
        }
        // 此时 n2 指向反转后的头(原链表尾)

        // 3. 比较前半部分和反转后的后半部分
        listnode* cur1 = A;
        listnode* cur2 = n2;
        while (cur1 && cur2) {
            if (cur1->val != cur2->val)
                return false;
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        return true;              // 全部匹配即为回文
    }
};

8.相交链表

输入两个链表,找出它们的第一个公共结点。

160. 相交链表 - 力扣(LeetCode)https://leetcode.cn/problems/intersection-of-two-linked-lists/

思路:遍历一遍找到两种走法相差的步数x,然后让路线长的那个先走x步,然后两个一起走相遇的那个节点就是

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
    typedef struct ListNode listnode;
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int n1 = 1, n2 = 1;             // 链表长度计数器,初始为1(包含头节点)
        listnode *cura = headA;
        listnode *curb = headB;

        // 1. 遍历链表A到末尾,同时统计长度
        while (cura->next) {
            cura = cura->next;
            n1++;
        }
        // 2. 遍历链表B到末尾,同时统计长度
        while (curb->next) {
            curb = curb->next;
            n2++;
        }

        // 3. 尾节点不同,说明两链表不相交
        if (cura != curb)
            return NULL;

        // 4. 指针回到起点,准备对齐
        cura = headA;
        curb = headB;

        // 5. 让较长链表的指针先走 |n1-n2| 步,使两指针剩余长度相等
        if (n1 > n2) {
            int skip = n1 - n2;
            while (skip--) {
                cura = cura->next;
            }
        } else {
            int skip = n2 - n1;
            while (skip--) {
                curb = curb->next;
            }
        }

        // 6. 同时移动两指针,相遇点即为相交起始节点
        while (cura != curb) {
            cura = cura->next;
            curb = curb->next;
        }
        return cura;
    }
};

9.环形链表

给定一个链表,判断链表中是否有环。

141. 环形链表 - 力扣(LeetCode)https://leetcode.cn/problems/linked-list-cycle/description/

cpp 复制代码
/**
 * 解题思路:快慢指针法(Floyd 判圈算法)
 * - 定义两个指针:慢指针每次走一步,快指针每次走两步。
 * - 若链表无环,快指针会先到达 nullptr,返回 false。
 * - 若链表有环,快慢指针最终会在环内相遇,返回 true。
 * - 特殊情况:空链表或只有一个节点且无环的直接返回 false。
 * 
 * 时间复杂度 O(n),空间复杂度 O(1)
 */

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
    typedef struct ListNode listnode;
public:
    bool hasCycle(ListNode *head) {
        // 空链表一定无环
        if (head == NULL)
            return false;
        // 只有一个节点且没有自环(next为NULL),无环
        if (head->next == NULL)
            return false;

        // 快慢指针初始化都指向头节点
        listnode *fast = head;
        listnode *slow = head;

        // 当快慢指针均非空时继续移动
        while (fast && slow) {
            slow = slow->next;          // 慢指针走一步
            fast = fast->next;          // 快指针先走一步
            if (fast)                   // 如果快指针未到末尾,再走一步(共两步)
                fast = fast->next;

            // 快慢指针相遇说明存在环
            if (fast == slow)
                return true;
        }

        // 遍历结束未相遇,无环
        return false;
    }
};
代码优化
cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode listnode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
          listnode*cura=headA;
          listnode*curb=headB;
          int counta=0,countb=0;
          while(cura->next)
          {
            cura=cura->next;
            counta++;
          }
          while(curb->next)
          {
            curb=curb->next;
            countb++;
          }
          if(cura!=curb)
          return NULL;
          int k=abs(counta-countb);
          listnode*longcur=headA;
          listnode*stortcur=headB;
          if(countb>counta)
          {
            longcur=headB;
            stortcur=headA;
          }
          while(k--)
          {
            longcur=longcur->next;
          }
          while(longcur!=stortcur)
          {
            longcur=longcur->next;
            stortcur=stortcur->next;
          }
          return longcur;
    
}
技巧⭐️⭐️⭐️
【扩展问题】

为什么快指针每次走两步,慢指针走一步可以?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可 能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动 一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前, 快指针肯定是可以追上慢指针的,即相遇。

其实还可以用相对速度来理解,他们进环之后他们两个的相对速度为一,而他们之间的距离除以一肯定是可以整除的,所以就是一定可以相遇。

快指针一次走3步,走4步,...n步行吗?

一次三步也可以(数学证明,列方程)

10.环形链表 II⭐️⭐️(结论)

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

142. 环形链表 II - 力扣(LeetCode)https://leetcode.cn/problems/linked-list-cycle-ii/description/

思路:先用快慢指针判断是否有环,相遇即有环, 从相遇点和头结点开始同时走,再次相遇的点就是环的入口!!

cpp 复制代码
/**
 * 解题思路:快慢指针法(Floyd 判圈算法找环入口)
 * 
 * 阶段1:判断是否有环,并找到快慢指针在环中的相遇点
 * - 快指针每次走2步,慢指针每次走1步。
 * - 初始时,快慢指针都从头节点出发,
 *   但为了让 while 条件能区分"无环结束"和"相遇",先手动让两指针错开一步。
 * - 随后在循环中继续移动,若快指针到达 nullptr 则无环返回 NULL。
 * - 若快慢指针相遇,说明有环,记录相遇点。
 * 
 * 阶段2:找到环的入口节点
 * - 数学证明:从头节点到环入口的距离 = 从相遇点到环入口的距离。
 * - 因此,一个指针从头节点出发,另一个指针从相遇点出发,
 *   两者每次均移动1步,相遇的节点即为环的入口。
 * 
 * 时间复杂度 O(n),空间复杂度 O(1)
 */

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
    typedef struct ListNode listnode;
public:
    ListNode *detectCycle(ListNode *head) {
        // 空链表或只有一个节点且无自环,不可能有环
        if (head == NULL || head->next == NULL)
            return NULL;

        listnode *fast = head;
        listnode *slow = head;

        // 先手动错开一步,避免初始时 fast==slow 导致误判相遇
        // 快指针先走两步
        fast = fast->next;
        if (fast)
            fast = fast->next;
        // 慢指针走一步
        slow = slow->next;

        // 如果 fast 和 slow 未相遇,且 fast 非空,继续移动
        while (fast != slow && fast && slow) {
            fast = fast->next;          // 快指针走一步
            if (fast)                   // 若未到末尾,再走一步(共两步)
                fast = fast->next;
            slow = slow->next;          // 慢指针走一步
        }

        // 若 fast 为空,说明链表无环
        if (fast == NULL)
            return NULL;

        // ---- 有环,寻找环的入口 ----
        listnode *cur1 = head;      // 从链表头出发
        listnode *cur2 = fast;      // 从相遇点出发

        // 两指针同步移动,相遇处即为环入口
        while (cur1 != cur2) {
            cur1 = cur1->next;
            cur2 = cur2->next;
        }
        return cur1;
    }
};
证明(我认为直接记结论)

4.顺序表和链表的区别⭐️

结语

感谢你看到这里!也欢迎订阅我的**「快速复习」专栏!我们一起进步!**

相关推荐
深邃-2 小时前
【数据结构与算法】-二叉树(1):树的概念与结构,二叉树的概念与结构
数据结构·算法·链表·二叉树··顺序表
风筝在晴天搁浅2 小时前
手撕归并排序
数据结构·算法·排序算法
qeen872 小时前
【数据结构】二叉树基本概念及堆的C语言模拟实现
c语言·数据结构·c++·
mounter6252 小时前
Linux Kernel Design Patterns (Part 2):从经典链表到现代 XArray,拆解内核复杂数据结构的设计哲学
linux·数据结构·链表·设计模式·内存管理·kernel
如君愿2 小时前
考研复习 Day 27 | 习题--计算机网络第四章(网络层 上)、数据结构(树与二叉树 上)
数据结构·计算机网络·考研·记录考研
苏渡苇2 小时前
Redis 核心数据结构(三)——Hash,把一堆字段塞进一个 Key
数据结构·redis·redis hash·redis hset
故事和你912 小时前
洛谷-算法2-4-字符串2
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
cpp_25012 小时前
P3374 【模板】树状数组 1
数据结构·c++·算法·题解·洛谷·树状数组
郝学胜-神的一滴2 小时前
干货版《算法导论》 02 :算法效率核心解密
java·开发语言·数据结构·c++·python·算法