LeetCode 热题 100 之 138. 随机链表的复制 148. 排序链表 23. 合并 K 个升序链表 146. LRU 缓存

  1. 随机链表的复制

  2. 排序链表

  3. 合并 K 个升序链表

  4. LRU 缓存

138. 随机链表的复制

复制代码
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return null;
        Map<Node, Node> map = new HashMap<>();
        Node cur = head;

        // 第一次遍历:复制所有节点,建立映射
        while (cur != null) {
            map.put(cur, new Node(cur.val));
            cur = cur.next;
        }

        // 第二次遍历:设置 next 和 random 指针
        cur = head;
        while (cur != null) {
            Node copyNode = map.get(cur);
            copyNode.next = map.get(cur.next);
            copyNode.random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(head);
    }
}
解题思路1:哈希表法

遍历原链表,用哈希表建立「原节点 → 新节点」的映射。

再次遍历原链表,根据映射关系,为新节点设置 nextrandom 指针。

复制代码
public class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return null;

        // 1. 复制节点并插入到原节点后
        Node cur = head;
        while (cur != null) {
            Node copy = new Node(cur.val);
            copy.next = cur.next;
            cur.next = copy;
            cur = copy.next;
        }

        // 2. 设置新节点的 random 指针
        cur = head;
        while (cur != null) {
            Node copy = cur.next;
            if (cur.random != null) {
                copy.random = cur.random.next;
            }
            cur = copy.next;
        }

        // 3. 拆分原链表和复制链表
        cur = head;
        Node dummy = new Node(0);
        Node copyCur = dummy;
        while (cur != null) {
            Node copy = cur.next;
            cur.next = copy.next;
            copyCur.next = copy;
            copyCur = copy;
            cur = cur.next;
        }
        return dummy.next;
    }
}
解题思路2:原地复制 + 拆分

复制节点 :在原链表每个节点后插入其副本,形成 原1 → 新1 → 原2 → 新2... 的结构。

设置 random 指针 :遍历链表,新节点的 random 指向原节点 random 指向节点的下一个副本。

拆分 链表:将原节点和新节点分离,得到复制后的链表。

148. 排序链表

复制代码
/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */

class Solution {
    public ListNode sortList(ListNode head) {
        // 递归终止条件:空链表或只有一个节点
        if (head == null || head.next == null) {
            return head;
        }

        // 1. 快慢指针找中点
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode mid = slow.next;
        slow.next = null; // 拆分链表

        // 2. 递归排序左右两部分
        ListNode left = sortList(head);
        ListNode right = sortList(mid);

        // 3. 合并两个有序链表
        return merge(left, right);
    }

    // 合并两个有序链表
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = l1 != null ? l1 : l2;
        return dummy.next;
    }
}
解题思路1:归并排序(推荐,时间复杂度 O (n log n),空间复杂度 O (log n))

这是链表排序的经典解法,利用快慢指针找中点,递归拆分后有序合并。

复制代码
public class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        // 计算链表长度
        int len = 0;
        ListNode cur = head;
        while (cur != null) {
            len++;
            cur = cur.next;
        }

        ListNode dummy = new ListNode(0);
        dummy.next = head;

        // 按长度 1,2,4... 分组合并
        for (int step = 1; step < len; step <<= 1) {
            ListNode pre = dummy;
            cur = dummy.next;
            while (cur != null) {
                // 拆分第一组
                ListNode left = cur;
                ListNode right = split(left, step);
                // 拆分第二组
                cur = split(right, step);
                // 合并两组
                pre.next = merge(left, right);
                // 移动 pre 到合并后链表尾部
                while (pre.next != null) {
                    pre = pre.next;
                }
            }
        }
        return dummy.next;
    }

    // 从 head 开始拆分 step 个节点,返回剩余部分的头节点
    private ListNode split(ListNode head, int step) {
        if (head == null) return null;
        while (step > 1 && head.next != null) {
            head = head.next;
            step--;
        }
        ListNode next = head.next;
        head.next = null;
        return next;
    }

    // 合并两个有序链表(同方法一)
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        cur.next = l1 != null ? l1 : l2;
        return dummy.next;
    }
}
解题思路2:自底向上归并排序(迭代版,空间复杂度 O (1))

