算法母题30道(第2篇):链表操作(6-10)

系列说明:本系列共6篇文章,涵盖30道经典算法母题,使用Java实现
适用场景:算法学习、面试准备、编程能力提升


📋 30道算法母题总览

第1篇:数组与字符串基础(1-5)

  1. 两数之和
  2. 三数之和
  3. 盛最多水的容器
  4. 接雨水
  5. 最长无重复字符子串

第2篇:链表操作(6-10)⭐ 本篇

  1. 反转链表
  2. 合并两个有序链表
  3. 链表中倒数第k个节点
  4. 判断链表是否有环
  5. 合并K个有序链表

第3篇:栈与队列(11-15)

  1. 有效的括号
  2. 用栈实现队列
  3. 最小栈
  4. 滑动窗口最大值
  5. 每日温度

第4篇:二叉树遍历(16-20)

  1. 二叉树的前序遍历
  2. 二叉树的中序遍历
  3. 二叉树的后序遍历
  4. 二叉树的层序遍历
  5. 二叉树的最大深度

第5篇:动态规划(21-25)

  1. 爬楼梯
  2. 最长递增子序列
  3. 最长公共子序列
  4. 背包问题(0-1背包)
  5. 编辑距离

第6篇:搜索与排序(26-30)

  1. 二分查找
  2. 快速排序
  3. 归并排序
  4. 岛屿数量(DFS/BFS)
  5. 全排列(回溯)

链表节点定义

// 所有链表题目的基础节点定义
class ListNode {
    int val;
    ListNode next;
    
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { 
        this.val = val; 
        this.next = next; 
    }
}

第6题:反转链表

📝 题目描述

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

示例

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

输入:head = [1,2]
输出:[2,1]

输入:head = []
输出:[]

💡 解题思路

迭代法(双指针)

  1. 使用两个指针:prev(前一个节点)和 curr(当前节点)
  2. 在遍历链表时,将当前节点的 next 指向前一个节点
  3. 需要先保存下一个节点,避免链表断裂
  4. 继续向前移动,直到遍历完整个链表

递归法

  1. 递归到链表末尾
  2. 从后往前反转指针
  3. 返回新的头节点

🎯 日常应用

  • 浏览器历史:前进后退功能的实现
  • 撤销操作:编辑器的 undo 功能
  • 数据流处理:反向处理数据序列
  • 缓存管理:LRU 缓存的实现基础

🔧 代码解决的问题

  • 链表的逆序操作
  • 指针的正确重定向
  • 避免内存泄漏和链表断裂

💻 Java实现代码

public class ReverseLinkedList {
    
    /**
     * 反转链表 - 迭代法(推荐)
     * @param head 链表头节点
     * @return 反转后的链表头节点
     */
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        
        while (curr != null) {
            // 保存下一个节点
            ListNode nextTemp = curr.next;
            
            // 反转当前节点的指针
            curr.next = prev;
            
            // 移动指针
            prev = curr;
            curr = nextTemp;
        }
        
