文章目录
1. 题目描述
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回翻转后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
解释:每2个节点一组进行翻转:
原始:1 → 2 → 3 → 4 → 5
翻转:2 → 1 → 4 → 3 → 5
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
解释:每3个节点一组进行翻转:
原始:1 → 2 → 3 → 4 → 5
翻转:3 → 2 → 1 → 4 → 5(最后2个节点不足3个,保持原序)
示例 3:
输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]
解释:k=1时,相当于不翻转
示例 4:
输入:head = [1], k = 1
输出:[1]
解释:单个节点的情况
示例 5:
输入:head = [], k = 2
输出:[]
解释:空链表的情况
提示:
- 链表中节点的数目在范围
[0, 5000]
内 0 <= Node.val <= 1000
1 <= k <= 链表长度
进阶要求:
- 你可以设计一个只使用
O(1)
额外内存空间的算法解决此问题吗? - 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
1.1 链表节点定义
/**
* 单链表节点的定义
*/
public class ListNode {
int val; // 节点的值
ListNode next; // 指向下一个节点的指针
// 无参构造函数
ListNode() {}
// 带值的构造函数
ListNode(int val) {
this.val = val;
}
// 带值和下一个节点的构造函数
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
2. 理解题目
K个一组翻转链表是链表操作中的经典难题,它是"两两交换链表中的节点"(LeetCode 24)的泛化版本。这道题考查的是对链表操作的深度理解和指针操作的精确控制。
关键概念:
- 分组翻转:将链表按照k个节点为一组进行划分
- 完整组翻转:只有当一组包含完整的k个节点时才进行翻转
- 剩余节点保持原序:不足k个节点的剩余部分保持原有顺序
- 实际节点交换:必须通过改变指针连接来实现,不能只修改节点值
2.1 问题可视化
示例 1 详细分析: head = [1,2,3,4,5], k = 2
原始链表:
1 → 2 → 3 → 4 → 5 → null
分组情况:
第1组:[1, 2] (2个节点,满足k=2,需要翻转)
第2组:[3, 4] (2个节点,满足k=2,需要翻转)
第3组:[5] (1个节点,不足k=2,保持原序)
翻转过程:
步骤1:翻转第1组 [1,2] → [2,1]
结果:2 → 1 → 3 → 4 → 5
步骤2:翻转第2组 [3,4] → [4,3]
结果:2 → 1 → 4 → 3 → 5
最终结果:[2,1,4,3,5]
示例 2 详细分析: head = [1,2,3,4,5], k = 3
原始链表:
1 → 2 → 3 → 4 → 5 → null
分组情况:
第1组:[1, 2, 3] (3个节点,满足k=3,需要翻转)
第2组:[4, 5] (2个节点,不足k=3,保持原序)
翻转过程:
步骤1:翻转第1组 [1,2,3] → [3,2,1]
结果:3 → 2 → 1 → 4 → 5
最终结果:[3,2,1,4,5]
2.2 核心挑战
- 精确分组:准确识别每个长度为k的组
- 条件翻转:只翻转完整的k个节点组
- 链表重连:翻转后正确连接各个组
- 边界处理:处理空链表、k=1、链表长度不足k等情况
- 指针管理:维护前驱、当前组、后继的指针关系
2.3 思路分析
方法一:迭代法(推荐)
- 先计算链表长度,确定需要翻转的组数
- 逐组进行翻转操作
- 维护前驱节点,处理组间连接
方法二:递归法
- 处理当前的k个节点
- 递归处理剩余部分
- 连接当前组和递归结果
方法三:栈辅助法
- 使用栈存储每k个节点
- 弹出栈中节点重新连接
- 处理剩余不足k个的节点
3. 解法一:迭代法(预计算长度)
3.1 算法思路
这是最容易理解的解法:先遍历一次链表计算总长度,然后根据长度确定需要翻转多少个完整的组。
核心步骤:
- 使用哨兵节点简化头节点处理
- 计算链表总长度
- 计算需要翻转的完整组数:
count = length / k
- 对每个完整组执行翻转操作
- 维护前驱指针,连接各个翻转后的组
3.2 Java代码实现
/**
* 解法一:迭代法(预计算长度)
* 时间复杂度:O(n),其中 n 是链表的长度
* 空间复杂度:O(1)
*/
class Solution1 {
public ListNode reverseKGroup(ListNode head, int k) {
// 边界处理
if (head == null || k <= 1) {
return head;
}
// 创建哨兵节点,简化头节点处理
ListNode dummy = new ListNode(0);
dummy.next = head;
// 第一步:计算链表长度
int length = 0;
ListNode current = head;
while (current != null) {
length++;
current = current.next;
}
// 第二步:计算需要翻转的完整组数
int groupCount = length / k;
// 第三步:逐组翻转
ListNode prev = dummy;
ListNode groupHead = prev.next;
for (int i = 0; i < groupCount; i++) {
// 翻转当前组的k个节点
prev = reverseGroup(prev, k);
groupHead = prev.next;
}
return dummy.next;
}
/**
* 翻转prev后面的k个节点
* @param prev 要翻转组的前驱节点
* @param k 组大小
* @return 翻转后该组的最后一个节点(下一组的前驱)
*/
private ListNode reverseGroup(ListNode prev, int k) {
// 保存要翻转组的第一个节点,翻转后它将成为该组的最后一个节点
ListNode groupTail = prev.next;
ListNode current = prev.next;
// 执行k-1次翻转操作
for (int i = 0; i < k - 1; i++) {
ListNode next = current.next;
current.next = next.next;
next.next = prev.next;
prev.next = next;
}
return groupTail;
}
}
3.3 详细执行过程演示
/**
* 带详细调试输出的迭代法实现
*/
public class IterativeMethodDemo {
public ListNode reverseKGroup(ListNode head, int k) {
System.out.println("=== 迭代法K个一组翻转链表 ===");
System.out.println("原始链表: " + printList(head));
System.out.println("k = " + k);
if (head == null || k <= 1) {
System.out.println("无需翻转,直接返回");
return head;
}
// 创建哨兵节点
ListNode dummy = new ListNode(0);
dummy.next = head;
System.out.println("创建哨兵节点: " + printListWithDummy(dummy));
// 计算链表长度
System.out.println("\n第一步:计算链表长度");
int length = 0;
ListNode current = head;
while (current != null) {
length++;
System.out.println(" 访问节点值: " + current.val + ",当前长度: " + length);
current = current.next;
}
int groupCount = length / k;
int remainCount = length % k;
System.out.println("链表总长度: " + length);
System.out.println("完整组数: " + groupCount + " (每组" + k + "个节点)");
System.out.println("剩余节点数: " + remainCount + " (保持原序)");
if (groupCount == 0) {
System.out.println("没有完整的组需要翻转");
return head;
}
// 逐组翻转
System.out.println("\n第二步:逐组翻转");
ListNode prev = dummy;
for (int i = 0; i < groupCount; i++) {
System.out.println("\n--- 翻转第" + (i + 1) + "组 ---");
System.out.println("翻转前: " + printListWithDummy(dummy));
prev = reverseGroupWithDebug(prev, k, i + 1);
System.out.println("翻转后: " + printListWithDummy(dummy));
}
ListNode result = dummy.next;
System.out.println("\n最终结果: " + printList(result));
return result;
}
private ListNode reverseGroupWithDebug(ListNode prev, int k, int groupIndex) {
System.out.println(" 开始翻转第" + groupIndex + "组的" + k + "个节点");
// 显示当前组的节点
ListNode temp = prev.next;
System.out.print(" 当前组节点: [");
for (int i = 0; i < k && temp != null; i++) {
System.out.print(temp.val);
if (i < k - 1 && temp.next != null) System.out.print(", ");
temp = temp.next;
}
System.out.println("]");
ListNode groupTail = prev.next;
ListNode current = prev.next;
// 执行翻转
for (int i = 0; i < k - 1; i++) {
ListNode next = current.next;
current.next = next.next;
next.next = prev.next;
prev.next = next;
System.out.println(" 翻转操作 " + (i + 1) + ": 将节点" + next.val + "移到组首");
System.out.println(" 当前状态: " + printListWithDummy(findDummy(prev)));
}
System.out.println(" 第" + groupIndex + "组翻转完成,返回新的前驱节点: " + groupTail.val);
return groupTail;
}
// 辅助方法:打印链表
private String printList(ListNode head) {
if (head == null) return "[]";
StringBuilder sb = new StringBuilder();
sb.append("[");
ListNode current = head;
while (current != null) {
sb.append(current.val);
if (current.next != null) {
sb.append(" → ");
}
current = current.next;
}
sb.append("]");
return sb.toString();
}
// 辅助方法:打印包含哨兵节点的链表
private String printListWithDummy(ListNode dummy) {
StringBuilder sb = new StringBuilder();
sb.append("[哨兵:0] → ");
if (dummy.next != null) {
sb.append(printList(dummy.next));
} else {
sb.append("null");
}
return sb.toString();
}
// 辅助方法:找到哨兵节点(用于调试)
private ListNode findDummy(ListNode node) {
// 简化实现,实际项目中不建议这样做
ListNode dummy = new ListNode(0);
dummy.next = node.next;
return dummy;
}
}
3.4 执行结果示例
示例:head = [1,2,3,4,5], k = 3
=== 迭代法K个一组翻转链表 ===
原始链表: [1 → 2 → 3 → 4 → 5]
k = 3
创建哨兵节点: [哨兵:0] → [1 → 2 → 3 → 4 → 5]
第一步:计算链表长度
访问节点值: 1,当前长度: 1
访问节点值: 2,当前长度: 2
访问节点值: 3,当前长度: 3
访问节点值: 4,当前长度: 4
访问节点值: 5,当前长度: 5
链表总长度: 5
完整组数: 1 (每组3个节点)
剩余节点数: 2 (保持原序)
第二步:逐组翻转
--- 翻转第1组 ---
翻转前: [哨兵:0] → [1 → 2 → 3 → 4 → 5]
开始翻转第1组的3个节点
当前组节点: [1, 2, 3]
翻转操作 1: 将节点2移到组首
当前状态: [哨兵:0] → [2 → 1 → 3 → 4 → 5]
翻转操作 2: 将节点3移到组首
当前状态: [哨兵:0] → [3 → 2 → 1 → 4 → 5]
第1组翻转完成,返回新的前驱节点: 1
翻转后: [哨兵:0] → [3 → 2 → 1 → 4 → 5]
最终结果: [3 → 2 → 1 → 4 → 5]
3.5 复杂度分析
时间复杂度: O(n)
- 第一次遍历计算长度:O(n)
- 翻转操作:每个节点最多被操作常数次,总计O(n)
- 总时间复杂度:O(n)
空间复杂度: O(1)
- 只使用了常数级别的额外空间
- 哨兵节点和几个指针变量占用固定空间
3.6 优缺点分析
优点:
- 逻辑清晰:先计算长度,再按组翻转,思路直观
- 易于理解:每个步骤都很明确,便于调试
- 空间效率高:只使用常数额外空间
- 边界处理简单:预先知道组数,避免越界
缺点:
- 需要两次遍历:先计算长度,再执行翻转
- 代码相对复杂:翻转逻辑需要仔细处理指针关系
4. 解法二:递归法
4.1 算法思路
递归法的核心思想是:处理前k个节点,然后递归处理剩余部分,最后将两部分连接起来。
核心思想:
- 递归边界:如果剩余节点不足k个,直接返回头节点
- 当前处理:翻转当前的k个节点
- 递归调用:递归处理第k+1个节点开始的剩余部分
- 连接结果:将翻转的k个节点与递归结果连接
4.2 Java代码实现
/**
* 解法二:递归法
* 时间复杂度:O(n)
* 空间复杂度:O(n/k) - 递归调用栈深度
*/
class Solution2 {
public ListNode reverseKGroup(ListNode head, int k) {
// 边界处理
if (head == null || k <= 1) {
return head;
}
// 检查剩余节点是否足够k个
ListNode current = head;
for (int i = 0; i < k; i++) {
if (current == null) {
// 不足k个节点,直接返回head
return head;
}
current = current.next;
}
// 翻转前k个节点
ListNode newHead = reverseFirstK(head, k);
// 递归处理剩余部分,并连接到翻转后的尾部
// 注意:翻转后,原来的head变成了尾节点
head.next = reverseKGroup(current, k);
return newHead;
}
/**
* 翻转以head开头的前k个节点
* @param head 要翻转的链表头节点
* @param k 要翻转的节点数量
* @return 翻转后的新头节点
*/
private ListNode reverseFirstK(ListNode head, int k) {
ListNode prev = null;
ListNode current = head;
for (int i = 0; i < k; i++) {
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
}
4.3 递归可视化演示
/**
* 带递归过程可视化的实现
*/
public class RecursiveMethodDemo {
private int depth = 0; // 递归深度
public ListNode reverseKGroup(ListNode head, int k) {
System.out.println("=== 递归法K个一组翻转链表 ===");
System.out.println("原始链表: " + printList(head));
System.out.println("k = " + k);
System.out.println();
ListNode result = reverseKGroupHelper(head, k);
System.out.println("\n最终结果: " + printList(result));
return result;
}
private ListNode reverseKGroupHelper(ListNode head, int k) {
String indent = " ".repeat(depth);
System.out.println(indent + "递归层级 " + depth + ":");
System.out.println(indent + " 输入链表: " + printList(head));
System.out.println(indent + " k = " + k);
// 边界处理
if (head == null || k <= 1) {
System.out.println(indent + " 边界条件:返回原链表");
return head;
}
// 检查是否有足够的k个节点
ListNode current = head;
int count = 0;
for (int i = 0; i < k; i++) {
if (current == null) {
System.out.println(indent + " 剩余节点不足" + k + "个,返回原链表");
return head;
}
count++;
current = current.next;
}
System.out.println(indent + " 前" + k + "个节点: " + printListRange(head, k));
System.out.println(indent + " 剩余节点: " + printList(current));
// 翻转前k个节点
System.out.println(indent + " 翻转前" + k + "个节点...");
ListNode newHead = reverseFirstKWithDebug(head, k, indent);
System.out.println(indent + " 翻转后的头节点: " + newHead.val);
System.out.println(indent + " 翻转后的尾节点: " + head.val);
// 递归处理剩余部分
System.out.println(indent + " 递归处理剩余部分...");
depth++;
ListNode recursiveResult = reverseKGroupHelper(current, k);
depth--;
System.out.println(indent + " 递归返回结果: " + printList(recursiveResult));
// 连接翻转部分和递归结果
head.next = recursiveResult;
System.out.println(indent + " 连接后的结果: " + printList(newHead));
return newHead;
}
private ListNode reverseFirstKWithDebug(ListNode head, int k, String indent) {
ListNode prev = null;
ListNode current = head;
System.out.println(indent + " 开始翻转前" + k + "个节点:");
for (int i = 0; i < k; i++) {
ListNode next = current.next;
current.next = prev;
System.out.println(indent + " 步骤" + (i + 1) + ": " + current.val + " → " +
(prev != null ? prev.val : "null"));
prev = current;
current = next;
}
System.out.println(indent + " 翻转完成,新头节点: " + prev.val);
return prev;
}
// 辅助方法:打印指定数量的节点
private String printListRange(ListNode head, int count) {
if (head == null) return "[]";
StringBuilder sb = new StringBuilder();
sb.append("[");
ListNode current = head;
for (int i = 0; i < count && current != null; i++) {
sb.append(current.val);
if (i < count - 1 && current.next != null) {
sb.append(" → ");
}
current = current.next;
}
sb.append("]");
return sb.toString();
}
// 辅助方法:打印链表
private String printList(ListNode head) {
if (head == null) return "null";
StringBuilder sb = new StringBuilder();
sb.append("[");
ListNode current = head;
while (current != null) {
sb.append(current.val);
if (current.next != null) {
sb.append(" → ");
}
current = current.next;
}
sb.append("]");
return sb.toString();
}
}
4.4 递归执行过程分析
递归调用栈的形成过程(输入: [1,2,3,4,5], k=3):
递归层级 0:
输入链表: [1 → 2 → 3 → 4 → 5]
k = 3
前3个节点: [1 → 2 → 3]
剩余节点: [4 → 5]
翻转前3个节点...
开始翻转前3个节点:
步骤1: 1 → null
步骤2: 2 → 1
步骤3: 3 → 2
翻转完成,新头节点: 3
翻转后的头节点: 3
翻转后的尾节点: 1
递归处理剩余部分...
递归层级 1:
输入链表: [4 → 5]
k = 3
剩余节点不足3个,返回原链表
递归返回结果: [4 → 5]
连接后的结果: [3 → 2 → 1 → 4 → 5]
最终结果: [3 → 2 → 1 → 4 → 5]
4.5 递归法复杂度分析
时间复杂度: O(n)
- 每个节点被访问常数次
- 递归深度为O(n/k),但每层处理O(k)个节点
空间复杂度: O(n/k)
- 递归调用栈的深度为n/k
- 每层递归使用常数空间
5. 解法三:栈辅助法
5.1 算法思路
使用栈来辅助翻转操作,将每k个节点入栈,然后依次出栈重新连接。
核心步骤:
- 遍历链表,每k个节点入栈
- 当栈中有k个节点时,依次出栈并重新连接
- 处理剩余不足k个的节点
5.2 Java代码实现
import java.util.Stack;
/**
* 解法三:栈辅助法
* 时间复杂度:O(n)
* 空间复杂度:O(k)
*/
class Solution3 {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k <= 1) {
return head;
}
Stack<ListNode> stack = new Stack<>();
ListNode dummy = new ListNode(0);
ListNode prev = dummy;
ListNode current = head;
while (current != null) {
// 收集k个节点到栈中
for (int i = 0; i < k && current != null; i++) {
stack.push(current);
current = current.next;
}
// 如果栈中有k个节点,则出栈重新连接
if (stack.size() == k) {
while (!stack.isEmpty()) {
prev.next = stack.pop();
prev = prev.next;
}
prev.next = current; // 连接到下一组
} else {
// 不足k个节点,保持原有顺序
prev.next = head;
// 找到最后k个节点的起始位置
for (int i = 0; i < stack.size(); i++) {
ListNode temp = head;
for (int j = 0; j < i; j++) {
temp = temp.next;
}
prev.next = temp;
prev = prev.next;
}
break;
}
}
return dummy.next;
}
}
5.3 栈法的简化实现
/**
* 解法三:栈辅助法(简化版)
* 更清晰的实现逻辑
*/
class Solution3Simplified {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k <= 1) {
return head;
}
// 首先检查总长度
int length = getLength(head);
if (length < k) {
return head;
}
Stack<ListNode> stack = new Stack<>();
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
ListNode current = head;
while (current != null) {
// 将k个节点入栈
for (int i = 0; i < k && current != null; i++) {
stack.push(current);
current = current.next;
}
// 如果栈中恰好有k个节点,则出栈重新连接
if (stack.size() == k) {
while (!stack.isEmpty()) {
tail.next = stack.pop();
tail = tail.next;
}
tail.next = null; // 断开与后续节点的连接
} else {
// 不足k个节点,将栈中节点按原顺序连接
ListNode[] nodes = new ListNode[stack.size()];
for (int i = stack.size() - 1; i >= 0; i--) {
nodes[i] = stack.pop();
}
for (ListNode node : nodes) {
tail.next = node;
tail = tail.next;
}
tail.next = null;
break;
}
}
return dummy.next;
}
private int getLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
}
6. 完整测试用例
6.1 测试框架
import java.util.*;
/**
* K个一组翻转链表完整测试类
*/
public class ReverseKGroupTest {
/**
* 创建测试链表的辅助方法
*/
public static ListNode createList(int[] values) {
if (values.length == 0) {
return null;
}
ListNode head = new ListNode(values[0]);
ListNode current = head;
for (int i = 1; i < values.length; i++) {
current.next = new ListNode(values[i]);
current = current.next;
}
return head;
}
/**
* 将链表转换为数组,便于比较结果
*/
public static int[] listToArray(ListNode head) {
List<Integer> result = new ArrayList<>();
ListNode current = head;
while (current != null) {
result.add(current.val);
current = current.next;
}
return result.stream().mapToInt(i -> i).toArray();
}
/**
* 运行所有测试用例
*/
public static void runAllTests() {
System.out.println("=== K个一组翻转链表完整测试 ===\n");
// 测试用例
TestCase[] testCases = {
new TestCase(new int[]{1, 2, 3, 4, 5}, 2,
new int[]{2, 1, 4, 3, 5}, "示例1:k=2,有剩余"),
new TestCase(new int[]{1, 2, 3, 4, 5}, 3,
new int[]{3, 2, 1, 4, 5}, "示例2:k=3,有剩余"),
new TestCase(new int[]{1, 2, 3, 4, 5}, 1,
new int[]{1, 2, 3, 4, 5}, "k=1,不翻转"),
new TestCase(new int[]{1, 2, 3, 4, 5}, 5,
new int[]{5, 4, 3, 2, 1}, "k=5,完全翻转"),
new TestCase(new int[]{1}, 1,
new int[]{1}, "单节点,k=1"),
new TestCase(new int[]{}, 2,
new int[]{}, "空链表"),
new TestCase(new int[]{1, 2}, 2,
new int[]{2, 1}, "正好k个节点"),
new TestCase(new int[]{1, 2, 3}, 2,
new int[]{2, 1, 3}, "一组完整+一个剩余"),
new TestCase(new int[]{1, 2, 3, 4}, 2,
new int[]{2, 1, 4, 3}, "两组完整"),
new TestCase(new int[]{1, 2, 3, 4, 5, 6}, 3,
new int[]{3, 2, 1, 6, 5, 4}, "两组完整"),
new TestCase(new int[]{1, 2, 3, 4, 5, 6, 7}, 3,
new int[]{3, 2, 1, 6, 5, 4, 7}, "两组完整+一个剩余"),
new TestCase(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 4,
new int[]{4, 3, 2, 1, 8, 7, 6, 5, 9, 10}, "k=4的复杂情况")
};
Solution1 solution1 = new Solution1();
Solution2 solution2 = new Solution2();
Solution3Simplified solution3 = new Solution3Simplified();
for (int i = 0; i < testCases.length; i++) {
TestCase testCase = testCases[i];
System.out.println("测试用例 " + (i + 1) + ": " + testCase.description);
System.out.println("输入链表: " + Arrays.toString(testCase.input));
System.out.println("k = " + testCase.k);
System.out.println("期望结果: " + Arrays.toString(testCase.expected));
// 创建测试链表(每种方法需要独立的链表)
ListNode head1 = createList(testCase.input);
ListNode head2 = createList(testCase.input);
ListNode head3 = createList(testCase.input);
// 测试迭代法
ListNode result1 = solution1.reverseKGroup(head1, testCase.k);
int[] array1 = listToArray(result1);
// 测试递归法
ListNode result2 = solution2.reverseKGroup(head2, testCase.k);
int[] array2 = listToArray(result2);
// 测试栈法
ListNode result3 = solution3.reverseKGroup(head3, testCase.k);
int[] array3 = listToArray(result3);
System.out.println("迭代法结果: " + Arrays.toString(array1));
System.out.println("递归法结果: " + Arrays.toString(array2));
System.out.println("栈法结果: " + Arrays.toString(array3));
boolean passed = Arrays.equals(array1, testCase.expected) &&
Arrays.equals(array2, testCase.expected) &&
Arrays.equals(array3, testCase.expected);
System.out.println("测试结果: " + (passed ? "✅ 通过" : "❌ 失败"));
System.out.println();
}
}
/**
* 性能测试
*/
public static void performanceTest() {
System.out.println("=== 性能测试 ===");
// 创建大链表进行性能测试
int[] largeArray = new int[10000];
for (int i = 0; i < largeArray.length; i++) {
largeArray[i] = i + 1;
}
Solution1 solution1 = new Solution1();
Solution2 solution2 = new Solution2();
Solution3Simplified solution3 = new Solution3Simplified();
int k = 3;
// 测试迭代法性能
ListNode head1 = createList(largeArray);
long start1 = System.nanoTime();
ListNode result1 = solution1.reverseKGroup(head1, k);
long end1 = System.nanoTime();
System.out.println("迭代法耗时: " + (end1 - start1) / 1_000_000.0 + " ms");
// 测试递归法性能(可能栈溢出,所以用较小的数组)
int[] mediumArray = new int[3000];
for (int i = 0; i < mediumArray.length; i++) {
mediumArray[i] = i + 1;
}
ListNode head2 = createList(mediumArray);
long start2 = System.nanoTime();
ListNode result2 = solution2.reverseKGroup(head2, k);
long end2 = System.nanoTime();
System.out.println("递归法耗时: " + (end2 - start2) / 1_000_000.0 + " ms");
// 测试栈法性能
ListNode head3 = createList(largeArray);
long start3 = System.nanoTime();
ListNode result3 = solution3.reverseKGroup(head3, k);
long end3 = System.nanoTime();
System.out.println("栈法耗时: " + (end3 - start3) / 1_000_000.0 + " ms");
}
/**
* 测试用例类
*/
static class TestCase {
int[] input;
int k;
int[] expected;
String description;
TestCase(int[] input, int k, int[] expected, String description) {
this.input = input;
this.k = k;
this.expected = expected;
this.description = description;
}
}
public static void main(String[] args) {
runAllTests();
performanceTest();
}
}
7. 算法复杂度对比
7.1 详细对比表格
解法 | 时间复杂度 | 空间复杂度 | 实现难度 | 可读性 | 性能表现 | 推荐度 |
---|---|---|---|---|---|---|
迭代法(预计算) | O(n) | O(1) | 中等 | 中等 | 优秀 | ⭐⭐⭐⭐⭐ |
递归法 | O(n) | O(n/k) | 简单 | 高 | 中等 | ⭐⭐⭐⭐ |
栈辅助法 | O(n) | O(k) | 简单 | 高 | 中等 | ⭐⭐⭐ |
7.2 选择建议
推荐使用迭代法的情况:
- 面试或实际项目中(空间复杂度最优)
- 对内存使用有严格要求
- 需要处理大规模数据
- 追求最佳性能
推荐使用递归法的情况:
- 学习算法思想,理解分治概念
- 代码简洁性优先
- 链表规模中等,不会导致栈溢出
- 教学演示场景
推荐使用栈法的情况:
- 初学者理解翻转过程
- 需要直观的可视化效果
- k值相对较小的情况
7.3 空间复杂度深入分析
迭代法: O(1)
- 只使用固定数量的指针变量
- 不随输入规模增长
递归法: O(n/k)
- 递归调用栈深度为⌊n/k⌋
- 当k=1时,空间复杂度为O(n)
- 当k=n时,空间复杂度为O(1)
栈法: O(k)
- 栈中最多存储k个节点
- 空间使用与k成正比,与n无关
8. 常见错误与调试技巧
8.1 常见错误分析
1. 边界条件处理不当
// 错误写法:没有检查k的边界值
public ListNode reverseKGroup(ListNode head, int k) {
// 缺少k <= 1的检查
if (head == null) {
return head;
}
// ...
}
// 正确写法:完整的边界检查
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k <= 1) { // 检查k <= 1的情况
return head;
}
// ...
}
2. 节点连接错误
// 错误写法:连接顺序错误导致链表断裂
ListNode next = current.next;
prev.next = next; // 错误:丢失了current节点
current.next = next.next;
next.next = current;
// 正确写法:正确的连接顺序
ListNode next = current.next;
current.next = next.next; // 先断开current与next的连接
next.next = prev.next; // next指向prev的下一个节点
prev.next = next; // prev指向next
3. 递归中的连接错误
// 错误写法:连接位置错误
public ListNode reverseKGroup(ListNode head, int k) {
// ... 检查和翻转前k个节点
ListNode newHead = reverseFirstK(head, k);
// 错误:应该是head.next而不是newHead.next
newHead.next = reverseKGroup(current, k);
return newHead;
}
// 正确写法:正确连接
public ListNode reverseKGroup(ListNode head, int k) {
// ... 检查和翻转前k个节点
ListNode newHead = reverseFirstK(head, k);
// 正确:head在翻转后成为尾节点
head.next = reverseKGroup(current, k);
return newHead;
}
4. 栈操作中的顺序错误
// 错误写法:没有检查栈的大小
while (current != null) {
for (int i = 0; i < k && current != null; i++) {
stack.push(current);
current = current.next;
}
// 错误:没有检查栈是否有足够的元素
while (!stack.isEmpty()) {
prev.next = stack.pop();
prev = prev.next;
}
}
// 正确写法:检查栈大小
while (current != null) {
for (int i = 0; i < k && current != null; i++) {
stack.push(current);
current = current.next;
}
// 正确:只有当栈中有k个元素时才处理
if (stack.size() == k) {
while (!stack.isEmpty()) {
prev.next = stack.pop();
prev = prev.next;
}
}
}
8.2 调试技巧
1. 添加详细的日志输出
private void debugPrint(String operation, ListNode head, int k, int step) {
System.out.println("=== " + operation + " (步骤" + step + ") ===");
System.out.println("当前链表: " + printList(head));
System.out.println("k = " + k);
System.out.println();
}
2. 使用断言验证中间状态
// 在关键步骤添加断言
assert prev != null : "prev不应为null";
assert groupTail != null : "groupTail不应为null";
assert k > 0 : "k应该大于0";
3. 分步骤验证链表完整性
private boolean isValidList(ListNode head, int expectedLength) {
int count = 0;
ListNode current = head;
Set<ListNode> visited = new HashSet<>();
while (current != null) {
if (visited.contains(current)) {
System.out.println("检测到环!");
return false;
}
visited.add(current);
count++;
current = current.next;
}
if (count != expectedLength) {
System.out.println("长度不匹配,期望: " + expectedLength + ", 实际: " + count);
return false;
}
return true;
}
4. 使用可视化辅助工具
/**
* 可视化打印链表连接关系
*/
private void printConnections(ListNode head, int limit) {
ListNode current = head;
int count = 0;
System.out.print("连接关系: ");
while (current != null && count < limit) {
System.out.print(current.val);
if (current.next != null) {
System.out.print(" → " + current.next.val);
} else {
System.out.print(" → null");
}
System.out.print(", ");
current = current.next;
count++;
}
System.out.println();
}
9. 相关题目与拓展
9.1 LeetCode 相关题目
链表翻转类:
- 206. 反转链表:K个一组翻转的基础
- 92. 反转链表 II:反转指定区间的链表
- 24. 两两交换链表中的节点:K=2的特殊情况
- 143. 重排链表:链表的复杂重新排列
分组处理类:
- 86. 分隔链表:按值分组重排链表
- 328. 奇偶链表:按位置分组
- 725. 分隔链表:按长度分组
- 817. 链表组件:连通组件问题
递归与分治:
- 21. 合并两个有序链表:递归合并
- 23. 合并K个升序链表:分治法应用
- 148. 排序链表:归并排序的递归实现
9.2 算法模式的扩展应用
1. 分块处理模式
/**
* 通用的链表分块处理框架
*/
public class LinkedListBlockProcessor {
public ListNode processInBlocks(ListNode head, int blockSize,
Function<ListNode, Integer, ListNode> processor) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
ListNode current = head;
while (current != null) {
// 找到当前块的结束位置
ListNode blockEnd = current;
for (int i = 1; i < blockSize && blockEnd.next != null; i++) {
blockEnd = blockEnd.next;
}
// 处理当前块
ListNode nextBlock = blockEnd.next;
blockEnd.next = null; // 临时断开
ListNode processedBlock = processor.apply(current, blockSize);
// 连接处理后的块
tail.next = processedBlock;
// 移动到处理后块的末尾
while (tail.next != null) {
tail = tail.next;
}
current = nextBlock;
}
return dummy.next;
}
}
2. K分组的其他应用
/**
* K个一组求和
*/
public List<Integer> sumInKGroups(ListNode head, int k) {
List<Integer> result = new ArrayList<>();
ListNode current = head;
while (current != null) {
int sum = 0;
int count = 0;
// 计算当前k个节点的和
for (int i = 0; i < k && current != null; i++) {
sum += current.val;
count++;
current = current.next;
}
if (count == k) {
result.add(sum);
}
}
return result;
}
/**
* K个一组取最大值
*/
public List<Integer> maxInKGroups(ListNode head, int k) {
List<Integer> result = new ArrayList<>();
ListNode current = head;
while (current != null) {
int max = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < k && current != null; i++) {
max = Math.max(max, current.val);
count++;
current = current.next;
}
if (count == k) {
result.add(max);
}
}
return result;
}
9.3 实际应用场景
1. 数据流处理
- 网络数据包的分组处理
- 日志数据的批量处理
- 流式计算中的窗口操作
2. 内存管理
- 内存页面的分组管理
- 缓存块的批量替换
- 垃圾回收的分代处理
3. 并行计算
- 任务的批量分发
- 数据的分块并行处理
- MapReduce中的数据分片
4. 数据库操作
- 批量插入操作
- 分页查询处理
- 事务的批量提交
10. 学习建议与总结
10.1 学习步骤建议
第一步:掌握基础知识
- 熟练掌握链表的基本操作
- 理解指针和引用的概念
- 掌握链表翻转的基础算法(LeetCode 206)
- 练习两两交换链表节点(LeetCode 24)
第二步:理解分组思想
- 理解K个一组的分组概念
- 掌握完整组和不完整组的处理差异
- 学会使用哨兵节点简化操作
- 练习手动模拟翻转过程
第三步:掌握多种解法
- 先实现迭代法,理解基本思路
- 学习递归法,理解分治思想
- 尝试栈法,理解数据结构的辅助作用
- 对比不同解法的优缺点
第四步:深入理解和应用
- 分析时间和空间复杂度
- 学习调试技巧和错误预防
- 练习相关的拓展题目
- 理解算法在实际中的应用
10.2 面试要点
常见面试问题:
- “请实现K个一组翻转链表”
- “能否优化空间复杂度到O(1)?”
- “递归和迭代方法有什么区别?”
- “如何处理链表长度不是k的倍数的情况?”
- “这个算法可以用在什么实际场景中?”
回答要点:
- 多种解法:能够提供迭代、递归、栈三种解法
- 复杂度分析:准确分析时间和空间复杂度
- 边界处理:展示对各种边界情况的考虑
- 代码质量:代码简洁、逻辑清晰、注释完整
- 实际应用:能够联系实际应用场景
10.3 常见陷阱与注意事项
1. 指针操作顺序
// 必须按正确顺序修改指针,避免链表断裂
// 正确顺序:先断开要移动的节点,再建立新连接
2. 边界条件遗漏
// 必须考虑:空链表、k=1、链表长度小于k、k等于链表长度等情况
3. 递归深度问题
// 当k=1或链表很长时,递归可能导致栈溢出
// 需要根据实际情况选择合适的解法
4. 内存泄漏风险
// 在某些语言中,要注意避免循环引用
// Java有垃圾回收,但仍要注意指针的正确性
10.4 实际应用价值
- 算法思维训练:培养分组处理和分治思想
- 指针操作精通:提高复杂指针操作的准确性
- 递归理解深化:理解递归在链表问题中的应用
- 工程实践能力:学会选择合适的算法和数据结构
10.5 扩展学习方向
1. 高级链表算法
- 跳表的实现和应用
- LRU缓存的链表实现
- 并发链表的设计
2. 分治算法深入
- 归并排序在链表上的实现
- 快速排序的链表版本
- 分治法在树结构中的应用
3. 系统设计中的应用
- 分布式系统中的数据分片
- 负载均衡算法的设计
- 缓存系统的分层设计
10.6 最终建议
- 多练习:通过大量练习巩固各种解法
- 画图理解:通过画图理解复杂的指针操作过程
- 对比学习:对比不同解法的优缺点和适用场景
- 举一反三:将学到的技巧应用到其他链表问题
- 注重实践:在实际项目中寻找应用场景
总结:
K个一组翻转链表是链表操作中的经典难题,它完美地结合了分组思想、指针操作和算法设计技巧。通过这道题,我们不仅可以掌握链表的高级操作技能,还能学习到重要的算法设计模式,如分治思想、递归设计、迭代优化等。