避免递归栈开销,通过迭代方式按长度 1,2,4... 分组合并。

23. 合并 K 个升序链表

复制代码
/**
 * Definition for singly-linked list.
 * 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; }
 * }
 */
 class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) return null;
        
        // 最小堆:按节点值升序排列
        PriorityQueue<ListNode> heap = new PriorityQueue<>((a, b) -> a.val - b.val);
        // 将所有非空链表的头节点入堆
        for (ListNode node : lists) {
            if (node != null) heap.offer(node);
        }

        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (!heap.isEmpty()) {
            // 取出当前最小节点
            ListNode minNode = heap.poll();
            cur.next = minNode;
            cur = cur.next;
            // 将该节点的下一个节点入堆(如果存在)
            if (minNode.next != null) heap.offer(minNode.next);
        }
        return dummy.next;
    }
}
解题思路1:优先队列(最小堆)
  • 所有 链表 的头节点放进最小堆,堆顶永远是当前最小节点

  • 取出堆顶节点接到结果上

  • 把这个节点的下一个节点再放回堆

  • 重复直到堆空v

    class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;
    return merge(lists, 0, lists.length - 1);
    }

    复制代码
      // 递归分治:合并 [left, right] 区间的链表
      private ListNode merge(ListNode[] lists, int left, int right) {
          if (left == right) return lists[left];
          int mid = left + (right - left) / 2;
          ListNode l1 = merge(lists, left, mid);
          ListNode l2 = merge(lists, mid + 1, right);
          return mergeTwoLists(l1, l2);
      }
    
      // 合并两个有序链表(复用之前的代码)
      private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
          ListNode dummy = new ListNode(0);
          ListNode cur = dummy;
          while (l1 != null && l2 != null) {
              if (l1.val < l2.val) {
                  cur.next = l1;
                  l1 = l1.next;
              } else {
                  cur.next = l2;
                  l2 = l2.next;
              }
              cur = cur.next;
          }
          cur.next = l1 != null ? l1 : l2;
          return dummy.next;
      }

    }

解题思路2:分治法(归并思想)
  • 把 K 个链表不断分成两组,直到每组只剩 1 个

  • 两两合并成有序链表

  • 层层合并,最后得到一条完整链表

    public class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;
    ListNode res = lists[0];
    for (int i = 1; i < lists.length; i++) {
    res = mergeTwoLists(res, lists[i]);
    }
    return res;
    }

    复制代码
      // 合并两个有序链表(同上)
      private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
          ListNode dummy = new ListNode(0);
          ListNode cur = dummy;
          while (l1 != null && l2 != null) {
              if (l1.val < l2.val) {
                  cur.next = l1;
                  l1 = l1.next;
              } else {
                  cur.next = l2;
                  l2 = l2.next;
              }
              cur = cur.next;
          }
          cur.next = l1 != null ? l1 : l2;
          return dummy.next;
      }

    }

解题思路3:顺序合并(暴力法)
  • 先拿第 1 个链表当结果

  • 依次把第 2、3...K 个 链表和结果合并

  • 全部合并完就是答案

146. LRU 缓存

什么是 LRU 缓存?

LRU 是 Least Recently Used (最近最少使用)的缩写,是一种常见的缓存淘汰策略