        // prev 成为新的头节点
        return prev;
    }
    
    /**
     * 反转链表 - 递归法
     */
    public ListNode reverseListRecursive(ListNode head) {
        // 基本情况:空链表或只有一个节点
        if (head == null || head.next == null) {
            return head;
        }
        
        // 递归反转后面的节点
        ListNode newHead = reverseListRecursive(head.next);
        
        // 反转当前节点和下一个节点的连接
        head.next.next = head;
        head.next = null;
        
        return newHead;
    }
    
    // 辅助方法:创建链表
    public ListNode createList(int[] arr) {
        if (arr == null || arr.length == 0) return null;
        ListNode head = new ListNode(arr[0]);
        ListNode curr = head;
        for (int i = 1; i < arr.length; i++) {
            curr.next = new ListNode(arr[i]);
            curr = curr.next;
        }
        return head;
    }
    
    // 辅助方法:打印链表
    public void printList(ListNode head) {
        ListNode curr = head;
        while (curr != null) {
            System.out.print(curr.val + " -> ");
            curr = curr.next;
        }
        System.out.println("null");
    }
    
    // 测试方法
    public static void main(String[] args) {
        ReverseLinkedList solution = new ReverseLinkedList();
        
        int[] arr = {1, 2, 3, 4, 5};
        ListNode head = solution.createList(arr);
        
        System.out.print("原链表: ");
        solution.printList(head);
        
        ListNode reversed = solution.reverseList(head);
        System.out.print("反转后: ");
        solution.printList(reversed);
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(n)

    • 需要遍历链表一次
    • 每个节点处理一次
  • 空间复杂度

    • 迭代法:O(1) - 只使用常数额外空间
    • 递归法:O(n) - 递归调用栈的深度为 n

第7题:合并两个有序链表

📝 题目描述

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

输入:l1 = [], l2 = []
输出:[]

输入:l1 = [], l2 = [0]
输出:[0]

💡 解题思路

迭代法

  1. 创建一个虚拟头节点 dummy
  2. 使用指针 curr 指向当前节点
  3. 比较两个链表的当前节点值
  4. 将较小的节点连接到结果链表
  5. 移动对应链表的指针
  6. 处理剩余部分(其中一个链表可能先遍历完)

递归法

  1. 比较两个链表头节点的值
  2. 较小的节点作为当前节点
  3. 递归合并剩余部分

🎯 日常应用

  • 归并排序:合并两个已排序的子序列
  • 数据流合并:合并多个有序数据源
  • 优先级队列:合并多个优先队列
  • 日志处理:合并多个时间有序的日志文件

🔧 代码解决的问题

  • 有序序列的合并
  • 避免创建新节点(复用原节点)
  • 处理边界情况(空链表)

💻 Java实现代码

public class MergeTwoSortedLists {
    
    /**
     * 合并两个有序链表 - 迭代法
     * @param l1 第一个有序链表
     * @param l2 第二个有序链表
     * @return 合并后的有序链表
     */
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 虚拟头节点
        ListNode dummy = new ListNode(-1);
        ListNode curr = dummy;
        
        // 比较两个链表的节点
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                curr.next = l1;
                l1 = l1.next;
            } else {
                curr.next = l2;
                l2 = l2.next;
            }
            curr = curr.next;
        }
        
        // 连接剩余部分
        curr.next = (l1 != null) ? l1 : l2;
        
        return dummy.next;
    }
    
    /**
     * 合并两个有序链表 - 递归法
     */
    public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
        // 基本情况
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        
        // 递归合并
        if (l1.val <= l2.val) {
            l1.next = mergeTwoListsRecursive(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoListsRecursive(l1, l2.next);
            return l2;
        }
    }
    
    // 辅助方法:创建链表
    public ListNode createList(int[] arr) {
        if (arr == null || arr.length == 0) return null;
        ListNode head = new ListNode(arr[0]);
        ListNode curr = head;
        for (int i = 1; i < arr.length; i++) {
            curr.next = new ListNode(arr[i]);
            curr = curr.next;
        }
        return head;
    }
    
    // 辅助方法:打印链表
    public void printList(ListNode head) {
        ListNode curr = head;
        while (curr != null) {
            System.out.print(curr.val + " -> ");
            curr = curr.next;
        }
        System.out.println("null");
    }
    
    // 测试方法
    public static void main(String[] args) {
        MergeTwoSortedLists solution = new MergeTwoSortedLists();
        
        int[] arr1 = {1, 2, 4};
        int[] arr2 = {1, 3, 4};
        
        ListNode l1 = solution.createList(arr1);
        ListNode l2 = solution.createList(arr2);
        
        System.out.print("链表1: ");
        solution.printList(l1);
        System.out.print("链表2: ");
        solution.printList(l2);
        
        ListNode merged = solution.mergeTwoLists(l1, l2);
        System.out.print("合并后: ");
        solution.printList(merged);
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(m + n)

    • m 和 n 分别是两个链表的长度
    • 需要遍历两个链表的所有节点
  • 空间复杂度

    • 迭代法:O(1) - 只使用常数额外空间
    • 递归法:O(m + n) - 递归调用栈的深度

第8题:链表中倒数第k个节点

📝 题目描述

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

示例

给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

💡 解题思路

快慢指针法(推荐)

  1. 使用两个指针 fastslow,都指向头节点
  2. 先让 fast 走 k 步
  3. 然后 fastslow 同时移动
  4. fast 到达末尾时,slow 正好在倒数第 k 个节点

为什么要这样做?

  • 链表只能从头到尾遍历,无法直接定位倒数位置
  • 快慢指针保持 k 的距离,相当于一个"窗口"
  • 当快指针到末尾,慢指针自然在倒数第 k 个位置

🎯 日常应用

  • 分页查询:获取倒数第 N 条记录
  • 缓存管理:LRU 缓存中定位特定位置
  • 日志分析:获取最近的 N 条日志
  • 排行榜:获取倒数排名

🔧 代码解决的问题

  • 单向链表的反向定位
  • 避免两次遍历链表
  • 优化时间和空间复杂度

💻 Java实现代码

public class KthNodeFromEnd {
    
    /**
     * 链表中倒数第k个节点 - 快慢指针法
     * @param head 链表头节点
     * @param k 倒数第k个
     * @return 倒数第k个节点
     */
    public ListNode getKthFromEnd(ListNode head, int k) {
        if (head == null || k <= 0) {
            return null;
        }
        
        ListNode fast = head;
        ListNode slow = head;
        
        // fast 先走 k 步
        for (int i = 0; i < k; i++) {
            if (fast == null) {
                // k 大于链表长度
                return null;
            }
            fast = fast.next;
        }
        
        // fast 和 slow 同时移动
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        
        // slow 此时在倒数第 k 个节点
        return slow;
    }
    
    /**
     * 链表中倒数第k个节点 - 两次遍历法(易理解)
     */
    public ListNode getKthFromEndTwoPass(ListNode head, int k) {
        if (head == null || k <= 0) {
            return null;
        }
        
        // 第一次遍历:计算链表长度
        int length = 0;
        ListNode curr = head;
        while (curr != null) {
            length++;
            curr = curr.next;
        }
        
        // 检查 k 是否合法
        if (k > length) {
            return null;
        }
        
        // 第二次遍历:找到倒数第 k 个节点
        // 即正数第 (length - k + 1) 个节点
        curr = head;
        for (int i = 0; i < length - k; i++) {
            curr = curr.next;
        }
        
        return curr;
    }
    
    // 辅助方法:创建链表
    public ListNode createList(int[] arr) {
        if (arr == null || arr.length == 0) return null;
        ListNode head = new ListNode(arr[0]);
        ListNode curr = head;
        for (int i = 1; i < arr.length; i++) {
            curr.next = new ListNode(arr[i]);
            curr = curr.next;
        }
        return head;
    }
    
    // 辅助方法:打印链表
    public void printList(ListNode head) {
        ListNode curr = head;
        while (curr != null) {
            System.out.print(curr.val + " -> ");
            curr = curr.next;
        }
        System.out.println("null");
    }
    
    // 测试方法
    public static void main(String[] args) {
        KthNodeFromEnd solution = new KthNodeFromEnd();
        
        int[] arr = {1, 2, 3, 4, 5};
        ListNode head = solution.createList(arr);
        
        System.out.print("链表: ");
        solution.printList(head);
        
        int k = 2;
        ListNode result = solution.getKthFromEnd(head, k);
        System.out.println("倒数第 " + k + " 个节点: " + (result != null ? result.val : "null"));
        System.out.print("从该节点开始的链表: ");
        solution.printList(result);
    }
}

⏱️ 复杂度分析

快慢指针法

  • 时间复杂度:O(n) - 只需要遍历一次链表
  • 空间复杂度:O(1) - 只使用两个指针

两次遍历法

  • 时间复杂度:O(n) - 需要遍历两次链表
  • 空间复杂度:O(1) - 只使用常数额外空间

第9题:判断链表是否有环

📝 题目描述

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

示例

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

解题思路

快慢指针法(Floyd 判圈算法)

  1. 使用两个指针:slow(慢指针)和 fast(快指针)
  2. slow 每次走一步,fast 每次走两步
  3. 如果链表有环,快慢指针最终会相遇
  4. 如果 fast 到达末尾(null),说明无环

为什么快慢指针会相遇?

  • 如果有环,快指针会在环内追赶慢指针
  • 每次循环,快指针比慢指针多走一步
  • 相对速度为 1,最终一定会追上

扩展:找到环的入口

  1. 快慢指针相遇后,将 slow 重置到链表头
  2. slowfast 都每次走一步
  3. 它们再次相遇的位置就是环的入口

🎯 日常应用

  • 死锁检测:操作系统中的资源循环等待
  • 游戏开发:检测循环路径或无限循环
  • 数据验证:检查数据结构的一致性
  • 网络路由:检测路由环路

代码解决的问题

  • 检测循环依赖
  • 避免无限循环
  • 优化空间复杂度(不用哈希表)

💻 Java实现代码

public class LinkedListCycle {
    
    /**
     * 判断链表是否有环 - 快慢指针法
     * @param head 链表头节点
     * @return 是否有环
     */
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;           // 慢指针走一步
            fast = fast.next.next;      // 快指针走两步
            
            // 快慢指针相遇,说明有环
            if (slow == fast) {
                return true;
            }
        }
        
        // fast 到达末尾,说明无环
        return false;
    }
    
    /**
     * 进阶:找到环的入口节点
     * @param head 链表头节点
     * @return 环的入口节点,无环则返回 null
     */
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        
        ListNode slow = head;
        ListNode fast = head;
        boolean hasCycle = false;
        
        // 第一步:判断是否有环
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }
        
        // 无环
        if (!hasCycle) {
            return null;
        }
        
        // 第二步:找到环的入口
        // 将 slow 重置到链表头
        slow = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        
        // 相遇点就是环的入口
        return slow;
    }
    
    /**
     * 判断链表是否有环 - 哈希表法(易理解但空间复杂度高)
     */
    public boolean hasCycleHashSet(ListNode head) {
        java.util.Set<ListNode> visited = new java.util.HashSet<>();
        ListNode curr = head;
        
        while (curr != null) {
            if (visited.contains(curr)) {
                return true;
            }
            visited.add(curr);
            curr = curr.next;
        }
        
        return false;
    }
    
    // 测试方法
    public static void main(String[] args) {
        LinkedListCycle solution = new LinkedListCycle();
        
        // 创建有环链表: 3->2->0->-4->2
        ListNode node1 = new ListNode(3);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(0);
        ListNode node4 = new ListNode(-4);
        
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node2; // 形成环
        
        System.out.println("有环链表检测结果: " + solution.hasCycle(node1));
        System.out.println("环的入口节点值: " + 
            (solution.detectCycle(node1) != null ? solution.detectCycle(node1).val : "null"));
        
        // 创建无环链表: 1->2->3
        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);
        head2.next.next = new ListNode(3);
        
        System.out.println("无环链表检测结果: " + solution.hasCycle(head2));
    }
}

