ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

sort函数完全指南:Python/JavaScript/C++排序用法与避坑

sort函数完全指南:Python/JavaScript/C++排序用法与避坑 写代码写到一定量之后你会发现一个特别有意思的现象不管业务多复杂最后处理数据的时候总会碰到排序。而 sort 函数就是几乎所有编程语言里那个帮你完成排序的标配工具。无论是 Python 的list.sort()、JavaScript 的Array.prototype.sort()还是 C 的std::sort()它们的核心目标都一样把一堆无序的数据按照你期望的规则排成有序序列。但目标一样并不代表用法一样。每个语言的 sort 都有自己的脾气默认行为、排序稳定性、自定义规则的方式都各不相同。这些年我看过不少人在排序上栽跟头JavaScript 里[10, 2, 1].sort()得到[1, 10, 2]Python 里对一个包含字符串和数字的列表直接 sort 抛出 TypeErrorC 里对链表调用std::sort直接编译出错。这些问题都不是算法多难纯粹是对 sort 函数的理解有盲区。所以这篇东西我打算把 sort 函数从使用角度完整拆一遍先理清它到底在底层做了什么再逐语言展示正确的打开方式最后把高频场景的完整写法和常见坑全部摆出来。不管你是刚接触编程的新手还是写了几年代码想查漏补缺的老手看完之后遇到排序需求应该能直接抄作业。1. 先把 sort 的底摸清楚1.1 sort 到底在干什么从工程角度看sort 就是一个帮你把数据按规则排好的工具函数。但在使用它之前你得清楚它背后处理的是比较这件事。排序算法的核心是两两比较 位置调整sort 函数之所以能工作是因为它知道如何判断两个元素谁在前谁在后。这个判断规则在不同语言里有不同的默认值。Python 默认按元素的大小比较数字按数值、字符串按 Unicode 码点序JavaScript 的默认行为则非常反直觉——它把元素转成字符串后按 UTF-16 码元序比较C 的std::sort默认用operator也就是小于号来决定次序。还有一个所有语言都绕不开的概念稳定性。稳定排序的意思是如果两个元素排序时判定相等它们在原序列里的相对顺序在排序后保持不变。这个看起来不起眼的特性在处理多级排序、分页列表、排行榜场景时极其重要。比如你先把学生按姓名排好再按分数排如果排序不稳定按分数排完之后同分学生的姓名顺序可能全乱了。Python 的 sort 用的是 Timsort它是稳定的JavaScript 在 ES2019 标准之后要求 sort 稳定V8 引擎底层也换成了 Timsort 一类混合算法C 的std::sort则明确不保证稳定想要稳定就得用std::stable_sort。选错的话某些精细场景下结果会有微妙差异。1.2 不同语言的 sort 接口长什么样先快速过一遍各个语言里 sort 的基本调用姿势有个整体印象再看后面的深入拆解。Python 有两个排序入口# list 的原地排序方法直接修改原列表 lst [3, 1, 2] lst.sort() print(lst) # [1, 2, 3] # 内置函数 sorted返回一个排好序的新列表原列表不动 lst [3, 1, 2] new_lst sorted(lst) print(lst) # [3, 1, 2] print(new_lst) # [1, 2, 3]JavaScript 的排序接口只有一个就是数组的sort方法但它会原地修改数组const arr [3, 1, 2]; arr.sort(); console.log(arr); // [1, 2, 3]C 里用std::sort参数是迭代器区间排的是vector、数组这类连续容器的区间#include algorithm #include vector std::vectorint v {3, 1, 2}; std::sort(v.begin(), v.end()); // v 变为 {1, 2, 3}光看接口就会发现一个关键差异Python 区分了原地修改和返回新列表两种需求JavaScript 只有原地修改C 通过迭代器区间操作所以非常灵活。理解了接口差异后面很多使用困惑其实就解开了一半。2. Python sort 的关键参数一个个掰开讲2.1sorted()与list.sort()别再用错了我见过不少人把sorted()和list.sort()混着用代码也照样能跑但如果不知道它们的区别迟早会踩坑。最核心的区别就两条list.sort()原地排序返回None也就是说你不能写成new_lst lst.sort()那样拿到的是None而不是排好的列表sorted()返回新列表原对象完全不受影响而且它不仅适用于列表还可以作用于元组、字典的键、字符串、生成器等任意可迭代对象。什么时候用哪个我的习惯是这样如果手头已经有一个列表而且后续逻辑不需要保留原顺序那就用list.sort()省内存、速度快如果原数据只是用来读取的或者你面对的是元组、字典这类不可变或非列表对象那就用sorted()。还有一个很实用的场景写函数式风格的链式调用时sorted()能直接嵌在表达式里list.sort()就做不到。words [pear, apple, banana] # 原地排序 words.sort() print(words) # [apple, banana, pear] # 不破坏原数据返回新列表 original [3, 1, 2] sorted_copy sorted(original, reverseTrue) print(original) # [3, 1, 2] print(sorted_copy) # [3, 2, 1]2.2 key 参数sort 的灵魂所在说key是 Python sort 的灵魂一点不夸张。key接收一个把元素映射成排序依据的函数sort 会先对每个元素执行这个函数然后用返回值而不是元素本身来比较大小。这个设计非常优雅因为它把元素长什么样和按什么规则排序彻底解耦了。常见用法有三种from operator import itemgetter, attrgetter # 1. lambda 临时定义一个映射规则 students [ {name: Alice, score: 89}, {name: Bob, score: 95}, {name: Carol, score: 82}, ] students.sort(keylambda s: s[score], reverseTrue) # 按分数从高到低排 # 2. operator.itemgetter按字典字段排序比 lambda 快 students.sort(keyitemgetter(score)) # 3. operator.attrgetter按对象属性排序 class Student: def __init__(self, name, age): self.name name self.age age objs [Student(Alice, 20), Student(Bob, 19)] objs.sort(keyattrgetter(age))为什么推荐itemgetter而不是lambda因为itemgetter底层是 C 实现的在数据量大到几万几十万条时性能差距能跑到 10% 以上。我在处理上百万行日志去重排序时就吃过这个性能亏后来统一换成itemgetter和attrgetter肉眼可见地快了一截。另外要特别提醒key函数是在排序前对每个元素调用一次还是排序过程中反复调用答案是每个元素只调用一次然后 sort 内部会缓存这些返回值。所以不用担心key函数被重复执行但反过来也说明key函数如果有副作用副作用也只会发生一次别指望靠它做额外操作。2.3 reverse 参数与排序稳定性理解相等元素怎么办reverseTrue就是降序没什么好说的。真正值得花心思的是理解排序稳定性配合多级排序的威力。场景是这样的有一批订单你要先按优先级排优先级相同的再按下单时间排。如果排序是不稳定的这个需求就得写一个复杂的 key 函数同时比较两个字段。但因为 Python 的 sort 稳定你可以分两步走orders [ {priority: 1, time: 2023-06-01}, {priority: 0, time: 2023-06-03}, {priority: 1, time: 2023-06-02}, ] # 先按下单时间排 orders.sort(keylambda o: o[time]) # 再按优先级排优先级相同的会保持前面的时间顺序 orders.sort(keylambda o: o[priority], reverseTrue)这句再按主排序字段排之所以有效就是因为稳定性保证了前一轮排序的结果不会被后一轮完全打乱只是在更高优先级上做了重新划分。如果你用不稳定的排序这个写法得到的结果就可能出现优先级相同但时间乱掉的情况。对降序多级排序还有个细节如果 second key 也要倒序在元组里没法直接写-得用reverse参数分步处理或者对数值字段取负值。字符串字段就不能简单取负所以我更推荐分步排序逻辑更清晰不容易错。3. 五种高频排序场景的实战写法3.1 数字列表从基本排序到自定义规则数字排序看起来最简单但自定义规则时容易绕晕。先看基本盘nums [3, 1, 4, 1, 5, 9, 2, 6] print(sorted(nums)) # 升序 [1, 1, 2, 3, 4, 5, 6, 9] print(sorted(nums, reverseTrue)) # 降序 [9, 6, 5, 4, 3, 2, 1, 1]自定义规则的典型需求是按绝对值排序、按距离某个数的远近排序nums [-4, 3, -1, 6, -9] # 按绝对值升序 print(sorted(nums, keyabs)) # [-1, 3, -4, 6, -9]keyabs的意思是拿每个数的绝对值去参与比较但返回的还是原数。这个映射后比较但返回原值的机制是理解所有 key 用法的关键。有些初学者以为sorted(nums, keylambda x: abs(x))会返回绝对值列表这是个经典的误区。还有一点必须讲如果列表里混入了字符串和数字直接 sort 会抛TypeError: not supported between instances of str and int。这是因为 Python 不允许字符串和数字直接比大小。如果非要按某种规则排你得自己定义 key 把类型统一掉data [3, 10, 1, 20, 2] # 统一转成 int 来排序 print(sorted(data, keyint)) # [1, 2, 3, 10, 20]3.2 字符串排序字典序、长度序与忽略大小写字符串排序的默认规则是按 Unicode 码点序也就是字典序。这个顺序对英文一般符合直觉但要处理大小写时就会出问题因为大写字母的码点比小写字母靠前apple和Banana排序时Banana会排在前面这显然不是日常想看到的。解决办法就是 key 参数配合大小写转换words [banana, Apple, cherry, Date] print(sorted(words)) # [Apple, Date, banana, cherry] print(sorted(words, keystr.lower)) # [Apple, banana, cherry, Date]按字符串长度排序也是高频需求words [python, go, java, c] print(sorted(words, keylen)) # [c, go, java, python]中文排序是个重灾区。Python 默认按 Unicode 码点排序这个顺序既不是拼音序也不是笔画序比如张和王谁在谁前面完全看码点。真要按拼音排得额外引入pypinyin库或者用sorted(words, keylambda s: pypinyin.lazy_pinyin(s))。这里先不展开后面专门写一节。3.3 字典列表和对象列表按字段排序的正确姿势处理[{...}, {...}]这种字典列表是 Python 后端开发最常遇到的排序场景。排序依据是某个键的值而且经常要处理倒序。users [ {name: Alice, age: 25}, {name: Bob, age: 30}, {name: Carol, age: 22}, ] # 按年龄升序 users.sort(keylambda u: u[age]) # 按年龄降序 users.sort(keylambda u: u[age], reverseTrue) # 性能更好的写法 from operator import itemgetter users.sort(keyitemgetter(age), reverseTrue)如果列表里是自定义类的实例就把itemgetter换成attrgetter。这个模式在工作里太常用了订单按金额排序、用户按注册时间排序、日志按时间戳排序本质都是同一个套路。还有一个常见需求是按多个字段排序比如先按部门排、部门内再按工号排。用元组作为 key 的返回值就能一步到位employees [ {dept: sales, level: 3}, {dept: tech, level: 5}, {dept: sales, level: 1}, {dept: tech, level: 2}, ] # 先分组再同级内升序key 返回 (dept, level) employees.sort(keylambda e: (e[dept], e[level]))如果要让 dept 升序、level 降序元组里不能直接负号字符串字段可以用分步排序配合稳定性employees.sort(keylambda e: e[level], reverseTrue) # 先排次要字段 employees.sort(keylambda e: e[dept]) # 再排主要字段3.4 多级排序用元组 key 实现复合排序规则多级排序的核心技巧就是让 key 返回一个元组sort 会按元组里元素的顺序逐级比较。元组的比较规则是先比较第一个元素如果相等再比较第二个以此类推。有个很经典的排行榜需求先按分数降序分数相同再按姓名升序。但reverseTrue会让两个维度都变成降序这就有问题了。解决办法是这样# 分数降序、姓名升序 students.sort(keylambda s: (-s[score], s[name]))数值字段可以取负来反向字符串字段就不能这么玩了。所以对字符串降序 其他字段升序这类混合方向排序最稳的办法还是分步排序利用上一节说的稳定性。总结一下全部升序或全部降序key返回元组配合reverse方向不一致对数值字段取负或者分步排序。分步排序还有一个额外好处代码可读性比一长串元组表达式好得多出 bug 也容易定位。3.5 自定义排序规则从按字符串长度到按业务顺序到了自定义规则这一步key 函数的威力才完全发挥出来。你可以把任何能从元素中提取出一个可比较数值的逻辑塞进 key 里。举几个高频需求按某个状态字段在业务里的优先级排而不是按字典序。比如订单状态有 pending、paid、shipped、done你希望按这个业务顺序排这时建一个映射表status_order {pending: 0, paid: 1, shipped: 2, done: 3} orders.sort(keylambda o: status_order[o[status]])这比把状态设计成数字再排数字要灵活得多而且逻辑一目了然。对业务字段做加权计算比如按综合评分 阅读量 * 2 点赞数 * 3排序posts.sort(keylambda p: p[views] * 2 p[likes] * 3, reverseTrue)把字符串里的数字提取出来按数值排这在处理文件名file1、file2、file10时尤其常见。直接排会得到字典序[file1, file10, file2]。想要自然序就得提取数字import re files [file10, file2, file1] files.sort(keylambda s: int(re.search(r\d, s).group())) # 结果是 [file1, file2, file10]自定义排序规则的核心方法论只有一句话想清楚排序依据是什么把它提取成一个可比较的 key其他的交给 sort。4. 其他语言里的 sort同一工具不同脾气4.1 JavaScript默认比较行为是个大坑JavaScript 的 sort 我见过太多人踩坑了最经典的const nums [3, 1, 10, 2, 21]; nums.sort(); console.log(nums); // [1, 10, 2, 21, 3]为什么是这个结果因为 JavaScript 的 sort 默认把所有元素转成字符串然后按 UTF-16 码元顺序比较。数字10转成字符串10它的第一位是1排在2前面所以 10 会跑到 2 前面。正确做法是传入比较函数const nums [3, 1, 10, 2, 21]; nums.sort((a, b) a - b); // 升序 [1, 2, 3, 10, 21] nums.sort((a, b) b - a); // 降序 [21, 10, 3, 2, 1]比较函数的规则是返回负数表示 a 排在 b 前返回正数表示 a 排在 b 后返回 0 表示相等。这个规则很多人记混我提供一个记忆方式直接想成a - b的结果正数说明 a 大大数往后排就是升序。对象数组按属性排const students [ { name: Alice, score: 89 }, { name: Bob, score: 95 }, ]; students.sort((a, b) b.score - a.score); // 按分数降序字符串数组忽略大小写排const words [banana, Apple, cherry]; words.sort((a, b) a.toLowerCase().localeCompare(b.toLowerCase()));中文按拼音排需要localeCompare加上中文语言标签a.name.localeCompare(b.name, zh-Hans-CN)否则不同浏览器得结果可能不一致这个取决于本机系统语言环境。4.2 Cstd::sort 的迭代器世界C 的排序思路和脚本语言不太一样。std::sort接收的是迭代器区间默认用operator升序排想降序就传第三个参数可以是一个函数对象、函数指针、或者 lambda。#include algorithm #include vector #include functional std::vectorint v {5, 3, 8, 1}; // 升序 std::sort(v.begin(), v.end()); // 降序 std::sort(v.begin(), v.end(), std::greaterint()); // 自定义规则按个位数大小排 std::sort(v.begin(), v.end(), [](int a, int b) { return a % 10 b % 10; });std::sort最大的两个注意事项第一它要求随机访问迭代器所以std::list不能用std::sort链表要用自己的sort()成员函数第二它不保证稳定性同等级元素顺序可能被改变需要稳定就换std::stable_sort。对结构体按字段排序时C 最常见的写法是 sort 外面传 lambdastruct Person { std::string name; int age; }; std::vectorPerson people {{Alice, 25}, {Bob, 30}, {Carol, 22}}; std::sort(people.begin(), people.end(), [](const Person a, const Person b) { return a.age b.age; // 按年龄升序 });C 的 lambda 比较规则和 JavaScript 类似返回true表示 a 排在 b 前面。区别是 C 用表达语义JavaScript 用a - b的正负本质都是一样的比较两个元素决定次序。4.3 各语言 sort 行为速查表语言默认规则是否稳定原地排序自定义方式Pythonlist.sort()按元素自身大小数字按值、字符串按码点稳定是keyreversePythonsorted()同上稳定否返回新列表同上JavaScriptsort()全部转字符串按 UTF-16 码元稳定ES2019 起是传入比较函数Cstd::sort()按operator不稳定是原地第三参数函数对象/lambdaCstd::stable_sort()按operator稳定是原地同上这张表建议存一下。跨语言开发时出问题百分之八九十都出在默认规则和稳定性这两列上。5. 常见问题与排查技巧5.1 sort 返回 None不是 bug 是特性Python 新手最容易困惑的就是这个lst [3, 1, 2] new_lst lst.sort() print(new_lst) # Nonelist.sort()是原地排序它故意返回None而不是排好的列表目的是提醒你它改了原列表防止你把两个用法混在一起。你需要返回新列表时就老老实实用sorted()。5.2 None 值混在列表里怎么办不管是 Python、JavaScript 还是别的语言处理含空值的列表排序都很烦因为None和数字、字符串都不好比较。Python 里的处理思路是key 函数把None映射成极值让它们固定排在最前或最后data [3, None, 1, None, 2] # None 永远排到最后其余按升序 data.sort(keylambda x: (x is None, x if x is not None else 0)) # 结果: [1, 2, 3, None, None]原理是 key 返回一个元组(是否为None, 值)如果第一个元素不同就按它比较那么非 None 的元组第一项是False永远排在True前面。这个模式的变体可以处理很多特殊值优先/滞后的需求。5.3 key 函数太重排序性能崩了怎么办很多人写排序先把复杂度放在算法上实际生产里更常遇到的是 key 函数太重导致排序慢。比如你从数据库拉了 10 万条记录每条在 key 里做一轮 JSON 解析或正则提取那排序时间就全耗在重复计算上了。Python sort 保证 key 函数每个元素只调用一次已经帮你省了很多。但如果 key 函数本身计算量很大还有一个经典优化叫装饰-排序-去装饰也就是 DSU 模式# 假设要根据一个很耗时的计算函数排序 def expensive_key(x): # 模拟耗时操作 return len(complex_calc(x)) # 一次性把所有 key 算好存下来 decorated [(expensive_key(x), x) for x in data] decorated.sort() result [x for _, x in decorated]其实key参数出来之后Python 底层已经帮你做了类似 DSU 的事情所以正常用key就行。真正需要手工 DSU 的场景很少但理解这个思路对排查性能问题有帮助。5.4 中文排序为什么结果总是不对中文排序在 Python 和 JavaScript 里都是老大难问题根源在于默认排序规则跟自然语言理解的顺序完全是两回事。Python 里sorted对中文按 Unicode 码点排序这个顺序既不是拼音序也不是笔画序。想要按拼音排常见的方案是装pypinyinfrom pypinyin import lazy_pinyin names [张三, 李四, 王五] names.sort(keylazy_pinyin)lazy_pinyin会把每个汉字转成拼音字符串再按拼音字母序排。这样基本能达到自然语言里的拼音排序效果。注意多音字的问题比如重庆会被读成zhongqing而不是chongqing服务端处理这种需求时最好专门维护一个多音字表。5.5 排序稳定性引发的隐藏 bug这类问题最隐蔽同事写了个排序结果重复项跑到哪儿去了都不一定。举个例子你在做榜单希望分数相同的两个人按后提交的排前面。如果排序不稳定这个需求就得靠 compare 函数里额外比较时间字段。但因为 Python 的 sort 稳定你可以先按提交时间升序排序再从后往前遍历的同时做一次降序分数排序。利用稳定性就能精确控制平局时的次序。C 里则恰恰相反std::sort不稳定这种对次序有要求的场景必须用std::stable_sort或者干脆比较函数里同时包含两个字段。说实话排序稳定性的价值不在教科书里而在真实业务场景的相等元素怎么处理这件事上。早想清楚早少踩坑。5.6 快速定位排序问题的排查清单遇到排序结果不符预期按下面这个顺序过一遍基本能定位问题确认默认规则是不是你预期的尤其 JavaScript 转字符串的坑检查比较方向升序还是降序a - b写反没有确认比较/映射规则作用的是排序依据而不是元素本身检查是否存在混合类型字符串数字混排、None 混排确认排序是否原地修改了原数组/原列表对平局次序有要求时确认排序算法是否稳定中文场景确认排序规则是按拼音、笔画还是码点有没有用对应函数。排查时多写几行中间输出看排序前的 key 值序列长什么样。比如 Python 里临时跑一个[(key_func(x), x) for x in data]看看映射后是不是正确的排序依据。这一步能筛掉八成问题。6. 一点使用习惯最后分享几个这些年沉淀下来的习惯算不上什么了不起的经验但确实帮我省过很多时间。第一个习惯是每到一个新语言环境先花两分钟跑一个简单的 sort 测试搞清楚默认规则、原地修改、稳定性这三个属性再放心写业务代码。这么做过一轮的语言后面基本没在排序上出过看不懂的结果。第二个习惯是排序前先把数据整理干净把类型统一、空值处理掉再上 sort不要让 sort 去处理脏数据。排序函数本身很无辜绝大多数排序的灵异事件都是数据问题。第三个习惯是多用 key 而不是手写复杂的比较函数key 把排序依据提取出来的写法比在比较函数里绕来绕去要好读得多也难出错。排序这活儿看起来是基础中的基础可就因为基础很多人反而不愿意停下来系统过一遍于是反复踩同样的坑。把这篇文章里提到的场景和问题过一遍以后再遇到 sort应该能直接放心使用了。
返回列表