链表算法三道

目录

  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个链表分治并。

相关推荐
GIS数据转换器3 分钟前
城市排水生命线安全运行监测平台深度解析
java·运维·人工智能·python·安全·数据挖掘·无人机
bIo7lyA8v4 分钟前
算法复杂度评估的实验统计方法与可视化的技术8
算法
李老师讲编程25 分钟前
中国电子学会图形化2020.12月Scratch三级考级题
算法·scratch·信息学奥赛·图形化编程·scratch素材
华如锦41 分钟前
面了很多 Java转AI Agent方向,一些面试题总结
java·开发语言·人工智能·python·ai
睡不醒男孩03082344 分钟前
CLup 6.x 版本中针对StarRocks 存算一体集群的完整操作手册
java·服务器·网络·clup
退休倒计时1 小时前
【每日一题】LeetCode 53. 最大子数组和 TypeScript
数据结构·算法·leetcode·typescript
旖-旎1 小时前
FloodFill(图像渲染)(1)
c++·算法·深度优先·力扣
戴西软件1 小时前
戴西 DLM 许可授权管理系统:破解无网络环境下工业软件授权难题,助力制造企业降本增效
网络·人工智能·python·深度学习·程序人生·算法·制造
2601_961875241 小时前
法考资料2026|全套|资料已整理
数据结构·算法·链表·贪心算法·eclipse·线性回归·动态规划