️ 复杂度分析

快慢指针法

  • 时间复杂度:O(n)

    • 无环时:快指针遍历 n/2 次
    • 有环时:最多遍历环的长度次
  • 空间复杂度:O(1)

    • 只使用两个指针

哈希表法

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
    • 需要存储所有访问过的节点

第10题:合并K个有序链表

题目描述

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到:
1->1->2->3->4->4->5->6

💡 解题思路

方法1:优先队列(最小堆)

  1. 将每个链表的头节点放入最小堆
  2. 每次取出堆顶元素(最小值)
  3. 将该节点的下一个节点(如果不为空)加入堆
  4. 重复直到堆为空

方法2:分治法

  1. 将 K 个链表两两合并
  2. 第一轮合并后剩下 K/2 个链表
  3. 第二轮合并后剩下 K/4 个链表
  4. 重复直到只剩一个链表

方法3:顺序合并

  1. 依次合并两个链表
  2. 将结果与下一个链表合并
  3. 重复直到所有链表合并完成

日常应用

  • 多路归并:合并多个有序数据流
  • 外部排序:大文件的排序和合并
  • 日志聚合:合并多个服务器的日志
  • 数据库查询:合并多个索引的结果

代码解决的问题

  • 多路归并问题
  • 优先级调度
  • 分治思想的应用

