第六题 回文链表

思路一:创建新链表,将原链表的值反转保存在新链表中,遍历比较两链表
那我们直接将上一篇的反转链表函数拿来:直接比较就完了:
cpp
class PalindromeList
{
public:
ListNode* reverseList(ListNode* head) {
// 1. 初始化三指针中的两个关键指针
struct ListNode* prev = NULL; // 前驱节点,初始为空(反转后的尾节点指向空)
struct ListNode* curr = head; // 当前节点,从头开始
// 2. 遍历链表
while (curr != NULL) {
// 【关键】先保存下一个节点,防止断链
struct ListNode* next_temp = curr->next;
// 3. 核心操作:将当前节点的 next 指向前一个节点(实现反转)
curr->next = prev;
// 4. 双指针同步后移
prev = curr; // prev 前进到当前位置
curr = next_temp; // curr 前进到原链表的下一个位置
}
// 5. 循环结束时,curr 为 NULL,prev 指向原链表尾部(即新链表头部)
return prev;
}
bool chkPalindrome(ListNode* A) {
// write code here
ListNode*newhead= reverseList(A);
while(newhead!=NULL)
{
if(newhead->val!=A->val)
return false;
A=A->next;
newhead=newhead->next;
}
return true;
}
};
这段代码的核心问题在于 reverseList 函数(三指针法)会彻底破坏原始链表的结构。
当你调用 ListNode* newhead = reverseList(A); 时,你传入的 A 链表本身就被反转了。reverseList 函数通过修改每个节点的 next 指针来实现反转,这意味着当函数执行完毕后,A 指向的链表已经不再是原来的样子,它的头变成了尾,尾变成了头。
因此,在接下来的 while 循环中,你试图用 newhead 和 A 进行比较,但实际上 newhead 和 A 指向的是同一个已经被反转了的链表。这会导致两个问题:
- 逻辑错误 :你是在拿一个反转后的链表和它自己比较,这当然永远是
true,无法判断原始链表是否为回文。 - 潜在的无限循环 :由于
A和newhead是同一个链表,你的遍历逻辑会变得混乱。
解决办法:上一篇博客的反转链表我们使用了三种办法,全部修改了原链表因此不可使用,想要实现思路一,就需要用到malloc申请新节点:
cpp
struct ListNode* createReverseList(struct ListNode* head) {
struct ListNode* new_head = NULL;
while (head != NULL) {
// 【关键区别】:申请一个全新的节点,而不是直接挪用原节点
struct ListNode* new_node = (struct ListNode*)malloc(sizeof(struct ListNode));
new_node->val = head->val; // 拷贝数据
// 头插法插入新节点
new_node->next = new_head;
new_head = new_node;
// 原链表指针正常后移
head = head->next;
}
return new_head;
}
转换成C++模式提交:
没学过C++也不打紧,因为这段代码和C语言的唯一区别就是不用写结构体的struct
cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList
{
public:
ListNode* createReverseList(ListNode* head) {
struct ListNode* new_head = NULL;
while (head != NULL) {
// 【关键区别】:申请一个全新的节点,而不是直接挪用原节点
ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
new_node->val = head->val; // 拷贝数据
// 头插法插入新节点
new_node->next = new_head;
new_head = new_node;
// 原链表指针正常后移
head = head->next;
}
return new_head;
}
bool chkPalindrome(ListNode* A) {
// write code here
ListNode*newhead= createReverseList(A);
while(newhead!=NULL)
{
if(newhead->val!=A->val)
return false;
A=A->next;
newhead=newhead->next;
}
return true;
}
};
结果正确
💡 优化思路
要判断一个链表是否为回文,标准的做法是:
- 找到链表的中间节点。
- 反转后半部分链表。
- 比较前半部分和反转后的后半部分。
这样既能达到目的,又只破坏了链表的后半部分(如果需要保持原链表不变,最后还可以再把后半部分反转回来)。
上一篇博客我们讲解了寻找中间节点函数和反转函数,简单修改:
cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
ListNode* reverseList(ListNode* head) {
// 1. 初始化三指针中的两个关键指针
ListNode* prev =
NULL; // 前驱节点,初始为空(反转后的尾节点指向空)
ListNode* curr = head; // 当前节点,从头开始
// 2. 遍历链表
while (curr != NULL) {
// 【关键】先保存下一个节点,防止断链
ListNode* next_temp = curr->next;
// 3. 核心操作:将当前节点的 next 指向前一个节点(实现反转)
curr->next = prev;
// 4. 双指针同步后移
prev = curr; // prev 前进到当前位置
curr = next_temp; // curr 前进到原链表的下一个位置
}
// 5. 循环结束时,curr 为 NULL,prev 指向原链表尾部(即新链表头部)
return prev;
}
ListNode* middleNode(ListNode* head) {
// 1. 定义快慢指针,都从头开始
ListNode* slow = head;
ListNode* fast = head;
// 2. 循环条件:
// fast != NULL 处理偶数长度情况(fast最终指向尾部节点的下一个空位)
// fast->next != NULL 处理奇数长度情况(fast最终指向最后一个节点)
while (fast != NULL && fast->next != NULL) {
slow = slow->next; // 慢指针走一步
fast = fast->next->next; // 快指针走两步
}
// 3. 返回慢指针,即为中点
return slow;
}
bool chkPalindrome(ListNode* A) {
// write code here
ListNode* head = A;
ListNode* newhead = middleNode(A);
newhead = reverseList(newhead);
while (newhead != NULL) {
if (newhead->val != head->val)
return false;
head = head->next;
newhead = newhead->next;
}
return true;
}
};
修正要点总结
middleNode函数:新增了这个函数,使用快慢指针法准确地找到链表的中间点。chkPalindrome逻辑重构 :- 先找到中间节点
mid。 - 只反转从
mid开始的后半部分链表,得到后半部分反转后的链表。 - 用两个指针 head 和 newhead 分别从链表头和 新链表头开始,同步向后遍历比较。
- 循环条件
while (newhead != NULL)是因为后半部分链表的长度总是小于或等于前半部分。
- 先找到中间节点
- 边界条件:不必增加对空链表和单节点链表的判断,它们都被包含在内。
第七题 相交链表