核心思想:

  • 缓存容量有限,当缓存满了要插入新数据时,优先删除「最久没被访问」的数据,为新数据腾出空间。

  • 「访问」包括两种操作:get(读取)和 put(更新 / 插入),只要操作了,就把该数据标记为「最近刚使用」。

    class LRUCache {
    private static class Node {
    int key, value;
    Node prev, next;

    复制代码
          Node(int k, int v) {
              key = k;
              value = v;
          }
      }
    
      private final int capacity;
      private final Node dummy = new Node(0, 0); // 哨兵节点
      private final Map<Integer, Node> keyToNode = new HashMap<>();
    
      public LRUCache(int capacity) {
          this.capacity = capacity;
          dummy.prev = dummy;
          dummy.next = dummy;
      }
    
      public int get(int key) {
          Node node = getNode(key); // getNode 会把对应节点移到链表头部
          return node != null ? node.value : -1;
      }
    
      public void put(int key, int value) {
          Node node = getNode(key); // getNode 会把对应节点移到链表头部
          if (node != null) { // 有这本书
              node.value = value; // 更新 value
              return;
          }
          node = new Node(key, value); // 新书
          keyToNode.put(key, node);
          pushFront(node); // 放到最上面
          if (keyToNode.size() > capacity) { // 书太多了
              Node backNode = dummy.prev;
              keyToNode.remove(backNode.key);
              remove(backNode); // 去掉最后一本书
          }
      }
    
      // 获取 key 对应的节点,同时把该节点移到链表头部
      private Node getNode(int key) {
          if (!keyToNode.containsKey(key)) { // 没有这本书
              return null;
          }
          Node node = keyToNode.get(key); // 有这本书
          remove(node); // 把这本书抽出来
          pushFront(node); // 放到最上面
          return node;
      }
    
      // 删除一个节点(抽出一本书)
      private void remove(Node x) {
          x.prev.next = x.next;
          x.next.prev = x.prev;
      }
    
      // 在链表头添加一个节点(把一本书放到最上面)
      private void pushFront(Node x) {
          x.prev = dummy;
          x.next = dummy.next;
          x.prev.next = x;
          x.next.prev = x;
      }

    }

    作者:灵茶山艾府

解题思路1:手写双向链表
复制代码
class LRUCache {
    private final int capacity;
    private final Map<Integer, Integer> cache = new LinkedHashMap<>(); // 内置 LRU

    public LRUCache(int capacity) {
        this.capacity = capacity;
    }

    public int get(int key) {
        // 删除 key,并利用返回值判断 key 是否在 cache 中
        Integer value = cache.remove(key);
        if (value != null) { // key 在 cache 中
            cache.put(key, value);
            return value;
        }
        // key 不在 cache 中
        return -1;
    }

    public void put(int key, int value) {
        // 删除 key,并利用返回值判断 key 是否在 cache 中
        if (cache.remove(key) != null) { // key 在 cache 中
            cache.put(key, value);
            return;
        }
        // key 不在 cache 中,那么就把 key 插入 cache,插入前判断 cache 是否满了
        if (cache.size() == capacity) { // cache 满了
            Integer eldestKey = cache.keySet().iterator().next();
            cache.remove(eldestKey); // 移除最久未使用 key
        }
        cache.put(key, value);
    }
}

作者:灵茶山艾府
解题思路2:标准库
相关推荐
Q741_1474 小时前
每日一题 力扣 3546. 等和矩阵分割 I 前缀和 贪心 剪枝 C++ 题解
算法·leetcode·前缀和·矩阵·剪枝·贪心
我是咸鱼不闲呀4 小时前
力扣Hot100系列23(Java)——[回溯]总结(上)(全排列,子集,电话号码的字母组合,组合总和)
java·算法·leetcode
tobias.b4 小时前
深度学习 超清晰通俗讲解 + 核心算法 + 使用场景
人工智能·深度学习·算法
测试19984 小时前
Jmeter接口测试:使用教程(上)
自动化测试·python·测试工具·jmeter·职场和发展·测试用例·接口测试
七夜zippoe4 小时前
量子计算入门:Qiskit框架实战
python·算法·量子计算·ibm·qiskit
小此方4 小时前
Re:从零开始的 C++ STL篇(八)深度解构AVL树自平衡机制:平衡维护与旋转调整背后的严密逻辑
开发语言·数据结构·c++·算法·stl
2301_789015624 小时前
封装哈希表实现unordered_set/undered_map
c语言·数据结构·c++·算法·哈希算法
落羽的落羽4 小时前
【Linux系统】中断机制、用户态与内核态、虚拟地址与页表的本质
java·linux·服务器·c++·人工智能·算法·机器学习
神工坊4 小时前
技术分享︱多重参考系模型在风扇通风仿真中的自动化实现:精度与效率的工程平衡
算法·hpc·并行计算·cfd·cae·流体力学·风扇仿真