💻 Java实现代码

import java.util.PriorityQueue;

public class MergeKSortedLists {
    
    /**
     * 合并K个有序链表 - 优先队列法(推荐)
     * @param lists K个有序链表
     * @return 合并后的有序链表
     */
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }
        
        // 最小堆,按照节点值排序
        PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
        
        // 将每个链表的头节点加入堆
        for (ListNode head : lists) {
            if (head != null) {
                pq.offer(head);
            }
        }
        
        // 虚拟头节点
        ListNode dummy = new ListNode(-1);
        ListNode curr = dummy;
        
        // 不断从堆中取出最小节点
        while (!pq.isEmpty()) {
            ListNode minNode = pq.poll();
            curr.next = minNode;
            curr = curr.next;
            
            // 将下一个节点加入堆
            if (minNode.next != null) {
                pq.offer(minNode.next);
            }
        }
        
        return dummy.next;
    }
    
    /**
     * 合并K个有序链表 - 分治法
     */
    public ListNode mergeKListsDivide(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }
        return merge(lists, 0, lists.length - 1);
    }
    
    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(-1);
        ListNode curr = dummy;
        
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                curr.next = l1;
                l1 = l1.next;
            } else {
                curr.next = l2;
                l2 = l2.next;
            }
            curr = curr.next;
        }
        
        curr.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }
    
    // 辅助方法:创建链表
    public ListNode createList(int[] arr) {
        if (arr == null || arr.length == 0) return null;
        ListNode head = new ListNode(arr[0]);
        ListNode curr = head;
        for (int i = 1; i < arr.length; i++) {
            curr.next = new ListNode(arr[i]);
            curr = curr.next;
        }
        return head;
    }
    
    // 辅助方法:打印链表
    public void printList(ListNode head) {
        ListNode curr = head;
        while (curr != null) {
            System.out.print(curr.val + " -> ");
            curr = curr.next;
        }
        System.out.println("null");
    }
    
    // 测试方法
    public static void main(String[] args) {
        MergeKSortedLists solution = new MergeKSortedLists();
        
        int[][] arrays = {
            {1, 4, 5},
            {1, 3, 4},
            {2, 6}
        };
        
        ListNode[] lists = new ListNode[arrays.length];
        for (int i = 0; i < arrays.length; i++) {
            lists[i] = solution.createList(arrays[i]);
        }
        
        System.out.println("输入链表数组:");
        for (int i = 0; i < lists.length; i++) {
            System.out.print("链表" + (i+1) + ": ");
            solution.printList(lists[i]);
        }
        
        ListNode merged = solution.mergeKLists(lists);
        System.out.print("合并后: ");
        solution.printList(merged);
    }
}