以下是针对"链表相交"问题的专业化解析,按照思路、核心逻辑实现、代码的顺序进行重构。
解题思路:双指针同步对齐法
该问题本质上是寻找两个单向链表的公共后缀起点。由于题目已保证链表无环,我们可以利用物理地址唯一性作为判断依据。
核心思想是消除长度差带来的起始位置偏移。若两个链表相交,它们从交点开始到末尾的长度必然相同。因此,只要让两个指针处于"距离尾部等长"的起跑线上,同步向后遍历,第一个地址相同的节点即为交点。这种方法避免了暴力双重循环,将时间复杂度控制在 O(M+N),空间复杂度为 O(1)。
核心逻辑实现步骤
- 计算链表长度 :分别遍历链表 A 和链表 B,统计各自的节点数量
lenA和lenB。 - 确定长短链表 :比较
lenA与lenB,定义longList指向较长链表的头节点,shortList指向较短链表的头节点,并计算长度差值gap = |lenA - lenB|。 - 预走差值步数 :让
longList指针先向前移动gap步。此时,longList和shortList距离各自链表尾部的剩余节点数完全相等。 - 同步遍历比对 :同时移动
longList和shortList,在每一步中严格比较内存地址 (即指针本身),而非节点的值。- 若
longList == shortList,说明找到了物理交点,直接返回该节点。 - 若遍历结束仍未相遇,说明两链表不相交,返回
NULL。
- 若
完整代码实现 (C语言)
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
// 1. 边界处理
if (!headA || !headB) return NULL;
// 2. 计算两个链表的长度
int lenA = 0, lenB = 0;
struct ListNode* curA = headA;
struct ListNode* curB = headB;
while (curA) { lenA++; curA = curA->next; }
while (curB) { lenB++; curB = curB->next; }
// 3. 确定长链表和短链表,并计算差值
struct ListNode* longList = headA;
struct ListNode* shortList = headB;
int gap = lenA - lenB;
// 如果 B 比 A 长,交换角色
if (lenB > lenA) {
longList = headB;
shortList = headA;
gap = lenB - lenA;
}
// 4. 长链表先走 gap 步,实现对齐
while (gap--) {
longList = longList->next;
}
// 5. 同步遍历,比较地址(指针)而非值
while (longList && shortList) {
// 物理相交意味着内存地址相同
if (longList == shortList) {
return longList;
}
longList = longList->next;
shortList = shortList->next;
}
// 6. 未找到交点
return NULL;
}
关键点提示
- 比较的是指针而非值 :代码中
if (longList == shortList)比较的是节点的内存地址。题目中的示例经常出现数值相同但并非交点的情况(如两个链表都有值为1的节点但未连接),必须通过地址判断。 - 不要修改原链表结构:此方法仅使用指针遍历,不涉及断开或重连节点,保证了数据的安全性。
这个解法是面试中最经典的'标准答案'。其实这道题还有个更巧妙的'浪漫解法'(双指针互换法),不用算长度就能:
优化思路:双指针互换法(浪漫解法)
这种解法被程序员称为"浪漫解法",因为它蕴含了一个极具美感的数学逻辑:A + B = B + A。
核心思想是利用路径长度的互补性来自动消除差值。我们定义两个指针 pA 和 pB 分别从 headA 和 headB 出发。当 pA 遍历完链表 A 后,让它跳转到链表 B 的头部继续走;同理,当 pB 遍历完链表 B 后,跳转到链表 A 的头部。
如果两链表相交,由于 len(A) + len(B_common) 与 len(B) + len(A_common) 的总步数必然相等,两个指针最终会在交点处相遇。如果不相交,它们会同时走到 NULL,此时 NULL == NULL 成立,循环结束并返回 NULL。这种方法无需计算长度,代码极其优雅。
为什么能相遇?
- 指针
pA走的路线是:先走完 A (比如5步),再走 B 的前半段(直到交点)。总路程 = 5+(B到交点的距离)5+(B到交点的距离) 。 - 指针
pB走的路线是:先走完 B (比如6步),再走 A 的前半段(直到交点)。总路程 = 6+(A到交点的距离)6+(A到交点的距离) 。 - 因为 A的独有部分+B的独有部分A的独有部分+B的独有部分 是固定的,所以它们最终会在同一个点相遇。
核心逻辑实现步骤
-
初始化双指针
定义指针
pA指向headA,指针pB指向headB。 -
同步遍历与跳转
进入循环,每次迭代中
pA和pB各向后移动一步。- 关键判断 :当指针移动到链表末尾(即
next为NULL)时,将其重定向到另一个链表的头节点。- 若
pA到达 A 的尾部,下一步指向headB。 - 若
pB到达 B 的尾部,下一步指向headA。
- 若
- 关键判断 :当指针移动到链表末尾(即
-
终止条件判定
循环持续进行,直到
pA == pB。这包含两种情况:- 相交:两者在某个非空节点相遇,该节点即为交点。
- 不相交 :两者同时变为
NULL(即都走完了A+B的全程),此时返回NULL。
完整代码实现 (C语言)
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
// 1. 边界处理:任一为空则不可能相交
if (!headA || !headB) return NULL;
// 2. 初始化双指针
struct ListNode *pA = headA;
struct ListNode *pB = headB;
// 3. 循环直到相遇(包括同时为 NULL 的情况)
while (pA != pB) {
// 4. 核心逻辑:走完自己的路,就去走对方的路
// 如果 pA 到了尽头,就跳到 headB;否则继续走 next
pA = (pA == NULL) ? headB : pA->next;
// 如果 pB 到了尽头,就跳到 headA;否则继续走 next
pB = (pB == NULL) ? headA : pB->next;
}
// 5. 返回结果
// 如果相交,pA 就是交点地址;如果不相交,pA 为 NULL
return pA;
}
关键点提示
- 为什么不会死循环?
即使两链表不相交,指针最终都会变成NULL。因为NULL == NULL为真,循环会正常终止。这巧妙地避免了额外的判空逻辑。 - 时间复杂度分析
每个指针最多遍历两个链表的总长度(M+N)。因此时间复杂度依然是 O(M+N),但常数因子更小,且省去了计算长度的两次遍历。 - 适用场景限制
此方法严格依赖于题目保证的"无环"条件。如果链表存在环,指针可能在环内无限兜圈而无法跳出,导致死循环。
第霸体 环形链表

