ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Python列表排序全解析:从sort()/sorted()基础到Timsort算法与性能优化

Python列表排序全解析:从sort()/sorted()基础到Timsort算法与性能优化 1. 项目概述从“排序”这个基础操作说起干了这么多年开发Python里的列表排序大概是每个新手学会print(Hello World)之后紧接着就会碰到的操作。表面上看list.sort()和sorted(list)用起来简单到不值一提不就是让一堆数字或字符串排个队嘛。但真往深了挖你会发现这里面门道不少。什么时候该用原地排序sort()什么时候该用sorted()生成新列表除了简单的升序降序怎么按自定义规则排面对复杂对象列表key参数怎么玩出花来更别提那些隐藏在背后的排序算法原理和性能考量了。今天我就以“Python列表排序升序和降序”这个最基础的命题为起点把它掰开了、揉碎了结合我这些年踩过的坑和总结的经验带你从“会用”到“精通”。这篇文章不仅适合刚入门、对reverseTrue还感到新奇的朋友也适合已经写过几年代码想深入理解排序稳定性和自定义排序技巧的老手。我们会从最基础的语法开始一步步深入到原理、场景和那些官方文档里不会写的“骚操作”目标就是让你下次再遇到任何排序需求时都能心里有谱手到擒来。2. 核心概念与基础语法拆解2.1 两种核心方法sort()与sorted()的本质区别这是理解Python排序的基石也是新手最容易混淆的地方。很多人在用了很久之后依然对两者的选择凭感觉。我们来彻底讲清楚。list.sort()是原地排序。这个词很关键“原地”in-place意味着它直接修改了原始列表不会返回一个新的列表。它的返回值是None。这个设计是有意为之的目的是提醒你原列表已经被改变了。你经常会看到类似这样的错误写法my_list [3, 1, 4, 1, 5] sorted_list my_list.sort() # 错误sorted_list 现在是 None print(sorted_list) # 输出None print(my_list) # 输出[1, 1, 3, 4, 5]原列表被修改了正确的用法是直接调用然后使用原列表my_list.sort() print(my_list) # 现在 my_list 就是排序后的结果sorted(iterable)是新建排序。它接受任何可迭代对象列表、元组、字符串、字典的键等并返回一个全新的、排序后的列表。原始数据丝毫不会受到影响。my_list [3, 1, 4, 1, 5] new_list sorted(my_list) print(new_list) # 输出[1, 1, 3, 4, 5] print(my_list) # 输出[3, 1, 4, 1, 5]原列表纹丝不动选择策略我个人的经验之谈当你确定原始数据不再需要或者明确希望修改原列表时用list.sort()。它更节省内存因为不需要创建列表的副本。在处理大型数据集时这点性能差异可能变得显著。当你需要保留原始数据或者需要对不可变序列如元组或生成器进行排序时必须用sorted()。这也是函数式编程中更常见的做法因为它避免了副作用让代码逻辑更清晰。在链式调用中只能用sorted()。例如process_data(sorted(raw_data))因为sort()返回None会中断链式调用。2.2 升序与降序reverse参数详解控制排序方向非常简单就是通过reverse这个布尔参数。reverseFalse(默认)升序排列从小到大从A到Z。reverseTrue降序排列从大到小从Z到A。# 升序 (默认) nums [5, 2, 9, 1] nums.sort() # 或 sorted(nums) print(nums) # [1, 2, 5, 9] # 降序 nums [5, 2, 9, 1] nums.sort(reverseTrue) # 或 sorted(nums, reverseTrue) print(nums) # [9, 5, 2, 1]这里有一个容易忽略的细节reverseTrue并不是先升序再反转列表而是在排序的比较过程中直接按照反向规则进行。对于Python使用的Timsort算法来说这通常是高效的。但如果你需要先升序再反转逻辑上应该写成list.sort(); list.reverse()不过几乎没人需要这么做。2.3 排序的基石key参数与自定义排序逻辑如果说reverse是控制方向的方向盘那么key就是决定排序依据的发动机。这是Python排序功能强大和灵活的核心所在。key参数接受一个函数通常用lambda表达式这个函数会被应用到列表的每一个元素上排序的依据是这个函数的返回值而不是元素本身。经典场景1按字符串长度排序words [apple, fig, banana, cherry] # 按默认字典序排序 print(sorted(words)) # [apple, banana, cherry, fig] # 按长度排序升序 print(sorted(words, keylen)) # [fig, apple, banana, cherry] # 按长度降序 print(sorted(words, keylen, reverseTrue)) # [banana, cherry, apple, fig]经典场景2按列表中元素的某个属性或键排序这是处理字典列表或对象列表的日常操作。# 字典列表 students [ {name: Alice, score: 85}, {name: Bob, score: 92}, {name: Charlie, score: 78} ] # 按分数升序排序 sorted_by_score sorted(students, keylambda x: x[score]) print(sorted_by_score) # Charlie, Alice, Bob # 对象列表 (假设有Student类有score属性) # sorted(student_objects, keylambda s: s.score)key函数的进阶技巧多级排序让key函数返回一个元组。Python会按元组中元素的先后顺序进行比较即先比较第一个如果相同再比较第二个。# 先按分数降序分数相同再按名字升序 students.sort(keylambda x: (-x[score], x[name]))这里用了一个小技巧对数字降序可以在其前面加负号-。对于无法取负的数据类型如字符串就需要借助其他方法我们后面会讲。使用operator模块对于常见的取属性或键的操作使用operator模块比lambda更快且更易读。from operator import itemgetter, attrgetter # 等价于 keylambda x: x[score] sorted(students, keyitemgetter(score)) # 等价于 keylambda s: s.score # sorted(student_objects, keyattrgetter(score)) # 多级排序也支持 sorted(students, keyitemgetter(score, name))在性能敏感或代码风格要求严格的场景下我推荐使用operator模块。3. 深入原理Python排序算法与稳定性3.1 幕后英雄Timsort算法简介当你调用sort()或sorted()时Python解释器内部使用的是名为Timsort的混合排序算法。它是由Tim Peters为Python设计的现在也成为了Java、Android等平台的默认排序算法。理解它的特点有助于你写出更高效的代码。Timsort是自适应、稳定、混合的排序算法它融合了归并排序Merge Sort和插入排序Insertion Sort的优点。稳定性这是它一个极其重要的特性。如果两个元素根据排序键key函数的返回值是相等的那么排序后它们的相对顺序会保持不变。这对于多级排序至关重要。例如你先按姓氏排序再按名字排序稳定性保证了同姓氏的人内部的名字顺序是正确的。自适应性Timsort会利用数据中已存在的有序片段称为“run”这使得它对部分有序或完全有序的数据排序速度非常快接近O(n)。时间复杂度最坏和平均情况都是O(n log n)最好情况已排序是O(n)。空间复杂度是O(n)。给开发者的启示不用担心算法选择Python已经为你选好了在绝大多数情况下都表现优异的算法。利用稳定性放心地进行多级排序这是语言层面给你的保证。对有序数据友好如果你的数据很可能已经部分有序Timsort会给你带来惊喜的性能。3.2 排序的“代价”时间复杂度与空间复杂度浅析虽然我们不需要自己实现算法但了解复杂度有助于评估排序操作的成本尤其是在处理大数据时。list.sort()原地排序空间复杂度主要来自算法内部的O(n)临时空间用于归并。sorted()需要额外分配一个与原列表等大的新列表空间复杂度是O(n)。一个重要的性能对比import random, time, sys large_list [random.randint(0, 1000000) for _ in range(10**6)] list_copy large_list[:] # 测试 sorted() start time.time() new_list sorted(list_copy) time_sorted time.time() - start mem_sorted sys.getsizeof(new_list) # 测试 .sort() start time.time() list_copy.sort() time_sort time.time() - start mem_sort_inplace sys.getsizeof(list_copy) # 注意原列表内存不变但内部有开销 print(fsorted() 耗时: {time_sorted:.3f}s, 额外内存: ~{mem_sorted/1024/1024:.1f} MB) print(f.sort() 耗时: {time_sort:.3f}s, 内存变化列表对象本身: 0 MB)在我的测试中两者耗时通常非常接近Timsort主导但.sort()在内存占用上一定有优势因为它避免了创建完整的新列表对象。对于巨大的列表这个内存差异可能成为是否触发磁盘交换Swapping的关键。注意sys.getsizeof()只返回列表对象本身的大小不包括列表内元素对象的大小。对于元素是整数等小对象的情况列表对象的内存占比很大如果元素本身是大型对象如字典、字符串那么创建新列表和原地排序的内存差异会相对变小因为元素对象并没有被复制。4. 实战进阶复杂场景下的排序技巧掌握了基础我们来看看那些真正让代码变得优雅和高效的进阶用法。4.1 多条件排序的多种实现方案前面提到了用元组实现多级排序但当降序和升序混合时直接对数字取负的技巧对字符串无效。这时有几种方案方案A利用排序的稳定性进行多次排序这是最直观的方法。因为Python排序是稳定的我们可以从最次要的键开始排序逐步排到最主要的键。# 目标按分数降序分数相同按名字升序 students [...] # 同上 # 先按次要键名字升序排序 students.sort(keylambda x: x[name]) # 再按主要键分数降序排序稳定排序保证了同分者名字顺序不变 students.sort(keylambda x: x[score], reverseTrue)这种方法代码清晰易懂但进行了多次排序理论上时间复杂度是O(k * n log n)k为排序次数。对于数据量不大或排序条件不多的情况完全没问题。方案B使用key函数返回元组并对需要降序的字段进行转换对于数字可以取负。对于其他类型可以将其映射到一个支持反向排序的域。# 数字字段降序取负 students.sort(keylambda x: (-x[score], x[name])) # 如果是字符串字段需要降序可以将其“反转”比较顺序但比较麻烦。 # 一个技巧是将其映射为按相反顺序比较的代理值但这通常不直观。这种方法只排序一次效率高。但局限性是只对数字等能进行数学转换的类型方便。方案C推荐使用functools.cmp_to_key回归比较函数Python 2.x时代sort()方法可以接受一个cmp比较函数。在Python 3中为了性能和清晰度移除了它但提供了functools.cmp_to_key来转换。from functools import cmp_to_key def compare_students(a, b): # 先比较分数降序 if a[score] b[score]: return -1 # a排在b前面 elif a[score] b[score]: return 1 # a排在b后面 else: # 分数相同比较名字升序 if a[name] b[name]: return -1 elif a[name] b[name]: return 1 else: return 0 students.sort(keycmp_to_key(compare_students))这种方法最为强大和灵活可以定义任意复杂的比较逻辑尤其适合那些无法用简单key函数描述的排序规则。缺点是代码量稍大并且由于每次比较都要调用Python函数可能比基于key的排序慢一些。我的建议是优先使用方案A稳定排序或方案B元组key仅在逻辑极其复杂时使用方案C。4.2 对自定义对象进行排序对于自己定义的类排序需要告诉Python如何比较两个实例。有两种主要方式方式一定义__lt__等富比较方法这是最“Pythonic”的方式。通过定义__lt__(小于)、__le__(小于等于) 等方法你的类实例就可以直接使用,等比较运算符自然也支持sort()。class Student: def __init__(self, name, score): self.name name self.score score def __lt__(self, other): # 定义默认的小于比较按分数从低到高 return self.score other.score def __repr__(self): return fStudent({self.name}, {self.score}) stu_list [Student(Bob, 90), Student(Alice, 85), Student(Charlie, 92)] stu_list.sort() # 直接排序使用 __lt__ 定义的规则 print(stu_list) # [Student(Alice, 85), Student(Bob, 90), Student(Charlie, 92)] # 如果想降序可以传 reverseTrue或者定义不同的 __lt__ 逻辑。这种方式将排序规则内化到类中适用于有明确“自然顺序”的类。方式二使用key或attrgetter更常见和灵活的是在排序时指定规则这样同一个类在不同场景下可以按不同方式排序。# 按分数排序 sorted(stu_list, keylambda s: s.score) # 按名字排序 sorted(stu_list, keylambda s: s.name) from operator import attrgetter sorted(stu_list, keyattrgetter(score, name)) # 多级排序如何选择如果你的类有一个公认的、最主要的排序标准比如“学生”按学号排那么实现__lt__是合适的。如果排序标准是场景相关的那么绝对不要定义__lt__而是在调用排序时通过key参数指定这样更清晰、更灵活。4.3 处理包含不可直接比较元素的列表有时列表里元素类型不一或者元素本身不支持比较比如复数、None、自定义对象没定义比较方法。直接排序会抛出TypeError。mixed [3, hello, 1.5, None, [1,2]] # sorted(mixed) # TypeError: not supported between instances of str and int解决方案巧用key函数进行标准化我们可以通过key函数将所有元素转换到同一个可比较的域。一个常用的技巧是返回一个元组元组的第一个元素是类型优先级。def type_sort_key(item): 给不同类型分配一个优先级数字并返回一个可比较的元组 type_priority {int: 0, float: 1, str: 2, list: 3, type(None): 4} # 获取类型的优先级如果类型不在字典中给一个较大的值 priority type_priority.get(type(item), 99) return (priority, item) sorted_mixed sorted(mixed, keytype_sort_key) print(sorted_mixed) # [1.5, 3, hello, [1, 2], None]这个例子中我们让数字int, float排在前面然后是字符串再是列表最后是None。key函数返回(优先级, 元素本身)这样Python会先按优先级排序同优先级的再按元素自身的规则排序数字、字符串本身是可比的。5. 性能优化与常见陷阱5.1 排序性能优化实践优先使用key而非cmp历史上Python的sort支持cmp函数但它在每次比较时都会被调用复杂度是O(n log n * C)其中C是cmp函数的开销。而key函数只对每个元素调用一次复杂度是O(n * K n log n)其中K是key函数的开销。在n很大时key的优势巨大。这也是Python 3移除cmp参数的主要原因。key函数要轻量key函数会被调用n次所以它的执行速度直接影响总时间。避免在key函数中进行复杂的计算、I/O操作或数据库查询。# 不佳每次比较都计算字符串长度虽然len很快这里仅是示例 # 如果计算代价高比如从对象属性中解析数据问题就大了。 # 好的做法是如果可能预先计算好。利用装饰-排序-反装饰模式Schwartzian transform当key函数计算非常昂贵时可以显式地先计算并存储键值。# 假设有一个昂贵的函数 expensive_func decorated [(expensive_func(item), item) for item in my_list] decorated.sort() # 对元组排序元组按第一个元素比较 sorted_list [item for _, item in decorated]这其实就是sorted(list, keyexpensive_func)内部做的事情。只有当expensive_func极其昂贵并且你需要在多处复用这个键值时手动这样做才有意义。对于几乎有序的数据Timsort很快如果你的业务能产生部分有序的数据那么排序开销会比完全随机数据小。5.2 十大经典排序陷阱与避坑指南陷阱一误用sort()的返回值# 错误 result my_list.sort() # 正确 my_list.sort() result my_list # 或直接使用 sorted result sorted(my_list)陷阱二在迭代过程中修改列表在遍历列表的同时对其进行排序或其它结构性修改是危险的可能导致意外跳过元素或无限循环。如果需要先复制一份。# 危险 for item in my_list: if some_condition(item): my_list.sort() # 在循环内排序 # 安全做法 list_copy my_list[:] list_copy.sort() for item in list_copy: ...陷阱三对包含非可比元素的列表排序如前所述需要提供key函数或确保元素类型一致。陷阱四自定义__eq__但不定义__lt__等富比较方法如果你定义了__eq__用于但没有定义__lt__那么你的对象默认是不可排序的。sort()依赖这些比较运算符。可以使用functools.total_ordering装饰器来简化只需定义__eq__和__lt__它会帮你补全其他比较方法。陷阱五忽略排序的稳定性在多级排序中的重要性在多级排序时顺序很重要。必须先按次要键排序再按主要键排序才能得到正确结果。或者使用返回元组的key函数一次完成。陷阱六在key函数中产生副作用key函数应该是纯函数即输出只依赖于输入不改变外部状态。在key函数里修改元素或其他全局变量会导致不可预测的结果因为Python不保证key函数被调用的次数和顺序。# 绝对不要这样做 counter 0 def bad_key(x): global counter counter 1 return x counter my_list.sort(keybad_key)陷阱七认为sorted()可以对所有迭代器进行原地排序sorted()返回一个新列表。如果你有一个迭代器如生成器并且希望“原地”处理这是不可能的因为迭代器可能是一次性的。你需要将迭代器转换为列表再排序。gen (x for x in range(10, 0, -1)) sorted_gen sorted(gen) # 正确生成一个新列表 # gen 现在已耗尽陷阱八对大规模数据使用list.sort()时内存不足虽然.sort()是原地操作但Timsort算法在归并阶段需要O(n)的临时空间。如果列表本身已经占据了大部分可用内存排序操作可能会因为无法分配临时空间而触发MemoryError。对于极端情况需要考虑外部排序算法。陷阱九字符串排序的本地化问题默认的字符串排序是基于Unicode码点对于Python 3这有时不符合语言习惯例如德语的“ä”应该排在“z”附近吗。对于需要本地化排序的场景应使用locale.strxfrm作为key函数或使用第三方库如pyuca。import locale locale.setlocale(locale.LC_COLLATE, de_DE.UTF-8) # 设置德语区域 words [äpfel, zebra, apfel] sorted_words sorted(words, keylocale.strxfrm)注意区域设置依赖操作系统环境可能不是跨平台的。陷阱十浮点数的特殊值NaN浮点数中的NaN(Not a Number) 是不可比较的任何与NaN的比较包括相等都返回False。在排序中NaN的行为是未定义的可能会被放在列表的开头或结尾取决于Python实现。如果你的数据可能包含NaN需要在排序前过滤或处理它们。import math numbers [3.0, float(nan), 1.0, 2.0] # sorted(numbers) 可能得到 [nan, 1.0, 2.0, 3.0] 或其他顺序 # 安全做法过滤掉 NaN safe_numbers [x for x in numbers if not math.isnan(x)] safe_numbers.sort()6. 扩展应用超越内置排序内置的sort/sorted已经非常强大但有些特殊需求需要我们自己动手或借助其他工具。6.1 实现自定义排序算法以快速排序为例虽然99.9%的情况都用内置的但理解算法原理和亲手实现对于面试和深入理解计算机科学很有帮助。这里实现一个简单的快速排序来对比def quicksort(arr): 经典的快速排序实现非原地易于理解 if len(arr) 1: return arr pivot arr[len(arr) // 2] # 选择中间元素作为基准 left [x for x in arr if x pivot] middle [x for x in arr if x pivot] right [x for x in arr if x pivot] return quicksort(left) middle quicksort(right) # 测试 my_list [3, 6, 8, 10, 1, 2, 1] print(quicksort(my_list)) # [1, 1, 2, 3, 6, 8, 10] print(my_list) # 原列表不变 [3, 6, 8, 10, 1, 2, 1]这个实现简洁易懂但不是原地排序且由于列表推导式创建了多个新列表空间开销大。内置的Timsort在几乎所有实际场景中都优于这种教学版本的快排。6.2 使用heapq模块进行部分排序如果你只需要列表中最小的几个或最大的几个元素对整个列表进行排序是浪费的。heapq模块提供了基于堆的部分排序接口时间复杂度是O(n log k)其中k是需要的元素个数比完全排序的O(n log n)更优。import heapq numbers [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] # 获取最小的3个元素 smallest_three heapq.nsmallest(3, numbers) print(smallest_three) # [1, 1, 2] # 获取最大的3个元素 largest_three heapq.nlargest(3, numbers) print(largest_three) # [9, 6, 5] # 同样支持 key 参数 students [...] top_students heapq.nlargest(3, students, keylambda s: s[score])当k远小于n时例如在10万个元素中找前10个heapq.nsmallest/nlargest比sorted(list)[:k]高效得多。当k接近n时直接排序可能更好。heapq内部会智能选择算法。6.3 第三方库pandas与numpy中的排序在数据科学领域pandas的Series和DataFrame以及numpy的ndarray都有自己优化的排序方法。NumPy (np.sort与ndarray.sort):import numpy as np arr np.array([3, 1, 4, 1, 5]) # 返回新数组 sorted_arr np.sort(arr) # array([1, 1, 3, 4, 5]) # 原地排序 arr.sort() # arr 变为 array([1, 1, 3, 4, 5]) # 沿特定轴排序多维数组 # 获取排序后的索引非常有用 idx np.argsort(arr) # 返回的是将数组排序的索引np.argsort是NumPy排序的杀手级功能它返回的是排序后元素在原数组中的索引位置可以用于同步排序多个相关联的数组。Pandas:import pandas as pd df pd.DataFrame({name: [Bob, Alice, Charlie], score: [90, 85, 92]}) # 按某一列排序 df_sorted df.sort_values(byscore, ascendingFalse) # 按多列排序 df_sorted_multi df.sort_values(by[score, name], ascending[False, True]) # 原地排序 df.sort_values(byscore, inplaceTrue)Pandas的排序功能与DataFrame的索引、筛选等功能深度集成是处理表格数据的标准操作。7. 综合案例与经验复盘7.1 案例处理来自数据库的用户日志数据假设我们从数据库拿到一组用户操作日志每条日志是一个字典我们想按时间戳降序最新在前同一秒内的操作按用户ID升序排列。import datetime logs [ {user_id: 101, action: login, timestamp: datetime.datetime(2023, 10, 27, 14, 30, 15)}, {user_id: 102, action: view, timestamp: datetime.datetime(2023, 10, 27, 14, 30, 15)}, {user_id: 101, action: click, timestamp: datetime.datetime(2023, 10, 27, 14, 30, 20)}, {user_id: 100, action: login, timestamp: datetime.datetime(2023, 10, 27, 14, 30, 10)}, ] # 方法1利用稳定性先排次要键再排主要键降序需注意 logs.sort(keylambda x: x[user_id]) # 先按user_id升序 logs.sort(keylambda x: x[timestamp], reverseTrue) # 再按时间降序 # 因为sort是稳定的所以同时间戳的日志会保持user_id升序 # 方法2使用元组key时间戳取负实现降序 # datetime对象不能直接取负我们可以用timestamp()转换成数字或者用负的秒数 logs.sort(keylambda x: (-x[timestamp].timestamp(), x[user_id])) # 或者如果担心浮点数精度可以用一个足够大的数减去时间戳 # from datetime import datetime as dt # reference dt.max # keylambda x: ((reference - x[timestamp]).total_seconds(), x[user_id]) # 方法3使用cmp_to_key逻辑最清晰但性能稍差 from functools import cmp_to_key def log_cmp(a, b): if a[timestamp] b[timestamp]: return -1 elif a[timestamp] b[timestamp]: return 1 else: return a[user_id] - b[user_id] logs.sort(keycmp_to_key(log_cmp)) for log in logs: print(f{log[timestamp]}: User {log[user_id]} - {log[action]})在这个案例中方法1稳定排序通常是最清晰易懂的除非数据量极大且对性能有极致要求。方法2元组key需要一点小技巧来处理日期时间的降序。方法3cmp_to_key在比较逻辑复杂时很有优势。7.2 经验复盘排序中的“坑”与最佳实践结合我多年的经验总结几条黄金法则默认选择sorted()除非你明确要修改原列表否则使用sorted()更安全避免了无意中改变原始数据的副作用。函数式风格让代码更容易推理。key函数保持简单key函数应该像投影仪快速地将元素映射到一个可比较的值。不要在里边做繁重的工作。如果需要复杂计算考虑预先计算好并存放在数据结构中。理解稳定性记住Python排序是稳定的。这是实现多级排序的利器也是保证某些特定业务逻辑正确的基石。对大列表保持警惕排序O(n log n)的复杂度意味着数据量翻倍时间增长不止一倍。对于非常大的列表例如数百万条记录排序可能成为性能瓶颈。考虑是否真的需要全排序能否用heapq找Top-K数据能否分块处理测试边界情况你的排序逻辑能正确处理None吗能处理浮点数的inf和nan吗对于自定义对象__eq__和__hash__的定义是否会影响排序在关键代码上线前务必用包含边界值的测试用例覆盖。善用operator模块itemgetter和attrgetter不仅比lambda表达式运行稍快而且使代码意图更明确尤其是在多级排序时。排序不是万能的对于频繁插入和删除并需要始终保持有序的场景考虑使用bisect模块维护列表有序性或者使用heapq实现优先队列或者直接使用sortedcontainers这样的第三方库如SortedList,SortedDict。排序这个看似基础的操作贯穿了程序开发的始终。从简单的数字列表到复杂的业务对象集合一个恰当的排序策略不仅能提升程序效率更能让数据呈现出清晰的逻辑为后续处理打下坚实基础。希望这篇长文能帮你把Python列表排序这个工具从“会用”变成“精通”在下次面对排序需求时能够游刃有余地选出最适合的方案。
返回列表