算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
通俗易懂讲解“重排链表”算法题目
一、题目是啥?一句话说清
给定一个单链表,重新排列节点顺序,使得第一个节点后跟最后一个节点,第二个节点后跟倒数第二个节点,以此类推,形成“首尾交替”的新链表。
示例:
- 输入:1 → 2 → 3 → 4 → 5
- 输出:1 → 5 → 2 → 4 → 3
二、解题核心
使用快慢指针找到链表中点,将链表分成前后两半,反转后半部分链表,然后像“拉链”一样交替合并前后两个链表。
这就像把链表从中间折断,反转后半段,然后像拉链一样将两段链表交错合并。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 快慢指针找中点
- 是什么:使用两个指针,慢指针每次走一步,快指针每次走两步。当快指针到达末尾时,慢指针正好在链表中点。
- 为什么重要:这样可以将链表分成两个部分:前半部分和后半部分,为后续反转和合并做准备。这是分割链表的高效方法,不需要计算长度。
2. 反转链表
- 是什么:将后半部分链表反转,使得原最后一个节点成为新头节点。
- 为什么重要:反转后,我们可以从后往前遍历后半部分链表,从而在合并时能够按顺序交替连接节点。反转链表是链表操作中的基础但关键技巧。
3. 合并两个链表
- 是什么:将前半部分链表和反转后的后半部分链表交替合并,即从前半部分取一个节点,然后从后半部分取一个节点,如此反复。
- 为什么重要:这是实现重排的关键步骤,确保节点顺序符合要求。合并时需要小心处理指针,避免链表断裂。
四、看图理解流程(通俗理解版本)
假设链表为:1 → 2 → 3 → 4 → 5
-
找中点:
- 快慢指针:慢指针从1开始,快指针从1开始。
- 第一轮:慢指针走到2,快指针走到3。
- 第二轮:慢指针走到3,快指针走到5(快指针无法再走两步,停止)。
- 中点是节点3。将链表分成前半部分:1→2→3 和后半部分:4→5。
-
反转后半部分:
- 后半部分4→5,反转后变成5→4。
-
合并链表:
- 前半部分:1→2→3
- 反转后半部分:5→4
- 交替合并:
- 1指向5:1→5
- 5指向2:1→5→2
- 2指向4:1→5→2→4
- 4指向3:1→5→2→4→3
- 最终链表:1→5→2→4→3
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
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:
void reorderList(ListNode* head) {
if (head == nullptr || head->next == nullptr) return;
// 步骤1: 使用快慢指针找到链表中点
ListNode *slow = head, *fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
// 步骤2: 反转后半部分链表
ListNode *prev = nullptr, *curr = slow->next;
slow->next = nullptr; // 将链表分成两半
while (curr != nullptr) {
ListNode *nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
// 步骤3: 合并两个链表
ListNode *first = head, *second = prev;
while (second != nullptr) {
ListNode *temp1 = first->next;
ListNode *temp2 = second->next;
first->next = second;
second->next = temp1;
first = temp1;
second = temp2;
}
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:1->2->3->4->5
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
Solution solution;
solution.reorderList(head);
printList(head); // 输出:1 5 2 4 3
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是链表长度。找中点、反转链表和合并链表都是线性时间,每个步骤大约需要O(n/2)时间,总时间为O(n)。
- 空间复杂度:O(1),只使用了常数额外空间(几个指针),没有使用递归或其他数据结构。
七、注意事项
- 空链表或单节点链表:直接返回,不需要处理。
- 正确分割链表:在找到中点后,必须将中点的next设置为null,以确保链表被正确分成两半,否则可能导致无限循环。
- 反转链表:反转时注意保存下一个节点,避免丢失后续节点。
- 合并链表:合并时需要临时保存两个链表的下一个节点,以免断链。合并循环的条件是后半部分链表不为空,因为后半部分可能比前半部分短(当链表长度为奇数时)。
- 测试边界情况:测试链表长度为奇数和偶数的情况,确保代码正确性。
算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解