链表算法三道

目录

  1. 链表反转

  2. 445. 两数相加 II

  3. 23. 合并K个升序链表(分治版)


一、链表反转

1.1 问题描述

将单链表的所有节点指针方向反转,使链表从 1→2→3→null 变为 3→2→1→null

1.2 核心思想

把每个节点的 next 指针从"指向后"改成"指向前"

复制代码
原链表:  1 → 2 → 3 → 4 → null
目标:    null ← 1 ← 2 ← 3 ← 4  (即 4 → 3 → 2 → 1 → null)

1.3 三指针分工

指针 作用 说明
prev 指向已反转部分的头部 "前一个",已处理完毕
cur 指向正在处理的节点 "当前的",处理中
next 暂存下一个待处理节点 保险绳,防止断链

1.4 代码实现

复制代码
public ListNode reverseList(ListNode head) {
    ListNode prev = null;   // 已反转部分的头部(初始为空)
    ListNode cur = head;    // 当前处理的节点
    
    while (cur != null) {
        ListNode next = cur.next;  // 1. 暂存下一个(保险绳)
        cur.next = prev;            // 2. 反转指针方向
        prev = cur;                 // 3. prev前进
        cur = next;                 // 4. cur前进
    }
    
    return prev;  // cur为null时,prev指向新头节点
}

1.5 图解全过程

复制代码
初始:  null    1 → 2 → 3 → null
       ↑       ↑
      prev    cur/next

第1轮后:  null ← 1    2 → 3 → null
                ↑      ↑
               prev   cur/next

第2轮后:  null ← 1 ← 2    3 → null
                     ↑     ↑
                    prev   cur/next

第3轮后:  null ← 1 ← 2 ← 3    null
                          ↑    ↑
                         prev  cur(结束)

返回 prev = 3 (新头节点)

1.6 四步口诀

1. 买保险: next = cur.next(暂存后路)
2. 反方向: cur.next = prev(调转箭头)
3. 往前走: prev = cur(后卫跟进)
4. 继续走: cur = next(前锋前进)

1.7 复杂度分析

类型 复杂度 说明
时间 O(n) 遍历一次链表
空间 O(1) 只使用三个指针

二、445. 两数相加 II

2.1 问题描述

两个非空 链表表示两个非负整数,最高位在前,每位存一位数字。返回相加后的链表(同样最高位在前)。

与题2的区别:

题号 存储方式 示例
2 逆序(个位在前) 2→4→3 表示 342
445 正序(高位在前) 7→2→4→3 表示 7243

2.2 核心思想

反转 → 相加(复用题2)→ 再反转

复制代码
l1: 7 → 2 → 4 → 3  (表示 7243)
l2: 5 → 6 → 4      (表示 564)

步骤1: 反转两个链表
  l1: 3 → 4 → 2 → 7
  l2: 4 → 6 → 5

步骤2: 相加(逆序相加,同题2)
  3+4=7, 4+6=10(写0进1), 2+5+1=8, 7+0=7
  结果: 7 → 0 → 8 → 7

步骤3: 反转结果
  最终: 7 → 8 → 0 → 7  (表示 7807)
  
验证: 7243 + 564 = 7807 ✓

2.3 代码实现

复制代码
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // 1. 反转两个链表(变成逆序,方便从低位加)
        ListNode n1 = reverse(l1);
        ListNode n2 = reverse(l2);
        
        // 2. 相加(此时和题2完全一样)
        ListNode dummy = new ListNode(), tail = dummy;
        int carry = 0;
        
        while (n1 != null || n2 != null || carry != 0) {
            int v1 = (n1 != null) ? n1.val : 0;
            int v2 = (n2 != null) ? n2.val : 0;
            
            int sum = v1 + v2 + carry;      // 先算总和
            int digit = sum % 10;            // 当前位(个位)
            carry = sum / 10;                // 新进位
            
            tail.next = new ListNode(digit); // 接个位
            tail = tail.next;
            
            if (n1 != null) n1 = n1.next;
            if (n2 != null) n2 = n2.next;
        }
        
        // 3. 反转结果(变回正序)
        return reverse(dummy.next);
    }
    
    // 链表反转(复用上一节代码)
    private ListNode reverse(ListNode head) {
        ListNode prev = null, cur = head;
        while (cur != null) {
            ListNode next = cur.next;
            cur.next = prev;
            prev = cur;
            cur = next;
        }
        return prev;
    }
}

2.4 关键技巧