⏱️ 复杂度分析

优先队列法

  • 时间复杂度:O(N log K)

    • N 是所有节点的总数
    • K 是链表的个数
    • 每个节点入堆和出堆一次,堆操作为 O(log K)
  • 空间复杂度:O(K)

    • 堆中最多存储 K 个节点

分治法

  • 时间复杂度:O(N log K)

    • 递归树的高度为 log K
    • 每层合并所有节点,总时间为 O(N)
  • 空间复杂度:O(log K)

    • 递归调用栈的深度为 log K

总结与对比

链表操作技巧总结

题目 核心技巧 关键数据结构 时间复杂度 空间复杂度
反转链表 双指针/递归 链表 O(n) O(1)/O(n)
合并两个有序链表 迭代/递归 链表 O(m+n) O(1)/O(m+n)
倒数第k个节点 快慢指针 链表 O(n) O(1)
判断链表有环 快慢指针 链表 O(n) O(1)
合并K个有序链表 优先队列/分治 堆/链表 O(N log K) O(K)/O(log K)

通用解题模板

1. 快慢指针模板
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
    
    // 检测相遇
    if (slow == fast) {
        // 有环或其他逻辑
    }
}
2. 链表反转模板
ListNode prev = null, curr = head;
while (curr != null) {
    ListNode nextTemp = curr.next;
    curr.next = prev;
    prev = curr;
    curr = nextTemp;
}
return prev;
3. 合并链表模板
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;

while (l1 != null && l2 != null) {
    if (l1.val <= l2.val) {
        curr.next = l1;
        l1 = l1.next;
    } else {
        curr.next = l2;
        l2 = l2.next;
    }
    curr = curr.next;
}

curr.next = (l1 != null) ? l1 : l2;
return dummy.next;
4. 虚拟头节点技巧
// 避免处理头节点的特殊情况
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;

// 操作完成后返回 dummy.next
return dummy.next;

📚 下一篇预告

第3篇:栈与队列(11-15)

  • 有效的括号
  • 用栈实现队列
  • 最小栈
  • 滑动窗口最大值
  • 每日温度

如果觉得这篇文章对您有帮助,欢迎:

  • 👍 点赞支持
  • 💬 留言讨论
  • ⭐ 收藏备用

有任何算法问题,欢迎在评论区交流!

更多推荐