我见过太多人把算法题写得很“苦”不是思路不行而是在算法学习的过程中栽在了常用函数上排序想自己写快排二分非要手撸结果不是边界写错就是超时。说实话刷算法题这件事一半拼思路一半拼对语言内置函数的熟悉程度。这篇我就把算法学习里最常用的一批函数按场景拆开来讲包括排序查找、数值计算、字符串处理、容器操作和输入输出优化逐个说清楚为什么用、怎么用、有哪些隐藏的坑。适合刚开始学算法、准备面试刷题、或者偶尔参加竞赛练手的读者哪怕你只熟练一门语言看完也能对号入座。先说一句个人体会。很多人以为熟练掌握一门语言就是会写循环和条件判断但真正的分水岭恰恰是能不能把标准库用出花来。同样是快排自己写十几行还容易在极端数据上退化人家一行sorted就带稳定性内部还是 Timsort 优化过的同样是维护前 K 大的序列手写堆容易出边界 bugheapq三行搞定。这不是偷懒而是把成熟工具的正确性、复杂度保证和边界处理都借过来用。文章里我会以 Python 为主线索因为它在算法学习里最直观也最好讲透同时在关键位置对比 C 的 STL 和 Java 标准库这样不管你主用哪门语言都能找到对应的姿势。1. 排序与查找最高频的一类函数1.1 sorted 与 sort不只是快排先讲排序因为算法题里排序出现的频率实在太高了。Python 里最常用的是sorted和list.sort()两兄弟。区别简单说list.sort()是原地排序直接修改原列表返回Nonesorted(可迭代对象)返回一个新列表不修改原数据。这个区别很容易踩坑——有些代码里写nums.sort() ans nums # 以为复制了一份其实还是同一个对象后续修改ans会连带影响nums。所以只要不想改原数据就用sorted。key参数是另一个值得说透的东西。很多新手只知道sorted(nums)排数字遇到字典列表就懵。实际上key决定了“按什么排”items [{name: apple, price: 3}, {name: banana, price: 1}] items.sort(keylambda x: x[price]) # 按价格升序 items.sort(keylambda x: (-x[price], x[name])) # 价格降序价格相同按名称升序key返回的是排序依据而不是比较函数这是和 C 的std::sort自定义比较器最不一样的地方。很多人把 C 的习惯带过来写items.sort(lambda a, b: a[price] b[price])在 Python 里语法虽然能过但行为完全不对因为lambda接收的是元素而不是元组比较。正确姿势永远是keylambda x: ...。还有稳定性。Python 的sort是稳定的意思是key相等的元素保持原有相对顺序。这个特性在“按多个条件排序”时特别好用先按次要条件排一次再按主要条件排一次稳定排序会保留前一次的次序。比如按成绩降序、成绩相同按姓名升序先按姓名升序排再按成绩降序排一步都不用写复合key。性能上Python 排序用的是一种特殊的归并排序变体 Timsort最好情况下对接近有序的序列接近 O(n)最坏也是 O(n log n)常数比大多数人手写的快排小。C 的std::sort是快速排序和插入排序混合不稳定需要稳定排序时用std::stable_sort。Java 里Arrays.sort对基本类型用双轴快排对对象数组用 TimSort稳定这点经常被忽略。1.2 二分查找调库还是手写有序数组里找目标值如果不限制必须手写二分我强烈建议直接用bisect模块。它长期被低估其实它是算法刷题里最值得背熟的模块之一。核心就两个函数from bisect import bisect_left, bisect_right arr [1, 3, 5, 5, 7] bisect_left(arr, 5) # 返回 2第一个 5 的位置 bisect_right(arr, 5) # 返回 4第一个 5 的位置bisect_left返回的是“第一个大于等于目标值”的下标bisect_right返回的是“第一个大于目标值”的下标。两者相减就是目标值在数组中出现的次数。这个语义对应 C 里的lower_bound和upper_boundJava 的Arrays.binarySearch返回的则可能是负数表示插入点为-insertion_point - 1用起来反而绕一些。用bisect的时候最容易犯的错是忽略“返回的是插入点不代表目标存在”。判断目标是否存在必须额外检查pos bisect_left(arr, 5) if pos len(arr) and arr[pos] 5: print(找到了) else: print(不存在)还有一点bisect只保证找到位置不保证复杂度以外的任何事。它的内部是二分查找O(log n)但是如果你每次都insort插入元素插入本身是 O(n) 的整体还是 O(n^2)别指望拿它当动态有序序列的高效解法。那种场景应该用堆或者维持前缀排序。有人问面试时允许用bisect吗我的经验是绝大多数算法面试不考察“手写二分”本身面试官要的是你能不能在有序数据里快速定位。当然如果你写的是“在值域上二分找答案”这类题目比如求最小可行值或者带约束的最优解内置二分帮不上忙那才需要自己写模板。我一般建议把一套二分模板背熟同时把bisect也背熟两者互补。2. 数值计算与数学工具别自己造轮子2.1 gcd、pow、abs基础数学函数别自己造轮子很多算法题牵扯到数学运算新手最常犯的毛病是看到最大公约数就开始手写辗转相除法。Python 3.5 的math.gcd、Python 3.9 的math.lcm都已经提供了C17 也有std::gcd和std::lcm。手写通常不超过十行但调库的好处是边界处理正确传 0、传负数都有明确行为还能避免在多个文件里重复造轮子。另一个容易被忽略的是pow的三参数形式pow(2, 10, 1000) # 求 2 的 10 次方对 1000 取模结果 24这和普通pow(2, 10)完全不同它内部走的是快速幂取模能在答案很大时不产生中间大数。很多组合数、模运算题都会用到尤其是结果要求 mod 10^97 的题目。如果不调库自己手写快速幂还要注意乘法取模的精度问题。Python 里pow(a, b, mod)直接解决一行搞定。abs、min、max看起来太简单了但min/max其实支持key参数这在实际刷题里非常实用。比如要从一堆坐标里找离原点最近的点nearest min(points, keylambda p: p[0]**2 p[1]**2)或者找出句子中出现次数最多的字符most max(counter, keycounter.get)很多人不知道这用法于是写循环加临时变量多出一堆无意义的代码。2.2 浮点与精度开方、对数、无穷大的那些坑数学函数里最容易出问题的是浮点精度。math.sqrt(9)返回 3.0看着没毛病但判断一个数是不是完全平方数时用int(math.sqrt(n))**2 n在 n 特别大的时候可能出错。Python 里更稳的写法是math.isqrtfrom math import isqrt n 9876543210123456789 r isqrt(n) print(r * r n) # 准确判断isqrt返回整数平方根向下取整没有任何浮点转换适合大数判断。类似地判断一个数是不是 2 的幂时很多人写while n % 2 0之类其实只要n (n-1) 0且n 0就能判这些位运算技巧在算法题里比浮点数函数可靠得多。math.log也有精度问题。需要计算“以 2 为底的整数对数”时直接用math.log(n, 2)可能会因为浮点误差得到 2.9999999999999996然后int掉就得到 2。稳妥做法是用n.bit_length() - 1来求整数对数下界或者在需要精确判断时改用循环逐步逼近。再有一个高频常量math.inf。动态规划里经常要初始化一个“不可能的大值”很多人用 10**9 或者float(inf)其实直接math.inf更清楚而且它和任何有限数比较都成立不会出现“选的极大值还不够大”的尴尬。C 里没有真正的无穷大一般用std::numeric_limitsint::max() / 2来避免加法溢出Java 里同理用Integer.MAX_VALUE / 2这个“除以二”的细节很多人都不知道——因为原值加一个正数会溢出成负数比出来反而变小。2.3 组合数学与枚举itertools 帮你省一半脑细胞算法题里经常会遇到“枚举所有排列/组合”“求笛卡尔积”的需求。itertools里的permutations、combinations、product这三兄弟几乎覆盖了所有暴力枚举场景from itertools import permutations, combinations, product permutations([1, 2, 3]) # 所有排列 combinations([1, 2, 3], 2) # 所有长度为 2 的组合 product([0, 1], repeat3) # 三位二进制数的所有可能它们返回的都是迭代器不会一次性把所有结果展开到内存里在枚举量很大的时候非常友好。用product([0, 1], repeatn)可以直接枚举 n 位的所有状态这在状态压缩题目里是常规操作。另一个很好用的是itertools.accumulate可以生成前缀和from itertools import accumulate pref list(accumulate(nums)) # 等价于逐位累加前缀和在区间查询里是基础中的基础能一行生成就不用手写循环。不过要注意accumulate默认是加法如果要做乘法或者自定义融合可以通过参数指定函数。3. 字符串处理函数刷题中最容易忽略的细节3.1 拼接、分割、查找标准姿势与性能陷阱字符串在算法题里出现频率极高但很多人对 Python 字符串的操作理解停留在“能跑就行”。先说拼接循环里用拼接字符串在数据量大时是 O(n^2) 的因为每次拼接都会生成一个新字符串并复制所有已有内容。正确做法是收集到列表里最后joinparts [] for word in words: parts.append(word) result .join(parts)这个例子虽然简单但join的写法在复杂题目里能让性能差出一个数量级。C 里对应的是用std::string的底层如果预分配了容量性能还好Java 里用StringBuilder注意别在循环里写s c。分割、查找也有讲究。str.split()默认按任意空白分割会去掉空串str.split(,)按指定分隔符分割。很多人在解析输入数据时忘记split可以带参数比如按空格分割却不知道split()不加参数时会自动处理多个连续空格。find和index的区别是一个经典坑find找不到返回 -1index找不到抛ValueError。算法题里如果用index就得try/except或者先判断字符串里是否存在反过来用find在找不到时可以安静返回 -1省去异常处理。但注意index的语义更明确适合确实认为目标一定存在的情况——比如已经用in判断过之后再调用index这样逻辑更清晰。startswith、endswith、count、isalpha、isalnum这些方法看着简单用对场景能减少大量正则表达式。比如判断一个字符是不是字母数字用c.isalnum()判断字符串是否纯大写用s.isupper()。在验证类题目里这些方法效率很高也几乎没有边界问题。3.2 翻转、编码、进制转换那些随手就能用的内置能力翻转字符串是笔试题里的网红题。Python 一行s[::-1]搞定不用手写双指针。不过这招只对str和list有效对不可变对象没问题如果要在原列表上翻转用nums.reverse()。字符和 ASCII 码的转换用ord和chr写哈希类题目统计字符频次映射到数组下标时经常会用到。比如统计小写字母频次cnt [0] * 26 for c in s: cnt[ord(c) - ord(a)] 1进制转换也是一组高频内置函数bin(n)、oct(n)、hex(n)分别生成0b/0o/0x前缀的字符串int(s, base)可以把任意进制字符串转回整数比如int(ff, 16)得到 255。很多面试题让你手写十进制转二进制其实调库只需一行当然手写个别题也别完全跳过基础能力还是要有。字符串与数字互转也有讲究。int(123)直接转但int(3.14)会报错需要float先转换或者用Decimal处理。刷在线评测时输入往往直接是字符串解析成int是常事注意边界就能少踩很多坑。3.3 正则表达式要不要用什么时候用正则表达式re模块在文本处理里非常强大但在算法题里我个人不太推荐默认使用。原因有两个一是性能复杂正则的匹配速度远没有直接字符串方法快二是可读性几个人合作维护代码时一段正则往往比三段普通代码更难理解。真要处理像“解析 IP 地址”“匹配括号嵌套”这类问题直接手写解析比正则更可控。但有一类题目例外当验证逻辑本身就是一个标准格式时比如判断字符串是否只包含数字和字母或者提取所有数字用re.findall(r\d, s)会非常顺手。我的原则是能用str自带方法解决的不用restr解决不了且re能明显减短代码量的才用re。而且如果决定用re最好在写之前先想清楚模式匹配的边界比如贪婪匹配 vs 非贪婪匹配搜索时漏掉边界很容易出诡异错误。4. 容器与数据结构操作堆、栈、哈希表的高效用法4.1 heapq用内置堆处理动态极值堆在算法题里主要用于求 Top K、合并有序序列、滑动窗口最大值这类动态极值问题。Python 的heapq模块提供最小堆heappush和heappop都是 O(log n)。heapify可以把一个普通列表在原位变成堆O(n)。常见的误区有两个。第一Python 没有直接的“最大堆”需要用负数技巧要取最大元素就把-x入堆取出时再取负。第二heapq不支持自定义比较器往堆里存对象时只能存元组比如存(-score, name)这样堆会先按-score比较再按name比较。那 C 的priority_queue呢默认是最大堆而且支持自定义比较器需要最小堆时可以用greaterT或者直接用priority_queueint, vectorint, greaterint。Java 的PriorityQueue默认是最小堆最大堆需要传入比较器。三个语言的默认方向都不一样每次切换语言时容易搞混建议记一张小表。heapq还有一个容易被忽视的兄弟函数nlargest和nsmallest它们用堆或者排序的混合策略在数据量不算很大时一条语句就能拿到 Top Ktop3 heapq.nlargest(3, scores)4.2 deque 与栈别再用 list.pop(0) 了Python 的list其实自带append和pop栈操作很方便。但如果想用队列或者需要在两端操作list就不合适了list.pop(0)是 O(n) 的因为要移动所有元素。正确的队列姿势是collections.deque它的popleft和appendleft都是 O(1)在滑动窗口、BFS 这类场景里是标配。deque还可以用来模拟“单调队列”这是解决滑动窗口最大值等问题的经典姿势此时不需要heapq因为窗口是滑动的堆无法处理过期元素。deque配合双端操作能保持候选下标单调递减每次取队首就是最大值全程每个元素最多进出一次整体 O(n)。BFS 时deque的作用更是直接queue deque(); queue.append(start);然后循环popleft。这些都是面试高频题背后的基础设施值得单独练熟。C 里对应的是std::dequestd::stack和std::queue都是容器适配器底层默认用deque。Java 里更推荐ArrayDeque而不是Stack因为Stack继承了Vector同步开销大而且ArrayDeque可以通过addFirst/removeFirst或addLast/removeLast灵活实现栈或队列。4.3 Counter 与 defaultdict哈希表的正确打开方式统计频次是哈希表最典型的应用。Python 的collections.Counter不仅统计频次还有几个很实用的操作most_common(n)取出频次最高的 n 个元素两个Counter可以直接加减返回的也是Counter这在处理多字符串频次比较、字母异位词分组时特别顺手。from collections import Counter cnt Counter(s) # 一行统计所有字符频次 cnt.most_common(1) # 出现频次最高的元素 cnt1 cnt2 # 频次对应相加defaultdict解决的是“访问不存在的键自动初始化”的问题。以前写哈希表总是要先if key not in dict再初始化用defaultdict(lambda: 0)或defaultdict(list)之后代码能删掉一大片样板代码。不过要注意defaultdict在访问不存在的键时会自动创建这在查询时可能会产生多余的空键偶尔会掩盖逻辑错误。只想查询不想写入时用dict.get(key, default)或者dict.setdefault更安全。Java 里的HashMap没有直接对应Counter但 Java 8 的merge方法、getOrDefault能让统计代码简洁不少。C 里则是std::unordered_map配合mp[key]一行搞定计数这个操作在三种语言各有各的写法相互迁移时容易踩坑建议记牢。5. 输入输出与性能优化抢时间的关键5.1 慢 IO 是超时的隐藏元凶很多人刷题喜欢用input()一行行读在小数据量时没有问题但数据量到 10^5 级别input()的开销就可能让你超时。Python 里更快的读写姿势是用sys.stdin的readline或者readimport sys data sys.stdin.buffer.read().split() n int(data[0]) for val in data[1:]: ...sys.stdin.buffer.read()一次读入全部数据再split比逐行input()快出一个量级。输出同理大量print也会拖慢速度可以拼接后一次性sys.stdout.write。这不是什么高深技巧纯粹是 IO 缓冲的差别。C 里对应的是ios::sync_with_stdio(false)和cin.tie(nullptr)这两行关掉和 C 标准库的同步、以及和cout的绑定后cin/cout速度能提升很多。Java 里对应的是BufferedReaderStringTokenizer而不是ScannerScanner在数据量大时慢得令人抓狂输出侧用StringBuilder一次性打印。这里也想强调一下IO 优化不需要在每道题里都做。几十行的小样例用最简单的方式读就行。判断标准很简单——当数据量超过 10^5 或者题目明确指出“输入规模较大”时再切换到快速 IO。提前优化反而会增加代码复杂度让调试变得麻烦。5.2 构造测试数据与调试技巧函数用熟了之后另一个提高效率的点是调试。算法题里最讨厌的 bug 往往是边界问题比如空数组、只有一个元素、目标值不存在、负号开头。我自己的习惯是写一个随机数据生成器用最小样例验证逻辑边界然后跑随机大样例看性能是否符合预期。比如构造 10^5 个随机整数的输入用.sort()后是否能秒回很快就能测出 IO 是不是瓶颈。内置函数排错时还有一个小技巧把中间结果打印出来但不要全量打印。比如只打印长度、首尾、频率最高的几个元素而不是把整个数组铺开。数据量大的时候print本身就会干扰性能分析我还见过有人把 10^5 个元素全部打印出来最后光输出就占了大量时间问题反而没看清。6. 常见问题与避坑实录6.1 调库反而更慢的几种情况不是所有情况都该调库。Python 里sorted很快但如果你只需要找一份数据里的最大值和最小值用max和min各扫一遍比sort再取首尾更高效因为排序是 O(n log n)max/min是 O(n)。同样道理如果只需要“前 3 大”直接用heapq.nlargest(3, data)通常比sorted(data)[-3:]更快因为nlargest在 k 远小于 n 时会走堆逻辑避免完整排序。C 里std::sort很快但如果不小心在循环里对子数组反复排序整体复杂度可能变成 O(n^2 log n)。函数本身不是问题问题是对函数的错误组合。这提醒我们调库的正确姿势是理解每个函数的时间复杂度再决定在什么位置调用。6.2 可变对象与返回值陷阱有些坑不是函数本身的问题而是语言特性。比如 Python 的list.sort()返回None如果你写nums nums.sort()nums就变成None了经典错误。再比如默认参数陷阱def add_element(x, arr[]): arr.append(x) return arr这个默认列表在多次调用之间是共享的第二次调用arr还是第一次的那个列表。看似和函数库无关但一旦你开始封装“常用函数集合”时就会踩到。还有一种更隐蔽的用*解包生成新列表时[ [0] * n for _ in range(m) ]才是正确的二维数组初始化而[[0] * n] * m会让每一行指向同一个列表修改一个就全部跟着变。6.3 快速自测清单写完一段用函数的代码后我习惯按这个清单快速过一遍我在原地排序还是需要保持原列表用sort还是sorted二分时是否确认了目标存在数据规模到 10^5 了吗用sys.stdin快速读了吗字符串拼接频率高吗该用join吗我是不是在用list模拟队列该换deque吗浮点数比较用了吗该用isclose或者整数判断吗统计频次是不是手写一堆if了Counter能简化吗这些问题既是避坑清单也是算法学习过程中的自查表。把这张表过一遍常见的小毛病基本都能在提交前发现。7. 常用函数速查按场景记忆7.1 一句话速查表这个部分做成速查表方便刷题时顺手一翻。Python 为主线C 和 Java 只列最核心对应项。场景PythonCJava排序sorted/list.sort(key...)std::sort/std::stable_sortArrays.sort/Collections.sort二分查找bisect_left/bisect_rightstd::lower_bound/std::upper_boundArrays.binarySearch最大公约数math.gcd/math.lcmstd::gcd/std::lcm手写 / 扩展库幂与取模pow(a, b, mod)手写快速幂BigInteger.modPow整数开方math.isqrtstd::sqrt需注意精度手写Top Kheapq.nlargest/nsmalleststd::priority_queuePriorityQueue双端队列collections.dequestd::dequeArrayDeque频次统计collections.Counterstd::unordered_mapHashMapmerge/getOrDefault字符串拼接.join(parts)需预分配StringBuilder进制转换int(s, base)/bin/hexstd::stoi(s, 0, base)Integer.parseInt(s, base)7.2 怎么用这张表别背要用这张表不是让你全背下来而是提醒你“这个场景其实有标准工具”。遇到问题先想一想有没有现成函数再考虑手写实现。很多时候我们和“老手”之间的差距不是算法思路本身而是对标准库的掌握程度。把表当成一个索引遇到不熟的函数就查一下文档跑一个小例子很快就能形成肌肉记忆。我个人刷了几百道题之后的体会是常用函数不是背出来的是用出来的。我的建议是每次看到一道题先不要急着写循环先想“有没有标准库函数能帮我处理这部分”用几次自然就熟了。也别怕查文档Python 文档、C cppreference 都是常开页面查完顺手在编辑器里跑一个小例子比死记硬背强得多。还有一个我自己的习惯建一个 snippets 文件把sorted(key...)、bisect、pow(a,b,mod)、deque、Counter这种高频又容易忘的函数模板都存在里面遇到新题先用 snippets 拼骨架再填业务逻辑速度会快很多。希望看完这篇你在算法学习路上能少踩几个坑。