【每日算法】LeetCode 25. K 个一组翻转链表

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 25. K 个一组翻转链表

1. 题目描述

给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。

如果节点总数不是 k 的整数倍,最后剩余的节点保持原有顺序。

示例1:

复制代码
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例2:

复制代码
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

说明:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

2. 问题分析

这道题的核心是链表操作,前端开发者经常处理类似结构:

  • DOM树节点操作(如批量重新排序元素)
  • 虚拟DOM的diff算法中节点位置调整
  • 数据流处理中的分批操作
  • 实现分页、轮播等组件时的节点管理

关键难点:

  1. 需要精确控制指针的指向关系
  2. 处理边界情况(不足k个的情况)
  3. 保持翻转后的正确连接
  4. 需要保存关键节点位置以便后续连接

3. 解题思路

3.1 递归法(清晰直观)

时间复杂度:O(n),空间复杂度:O(n/k)(递归栈深度)

  • 递归处理每k个节点
  • 翻转当前k个节点后,递归处理后续部分
  • 将翻转后的子链表连接起来

3.2 迭代法(最优解)

时间复杂度:O(n),空间复杂度:O(1)

  • 使用虚拟头节点简化操作
  • 分组遍历并翻转每一组
  • 维护关键指针:前驱节点、当前组头、当前组尾
  • 处理不足k个的情况

最优解:迭代法,因为它在O(n)时间内解决问题,且只使用常数级额外空间。

4. 代码实现

4.1 递归实现

javascript 复制代码
/**
 * 递归解法
 * 时间复杂度:O(n),空间复杂度:O(n/k)(递归调用栈)
 */
const reverseKGroupRecursive = function(head, k) {
    // 检查是否有k个节点可供翻转
    let curr = head;
    let count = 0;
    
    // 检查剩余节点是否足够k个
    while (curr !== null && count < k) {
        curr = curr.next;
        count++;
    }
    
    // 如果不足k个,直接返回当前头节点
    if (count < k) {
        return head;
    }
    
    // 翻转当前k个节点
    let prev = null;
    let current = head;
    
    for (let i = 0; i < k; i++) {
        const next = current.next;
        current.next = prev;
        prev = current;
        current = next;
    }
    
    // 递归处理后续部分,并将当前翻转后的尾节点连接到后续结果
    head.next = reverseKGroupRecursive(current, k);
    
    // prev现在是翻转后的新头节点
    return prev;
};

4.2 迭代实现(最优)

javascript 复制代码
/**
 * 迭代解法(最优解)
 * 时间复杂度:O(n),空间复杂度:O(1)
 */
const reverseKGroup = function(head, k) {
    // 创建虚拟头节点,简化边界处理
    const dummy = new ListNode(0);
    dummy.next = head;
    
    // pre指向当前要翻转的链表的前一个节点
    let pre = dummy;
    
    while (head) {
        // tail指向当前要翻转的链表的尾部
        let tail = pre;
        
        // 查看剩余部分长度是否大于等于k
        for (let i = 0; i < k; i++) {
            tail = tail.next;
            if (!tail) {
                // 不足k个,直接返回结果
                return dummy.next;
            }
        }
        
        // next指向下一个要翻转的链表头
        const nextGroup = tail.next;
        
        // 翻转当前k个节点,返回翻转后的头尾节点
        const [newHead, newTail] = reverseList(head, tail);
        
        // 把翻转后的子链表重新接回原链表
        pre.next = newHead;
        newTail.next = nextGroup;
        
        // 更新pre和head,准备下一轮翻转
        pre = newTail;
        head = nextGroup;
    }
    
    return dummy.next;
};

/**
 * 辅助函数:翻转从head到tail的链表
 * 返回翻转后的新头节点和新尾节点
 */
const reverseList = function(head, tail) {
    let prev = tail.next; // 关键:连接到下一组的头
    let curr = head;
    
    while (prev !== tail) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    
    // 翻转后,tail成为新头,head成为新尾
    return [tail, head];
};

4.3 可读性更好的迭代实现(适合前端理解)

javascript 复制代码
/**
 * 更易理解的迭代解法
 * 将翻转逻辑拆解为更小的函数
 */
const reverseKGroupEasy = function(head, k) {
    // 计算链表长度
    const getLength = (node) => {
        let len = 0;
        while (node) {
            len++;
            node = node.next;
        }
        return len;
    };
    
    // 翻转链表的一部分
    const reversePart = (start, end) => {
        let prev = end.next; // 连接到下一组的头
        let curr = start;
        
        while (prev !== end) {
            const next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        
        return [end, start]; // 返回新头和新尾
    };
    
    const length = getLength(head);
    const dummy = new ListNode(0);
    dummy.next = head;
    let prev = dummy;
    
    // 计算可以翻转多少组
    const groups = Math.floor(length / k);
    
    for (let i = 0; i < groups; i++) {
        // 定位当前组的头和尾
        let groupHead = prev.next;
        let groupTail = prev;
        
        for (let j = 0; j < k; j++) {
            groupTail = groupTail.next;
        }
        
        // 下一组的头
        const nextGroup = groupTail.next;
        
        // 翻转当前组
        const [newHead, newTail] = reversePart(groupHead, groupTail);
        
        // 重新连接
        prev.next = newHead;
        newTail.next = nextGroup;
        
        // 更新prev,准备下一组
        prev = newTail;
    }
    
    return dummy.next;
};

5. 复杂度对比分析

方法 时间复杂度 空间复杂度 优点 缺点
递归法 O(n) O(n/k) 递归栈空间 代码简洁,逻辑清晰 递归栈可能溢出,空间复杂度较高
迭代法(最优) O(n) O(1) 空间效率高,适合处理长链表 指针操作复杂,容易出错
改进迭代法 O(n) O(1) 逻辑更清晰,易于理解和维护 需要额外计算链表长度

性能总结:

  • 迭代法是最优选择,尤其对于大规模数据
  • 递归法在k值较小时表现良好,代码更简洁
相关推荐
Swizard2 小时前
别再迷信“准确率”了!一文读懂 AI 图像分割的黄金标尺 —— Dice 系数
python·算法·训练
s09071362 小时前
紧凑型3D成像声纳实现路径
算法·3d·声呐·前视多波束
可爱的小小小狼2 小时前
算法:二叉树遍历
算法
d111111111d3 小时前
在STM32函数指针是什么,怎么使用还有典型应用场景。
笔记·stm32·单片机·嵌入式硬件·学习·算法
kingmax542120083 小时前
《数据结构C语言:单向链表-链表基本操作(尾插法建表、插入)》15分钟试讲教案【模版】
c语言·数据结构·链表
AI科技星3 小时前
质量定义方程常数k = 4π m_p的来源、推导与意义
服务器·数据结构·人工智能·科技·算法·机器学习·生活
摇摆的含羞草4 小时前
哈希(hash)算法使用特点及常见疑问解答
算法·哈希算法
仰泳的熊猫4 小时前
1077 Kuchiguse
数据结构·c++·算法·pat考试
LYFlied5 小时前
【每日算法】LeetCode 19. 删除链表的倒数第 N 个结点
算法·leetcode·链表