链表
学习链表及以后的数据结构,最重要的就是 " 学会画图 !!!!"
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个结点。
上一题的变式题。两个指针间隔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. 链表的回文结构⭐️⭐️(基础题的大综合)
寻找中间节点+链表翻转
思路:把这个链表从中间分开成两个链表,翻转第二个链表,判断两个链表是否一样。
注意:分开两个链表后,两个链表可以不断开,因为,我们判断两个链表是否一样时,短的那个结束了,就判断结束了。
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.顺序表和链表的区别⭐️

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