技巧 代码 作用
哑节点 ListNode dummy = new ListNode() 简化头节点处理
0补位 (n1 != null) ? n1.val : 0 处理链表长度不一
进位条件 `while (...
三次反转 输入两链表反转,结果再反转 将正序问题转为逆序问题

2.5 复杂度分析

类型 复杂度 说明
时间 O(max(m, n)) 三次遍历,常数倍
空间 O(max(m, n)) 结果链表空间

2.6 常见错误

复制代码
// ❌ 错误:进位和结果搞反
in = (v1 + v2 + carry) % 10;      // 把个位存进carry变量!
int result = (v1 + v2 + in) / 10;  // 错误!in已被修改

// ✅ 正确:先存sum,再拆分
int sum = v1 + v2 + carry;
int digit = sum % 10;   // 当前位
carry = sum / 10;        // 新进位

三、23. 合并K个升序链表(分治版)

3.1 问题描述

给定链表数组 lists,每个链表已按升序排列,合并为一个升序链表返回。

复制代码
输入: lists = [[1,4,5], [1,3,4], [2,6]]
输出: 1→1→2→3→4→4→5→6

3.2 核心思想:分治归并

将k个链表两两配对,逐层合并,类似归并排序

复制代码
lists: [L1, L2, L3, L4, L5, L6, L7, L8]

第1轮: (L1+L2), (L3+L4), (L5+L6), (L7+L8)  → 4个结果
第2轮: (L12+L34), (L56+L78)                 → 2个结果  
第3轮: (L1234+L5678)                        → 1个结果

3.3 递归三要素

要素 内容
函数定义 mergeRange(lists, left, right) 合并 lists[left..right]
终止条件 left == right,只有一个链表,直接返回
递归逻辑 分两半分别合并,再合并两个结果

3.4 代码实现

复制代码
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) return null;
        return mergeRange(lists, 0, lists.length - 1);
    }
    
    // 分治:合并 lists[left..right]
    private ListNode mergeRange(ListNode[] lists, int left, int right) {
        // 【终止条件】只有一个,直接返回
        if (left == right) {
            return lists[left];
        }
        
        // 【分】找中点,分成两半
        int mid = left + (right - left) / 2;
        
        // 【治】递归合并左半部分
        ListNode l1 = mergeRange(lists, left, mid);
        
        // 【治】递归合并右半部分
        ListNode l2 = mergeRange(lists, mid + 1, right);
        
        // 【合】合并两个有序链表
        return mergeTwoLists(l1, l2);
    }
    
    // 合并两个有序链表(复用题21代码)
    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(), tail = dummy;
        
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                tail.next = l1;
                l1 = l1.next;
            } else {
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        
        tail.next = (l1 != null) ? l1 : l2;  // 接剩余部分
        return dummy.next;
    }
}

3.5 递归展开图解

lists = [L1, L2, L3, L4] 为例:

复制代码
调用: mergeRange(0, 3)  // 合并 L1,L2,L3,L4
       │
       ├── mid = 1
       │
       ├── 左: mergeRange(0, 1)  // 合并 L1,L2
       │       │
       │       ├── mid = 0
       │       ├── 左: mergeRange(0, 0) → L1 (base case)
       │       └── 右: mergeRange(1, 1) → L2 (base case)
       │       └── 合并 L1+L2 → L12
       │
       └── 右: mergeRange(2, 3)  // 合并 L3,L4
               │
               ├── mid = 2
               ├── 左: mergeRange(2, 2) → L3 (base case)
               └── 右: mergeRange(3, 3) → L4 (base case)
               └── 合并 L3+L4 → L34
       │
       └── 合并 L12+L34 → L1234

3.6 为什么比暴力合并快?

方式 每个链表被合并次数 总比较次数
暴力(逐个合并) 第i个合并i次 1+2+...+(k-1) = O(k²)
分治(两两配对) 每层合并1次,共log k层 k × log k = O(k log k)

暴力的问题: 最后一个链表要和前面所有结果比较,次数不均衡
分治的优势: 每个链表只在每一层参与一次合并,完全均衡!

3.7 复杂度分析

类型 复杂度 说明
时间 O(kn log k) k个链表,每个长度n,log k层
空间 O(log k) 递归栈深度

3.8 记忆口诀

分:找中点劈两半
治:递归合并各自半
合:两个结果再合并
终:只剩一个就返回


四、三题对比总结

题目 核心技巧 关键操作 复杂度
链表反转 三指针 next暂存、cur.next = prev、双指针前移 时间O(n) 空间O(1)
两数相加II 三次反转 反转→相加→反转,将正序转逆序处理 时间O(n) 空间O(n)
合并K个链表 分治归并 递归分半、合并两个、逐层归并 时间O(kn log k) 空间O(log k)

五、通用模板速查

5.1 哑节点 + 尾指针模板

复制代码
ListNode dummy = new ListNode(), tail = dummy;
// 处理逻辑...
tail.next = new ListNode(val);  // 或 tail.next = 已有节点
tail = tail.next;
return dummy.next;

5.2 链表反转模板

复制代码
ListNode prev = null, cur = head;
while (cur != null) {
    ListNode next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
}
return prev;

5.3 合并两个链表模板

复制代码
ListNode dummy = new ListNode(), tail = dummy;
while (l1 != null && l2 != null) {
    if (l1.val <= l2.val) {
        tail.next = l1; l1 = l1.next;
    } else {
        tail.next = l2; l2 = l2.next;
    }
    tail = tail.next;
}
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;

5.4 分治递归模板

复制代码
private ListNode mergeRange(ListNode[] lists, int left, int right) {
    if (left == right) return lists[left];
    int mid = left + (right - left) / 2;
    ListNode l1 = mergeRange(lists, left, mid);
    ListNode l2 = mergeRange(lists, mid + 1, right);
    return merge(l1, l2);
}

六、一句话总结

反转用三指针,正序加法先反转,K个链表分治并。

相关推荐
百锦再1 小时前
Java InputStream和OutputStream实现类完全指南
java·开发语言·spring boot·python·struts·spring cloud·kafka
二年级程序员1 小时前
一篇文章掌握“栈”
c语言·数据结构
再难也得平2 小时前
[LeetCode刷题]128.最长连续序列(从零开始的java题解)
java·算法·leetcode
亓才孓2 小时前
【MyBatis Exception】SQLSyntaxErrorException(按批修改不加配置会报错)
java·开发语言·mybatis
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-双指针》--05有效三角形的个数,06查找总价值为目标值的两个商品
c++·算法
亓才孓2 小时前
【MyBatis Runtime Exception】自动驼峰映射对Map不生效,应该在查询中起别名
java·windows·mybatis
ArturiaZ2 小时前
【day31】
开发语言·c++·算法
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-双指针》--07三数之和,08四数之和
c++·算法
没有bug.的程序员2 小时前
调试艺术进阶:从断点内核到日志动态化的高效问题定位深度实战指南
java·调试·断点·日志动态化