思路
采用快慢指针(Floyd 判圈算法)。初始化两个指针均指向头节点,慢指针每次向后移动一步,快指针每次向后移动两步。若链表中存在环,快指针最终会在环内追上慢指针(两指针相遇);若链表无环,快指针会率先遍历到链表尾部(遇到 NULL)。
核心逻辑实现
- 定义慢指针
slow与快指针fast,初始均指向head。 - 在循环条件中持续判断
fast及其下一个节点fast->next是否为NULL,防止空指针解引用。 - 循环体内,先更新
fast走两步,再更新slow走一步。 - 每次移动后检查
fast == slow,若相等则说明存在环,返回相遇节点;若循环正常结束,说明无环,返回NULL。
代码
cpp
typedef struct ListNode listnode;
struct ListNode *hasCycle(struct ListNode *head)
{
listnode *slow = head;
listnode *fast = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
return slow; // 存在环,返回相遇节点
}
}
return NULL; // 无环
}
第九题 环形链表II

思路
环形链表 II 的核心目标是找到链表入环的第一个节点。依然使用快慢指针(Floyd 判圈算法):
- 第一阶段:快指针每次走两步,慢指针每次走一步,直到两者在环内相遇。
- 第二阶段:将快指针(或慢指针)重新指向头节点,然后两个指针都改为每次走一步。当它们再次相遇时,相遇点即为入环的第一个节点。
数学原理 :
设链表头到入环点的距离为 a,入环点到快慢指针首次相遇点的距离为 b,相遇点继续走到入环点的距离为 c。
- 慢指针走过的距离:
a + b - 快指针走过的距离:
a + b + n(b + c)(n为快指针在环内多走的圈数)
因为快指针速度是慢指针的两倍,所以:2(a + b) = a + b + n(b + c)
化简得到:a = n(b + c) - b=(n - 1)(b + c) + c
这个等式说明:从链表头走到入环点的距离a,等于从相遇点继续走c的距离再加上整数圈 。因此,当两个指针分别从head和相遇点以相同速度前进时,必定会在入环点相遇。
核心逻辑实现
- 初始化
slow和fast均指向head。 - 循环移动,直到
fast == slow(若中途fast或fast->next为NULL则直接返回NULL)。 - 将
fast重新指向head。 fast和slow每次各走一步,直到再次相遇,返回该节点。
代码
cpp
typedef struct ListNode listnode;
struct ListNode *detectCycle(struct ListNode *head)
{
listnode *slow = head;
listnode *fast = head;
// 第一阶段:寻找相遇点
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
// 第二阶段:寻找入环点
fast = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return slow; // 返回入环的第一个节点
}
}
return NULL; // 链表无环
}
第十题 随机链表的复制

