【算法--链表】61.旋转链表--通俗讲解

51 阅读4分钟

一、题目是啥?一句话说清

给你一个链表,将链表每个节点向右移动 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 的例子来可视化过程:

  1. 计算链表长度

    • 链表有 5 个节点,所以长度 n = 5。
    • 计算有效 k = 2 % 5 = 2(因为旋转 5 次相当于不旋转)。
  2. 找到关键节点

    • 新头节点应该是倒数第 2 个节点,即节点 4。
    • 新尾节点应该是倒数第 3 个节点(即第 n - k = 3 个节点),即节点 3。
    • 原尾节点是节点 5。
  3. 重新连接

    • 将新尾节点(节点 3)的 next 置为 null。
    • 将原尾节点(节点 5)的 next 指向原头节点(节点 1)。
    • 新链表变为:4 → 5 → 1 → 2 → 3
  4. 返回结果:返回新头节点 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。

掌握这三点,你就能高效解决旋转链表问题。这道题是链表操作的经典题目,考察了对链表结构的理解和指针操作能力。多练习几次,注意细节,就能熟练运用。