一、题目是啥?一句话说清
给你一个链表,将链表每个节点向右移动 k 个位置,相当于把链表的最后 k 个节点移动到链表的前面。
示例:
- 输入:head = [1,2,3,4,5], k = 2
- 输出:[4,5,1,2,3](最后2个节点4和5移动到了前面)
二、解题核心
先计算链表长度,然后找到新链表的头节点和尾节点,重新连接链表。 这就像把一列火车的最后几节车厢连接到火车的前面。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 处理 k 大于链表长度的情况
- 是什么:如果 k 大于链表长度,实际有效的旋转次数是 k 对链表长度取模后的值。
- 为什么重要:因为旋转链表长度倍数次相当于没有旋转,取模可以避免不必要的操作。
2. 找到新链表的头节点和尾节点
- 是什么:新头节点是原链表的倒数第 k 个节点,新尾节点是原链表的倒数第 k+1 个节点。
- 为什么重要:只有正确找到这些节点,才能正确断开和重新连接链表。
3. 重新连接链表
- 是什么:将原链表的尾节点连接到头节点,并将新尾节点的 next 置为 null。
- 为什么重要:这是形成新链表的关键步骤,如果连接错误会导致链表断开或形成环。
四、看图理解流程(通俗理解版本)
让我们用链表 1 → 2 → 3 → 4 → 5 和 k=2 的例子来可视化过程:
-
计算链表长度:
- 链表有 5 个节点,所以长度 n = 5。
- 计算有效 k = 2 % 5 = 2(因为旋转 5 次相当于不旋转)。
-
找到关键节点:
- 新头节点应该是倒数第 2 个节点,即节点 4。
- 新尾节点应该是倒数第 3 个节点(即第 n - k = 3 个节点),即节点 3。
- 原尾节点是节点 5。
-
重新连接:
- 将新尾节点(节点 3)的 next 置为 null。
- 将原尾节点(节点 5)的 next 指向原头节点(节点 1)。
- 新链表变为:4 → 5 → 1 → 2 → 3
-
返回结果:返回新头节点 4。
五、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:
ListNode* rotateRight(ListNode* head, int k) {
// 处理特殊情况:链表为空或只有一个节点
if (head == nullptr || head->next == nullptr) {
return head;
}
// 计算链表长度并找到尾节点
int n = 1;
ListNode* tail = head;
while (tail->next != nullptr) {
tail = tail->next;
n++;
}
// 计算有效的 k 值
k = k % n;
if (k == 0) {
return head; // 不需要旋转
}
// 找到新尾节点(第 n - k 个节点)
ListNode* new_tail = head;
for (int i = 0; i < n - k - 1; i++) {
new_tail = new_tail->next;
}
// 新头节点是新尾节点的下一个节点
ListNode* new_head = new_tail->next;
// 重新连接链表
new_tail->next = nullptr; // 新尾节点指向 null
tail->next = head; // 原尾节点指向原头节点
return new_head;
}
};
// 辅助函数:打印链表
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, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5))));
Solution solution;
ListNode* result = solution.rotateRight(head, 2);
printList(result); // 输出:4 5 1 2 3
// 释放内存(实际面试中可能不需要完整释放)
return 0;
}
六、注意事项
- 边界情况处理:链表为空或只有一个节点时,直接返回原链表。
- k 为 0 的情况:如果 k 为 0 或链表长度的倍数,不需要旋转,直接返回原链表。
- 指针操作:在移动指针时,确保不会访问空指针,特别是在寻找新尾节点时。
- 内存管理:在 C++ 中,如果使用了
new创建节点,记得在适当的时候释放内存,但面试中通常更关注算法逻辑。
七、总结
理解此题的关键在于:
- 取模操作:处理 k 大于链表长度的情况。
- 找到关键节点:正确找到新头节点和新尾节点。
- 重新连接:将原尾节点连接到头节点,并设置新尾节点的 next 为 null。
掌握这三点,你就能高效解决旋转链表问题。这道题是链表操作的经典题目,考察了对链表结构的理解和指针操作能力。多练习几次,注意细节,就能熟练运用。