1. 题目理解
我们需要复制一个带有随机指针的链表。每个节点除了 next 指针外,还有一个 random 指针,可以指向链表中的任意节点或 NULL。复制后的链表必须完全独立,即新节点的 next 和 random 指针都不能指向原链表的节点,而是指向新链表中的对应节点。
关键点:
- 随机指针的存在使得我们不能简单地按顺序复制,因为当复制某个节点时,它的
random指向的节点可能还未被创建。 - 需要保证新链表与原链表结构完全一致,且节点之间是独立的。
2. 解题思路
常见的解法有三种:哈希表法、原地拼接法(浪漫解法)、递归法。这里重点讲解原地拼接法,因为它不需要额外空间(除新节点本身外),且逻辑巧妙。
核心思想:
- 第一步:复制节点并插入原节点之后。
遍历原链表,对于每个节点cur,创建一个新节点newNode(值与cur相同),将newNode插入到cur和cur->next之间。此时链表结构变为:原1 -> 新1 -> 原2 -> 新2 -> ...。 - 第二步:设置新节点的
random指针。
由于新节点newNode紧跟在原节点cur之后,若原节点cur->random指向target,则newNode->random应指向target->next(即target的复制节点)。 - 第三步:拆分链表。
将原链表和新链表分离,恢复原链表的next指针,同时提取新链表的头节点。
3. 详细步骤
步骤1:复制节点并插入
- 初始化
cur = head。 - 当
cur不为空时:- 创建
newNode,newNode->val = cur->val。 - 将
newNode插入到cur之后:newNode->next = cur->next,cur->next = newNode。 - 移动
cur到下一个原节点:cur = newNode->next(即跳过新节点,到下一个原节点)。
- 创建
示例:
原链表:A -> B -> C
插入后:A -> A' -> B -> B' -> C -> C'(A' 是 A 的复制,依此类推)。
步骤2:设置 random 指针
- 再次遍历链表,
cur指向原节点(A, B, C)。 - 对于每个
cur,其复制节点是cur->next(A', B', C')。 - 若
cur->random不为空,则cur->next->random = cur->random->next(因为cur->random->next是cur->random的复制节点)。 - 若
cur->random为空,则cur->next->random = NULL。 - 移动
cur到下一个原节点:cur = cur->next->next。
步骤3:拆分链表
- 初始化
dummy = new Node(0),p = dummy(用于构建新链表),cur = head(原链表头)。 - 当
cur不为空时:- 新节点
newNode = cur->next。 - 将
newNode连接到新链表:p->next = newNode,p = newNode。 - 恢复原链表的
next指针:cur->next = newNode->next(跳过新节点,连接到下一个原节点)。 - 移动
cur到下一个原节点:cur = cur->next。
- 新节点
- 最后,
dummy->next是新链表的头节点,返回它。
4. 代码实现(C语言)
cpp
// 定义链表节点结构
typedef struct Node {
int val;
struct Node *next;
struct Node *random;
} Node;
Node* copyRandomList(Node* head) {
if (head == NULL) {
return NULL;
}
// 步骤1:复制节点并插入原节点之后
Node *cur = head;
while (cur != NULL) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->val = cur->val;
newNode->next = cur->next;
cur->next = newNode;
cur = newNode->next; // 移动到下一个原节点
}
// 步骤2:设置新节点的 random 指针
cur = head;
while (cur != NULL) {
if (cur->random != NULL) {
cur->next->random = cur->random->next; // 原 random 的 next 是复制节点
} else {
cur->next->random = NULL;
}
cur = cur->next->next; // 移动到下一个原节点
}
// 步骤3:拆分链表
Node *dummy = (Node*)malloc(sizeof(Node)); // 虚拟头节点
dummy->next = NULL;
Node *p = dummy; // 新链表的尾指针
cur = head;
while (cur != NULL) {
Node *newNode = cur->next; // 当前原节点的复制节点
p->next = newNode; // 连接到新链表
p = newNode;
cur->next = newNode->next; // 恢复原链表的 next
cur = cur->next; // 移动到下一个原节点
}
p->next = NULL; // 新链表尾节点的 next 置空
Node *newHead = dummy->next;
free(dummy); // 释放虚拟头节点
return newHead;
}
5. 复杂度分析
- 时间复杂度: O(n),三次遍历链表(复制、设 random、拆分),每次 O(n)。
- 空间复杂度: O(1),除了新节点本身,没有使用额外空间(哈希表法需要 O(n) 空间)。
6. 示例验证
原链表:
A (val=1, random=B) → B (val=2, random=A) → C (val=3, random=NULL)
步骤1后:
A → A' → B → B' → C → C'
步骤2后:
A'->random = A->random->next = B->next = B'B'->random = B->random->next = A->next = A'C'->random = NULL
步骤3后:
原链表恢复为 A → B → C,新链表为 A' → B' → C',且 A'->random = B',B'->random = A',C'->random = NULL,完全符合要求。
7. 总结
原地拼接法通过巧妙地将新节点插入原节点之后,解决了随机指针的指向问题,避免了哈希表的额外空间开销。这种方法逻辑清晰,代码简洁,是解决本题的最优解之一。
第六题
本来是要写成一篇博客的,但是现在就已经有12000字了,所以还是分成两篇把......
🌟 写在最后
代码写完了,但我想对正在阅读这篇博客的你说几句心里话。
最近身边有很多同学在迷茫、害怕,大体原因是只能靠自己一个人在这个世界打拼,无依无靠;而世界又在不断变化进步,不知道自己的出路在哪里。
我把送给他们的话,也送给正在屏幕前努力的你:
保持乐观,悲观者正确,乐观者前行。
相信时间的力量,就不必问何时才有回报。
拥有坚持的毅力,就不必在意暂时的苟且。
保持每天的进步,就不必对未来感到迷茫。
愿你在代码的世界里披荆斩棘,也在人生的道路上步履不停。
版权声明:本文为CSDN博主「东华万里」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_86187158/article/details/162606768
版权声明:本文为CSDN博主「东华万里」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_86187158/article/details/162630563