平时编码都用 C,但 C 刷算法效率太低(手写哈希/手动内存管理/一个反转字符串要写 5 行)。Python 把该省的事全省了。本文目标是:有 C 基础 + 零 Python 经验 → 读完能直接上 LeetCode 做题 。
算法基础练习总结入口:我的算法地图
文章目录
-
- [一、背景:为什么要从 C 切换到 Python 刷题](#一、背景:为什么要从 C 切换到 Python 刷题)
-
- [1. C vs Python 刷题差异总览---一张表看清省了什么、代价是什么](#1. C vs Python 刷题差异总览---一张表看清省了什么、代价是什么)
- [2. Python 刷题的核心哲学---代码是解法的直译,不是在"编程"](#2. Python 刷题的核心哲学---代码是解法的直译,不是在"编程")
- [3. 刷题需要的 Python 工具箱速览---一句话记住全景图](#3. 刷题需要的 Python 工具箱速览---一句话记住全景图)
- [二、基础语法:C 程序员最关心的差异---数据类型、控制流、函数](#二、基础语法:C 程序员最关心的差异---数据类型、控制流、函数)
-
- [1. 基础数据类型---int / float / bool / None,四种就够](#1. 基础数据类型---int / float / bool / None,四种就够)
- [2. 数字运算---/ 总是浮点结果,// 才是整除,** 是乘方](#2. 数字运算---/ 总是浮点结果,// 才是整除,** 是乘方)
- [3. 控制流---and/or/not 替代 &&/||/!,缩进替代 {},for 只有 range 一种写法](#3. 控制流---and/or/not 替代 &&/||/!,缩进替代 {},for 只有 range 一种写法)
- [4. 函数---不声明类型 + 多返回值](#4. 函数---不声明类型 + 多返回值)
- [三、输入输出---极简但 ACM 模式要知道几招](#三、输入输出---极简但 ACM 模式要知道几招)
-
- [1. C vs Python 输入输出对照---scanf/printf → input/print,一张表覆盖语法迁移](#1. C vs Python 输入输出对照---scanf/printf → input/print,一张表覆盖语法迁移)
- [2. 基础输入 `input()`---读一行字符串,剩下的自己拆](#2. 基础输入
input()---读一行字符串,剩下的自己拆) - [3. 基础输出 `print()`---默认换行,用参数调行为](#3. 基础输出
print()---默认换行,用参数调行为) - [4. ACM 四种输入模式---读到 EOF / 先读组数 / 不定长 / 混合输入,四段模板复制即用](#4. ACM 四种输入模式---读到 EOF / 先读组数 / 不定长 / 混合输入,四段模板复制即用)
- [四、基础容器---list / dict / str / set / tuple,搞懂这五个 = 算法题不卡壳](#四、基础容器---list / dict / str / set / tuple,搞懂这五个 = 算法题不卡壳)
-
- [1. list------万能动态数组,malloc+realloc 换成一个 \[\]](#1. list——万能动态数组,malloc+realloc 换成一个 [])
- [2. dict------哈希表,手写哈希函数 → {} 直接用](#2. dict——哈希表,手写哈希函数 → {} 直接用)
- [3. str------不可变但切片是杀手级武器](#3. str——不可变但切片是杀手级武器)
- [4. set------去重和 O(1) 查找,比 C 的 bool 数组更通用](#4. set——去重和 O(1) 查找,比 C 的 bool 数组更通用)
- [5. tuple------不可变轻量序列,pair<int,int> 的 Python 版](#5. tuple——不可变轻量序列,pair<int,int> 的 Python 版)
- [五、专用工具---deque / heapq / bisect,特定场景用对工具](#五、专用工具---deque / heapq / bisect,特定场景用对工具)
-
- [1. deque------双端队列,BFS 标配,list.pop(0) 的 O(1) 替代](#1. deque——双端队列,BFS 标配,list.pop(0) 的 O(1) 替代)
- [2. heapq------堆,TopK 三行搞定,但只有小顶堆](#2. heapq——堆,TopK 三行搞定,但只有小顶堆)
- [3. bisect------二分查找直接用,不用手写 left/right/mid 边界](#3. bisect——二分查找直接用,不用手写 left/right/mid 边界)
- [六、C → Python 避坑指南---最容易踩的 8 个坑](#六、C → Python 避坑指南---最容易踩的 8 个坑)
一、背景:为什么要从 C 切换到 Python 刷题
1. C vs Python 刷题差异总览---一张表看清省了什么、代价是什么
Python 刷题比 C 少写 3-5 倍代码,这个压缩率不是来自更短的语法,而是"不需要写的东西变少了"。下面这张表列出刷题场景下 C 和 Python 的差异------关注"省了什么"那一列。
| 特性 | C | Python | 省了什么 |
|---|---|---|---|
| 变量 | int a; 声明类型 |
a = 10 |
类型声明 |
| 语句结尾 | 必须 ; |
无 | 分号 |
| 代码块 | {} 大括号 |
缩进(4 空格) | 括号 |
| 编译 | 编译→链接→运行 | python3 a.py 直跑 |
编译步骤 |
| 内存 | malloc/free 手动管理 |
Python 自动回收不用管 | 忘掉 free() |
| 数据结构 | 手写哈希/堆/树 | dict/set/heapq/deque 内置 |
全部实现代码 |
| 代码量 | 100 行 | 15-30 行 | 3-5x 压缩 |
核心结论 :算法题 90% 的场景中,Python 的代价可忽略,省下的代码量是实打实的。考试上机写 Python,写完 C 选手还在调 realloc。
2. Python 刷题的核心哲学---代码是解法的直译,不是在"编程"
C 写算法是"指挥计算机怎么做"------分配内存、管理指针、写循环。Python 写算法是"描述解法本身"。
C: 想好解法 → 翻译成 C 的数据结构 → 手写辅助逻辑 → 排内存 bug → 得到答案
Python:想好解法 → 用内置容器描述 → 得到答案
中间两步被语言本身吃掉了。这就是代码量 3-5x 压缩的根源------不是语法更短,是少写了两个中间层。学习目标从"管理内存和指针"转变为"用内置工具拼装解法"。
3. 刷题需要的 Python 工具箱速览---一句话记住全景图
本文只覆盖以下内容,这些是算法题 Python 代码中 95% 出现的元素。不讲面向对象、不讲装饰器原理、不讲异步------刷题用不上。
text
基础语法层 list → dict → str → set → tuple ← 第四章 基础容器
专用工具层 deque → heapq → bisect ← 第五章 专用工具
辅助技能层 input() / print() / f-string ← 第三章 输入输出
"改造" C 程序员的习惯,不是学新语法,是忘记旧语法------忘掉
malloc、忘掉strcpy、忘掉手写哈希。Python 的世界里,这些东西根本不存在。
二、基础语法:C 程序员最关心的差异---数据类型、控制流、函数
只用对照表 + 最简代码示例,不给完整语法教程。目标:3 分钟能看懂 Python 代码。
1. 基础数据类型---int / float / bool / None,四种就够
变量无需声明,类型跟着值走 。Python 是动态类型 语言------变量只是名字,类型存在值身上。a = 10 之后 a 指向一个整数,a = "hello" 之后同一个 a 指向一个字符串。不需要 int a; 声明,也不需要关心这个变量"是什么类型"------拿来用就行。
| 类型 | 写法 | C 等价 | 值得注意的区别 |
|---|---|---|---|
| 整数 | a = 10 |
int a = 10; |
无限大 ,超出 32 位不会溢出;3/2 结果是 1.5(不是 1);整除用 // |
| 浮点 | b = 3.14 |
double b = 3.14; |
IEEE 754,和 C 行为一致 |
| 布尔 | t = True / f = False |
bool t = true; |
首字母大写 ,不是 true/false |
| 空值 | x = None |
NULL |
首字母大写,不是 NULL |
类型转换 :int("42") 字符串→整数,str(42) 整数→字符串,float("3.14") 字符串→浮点。不用记 atoi/itoa。
2. 数字运算---/ 总是浮点结果,// 才是整除,** 是乘方
数字运算的规则由参与运算的值决定 。Python 遇到 3/2,两个操作数都是整数,但 / 运算符保证返回浮点------结果类型由运算符决定,不是由操作数决定 。+ - * 则相反:整数 + 整数 = 整数,只要有一个浮点就返回浮点。动态类型的核心影响就这一个:你不声明类型,结果的类型由运算规则产生。
| 运算符 | 含义 | C 等价 | 注意 |
|---|---|---|---|
+ - * |
加减乘 | 完全一样 | --- |
/ |
除法 | --- | 结果总是浮点 ,3/2 = 1.5 |
// |
整除(地板除) | /(整数除法) |
3//2 = 1,-3//2 = -2(向下取整,不是向零) |
% |
取模 | % |
-7%3 = 2(结果符号同除数,C 是 -1) |
** |
乘方 | pow() |
2**10 = 1024 |
int 不溢出 :Python 整数可以任意大,2**100 算完直接打印,不需要 long long 也不需要手写大数。
五个内置函数速查 :abs(x) 绝对值,round(x) 四舍五入,pow(x, y) 等效 x**y,max(a, b) / min(a, b) 取大小------不需要 if (a > b)。
3. 控制流---and/or/not 替代 &&/||/!,缩进替代 {},for 只有 range 一种写法
Python 的控制流和 C 逻辑完全一样,只是写法不同 。三个核心变化:① &&/||/! 变成 and/or/not------读起来更像英语;② {} 变成缩进------代码块边界由缩进层级决定,不是大括号;③ for 循环只有 for i in range(n) 一种------没有 C 的三段式 for(;;)。
条件判断:
| 场景 | C | Python |
|---|---|---|
| 基本 if | if (a > 0) { ... } |
if a > 0: |
| 否则如果 | else if (a == 0) { ... } |
elif a == 0: |
| 否则 | else { ... } |
else: |
| 逻辑与 | (a > 0) && (b < 10) |
a > 0 and b < 10 |
| 逻辑或 | `(a > 0) | |
| 逻辑非 | !flag |
not flag |
循环:
| 场景 | C | Python | 说明 |
|---|---|---|---|
| 基础 for | for (int i=0; i<n; i++) |
for i in range(n): |
i 取 0 到 n-1 |
| 带步长 | for (int i=0; i<n; i+=2) |
for i in range(0, n, 2): |
步长放第三个参数 |
| 倒序 | for (int i=n-1; i>=0; i--) |
for i in range(n-1, -1, -1): |
倒序用负步长 |
| while | while (a > 0) { ... } |
while a > 0: |
去掉括号 |
| 中断/继续 | break / continue |
完全一样 | --- |
| do-while | do { ... } while (...) |
Python 没有 | 改用 while True: + if 条件: break |
一句话总结 :C 的 for (;;) 只有一种等价物------for i in range(n):。range(start, stop, step) 是 Python 最常用的内置函数之一,含义和 C 的 for 循环三要素一一对应。
4. 函数---不声明类型 + 多返回值
Python 函数用 def 定义,不声明参数类型和返回值类型 。和 C 的核心差异就两条------参数和返回值不写类型(靠约定),一个函数可以返回多个值(本质返回 tuple)。
| C | Python |
|---|---|
int add(int a, int b) { return a + b; } |
def add(a, b): return a + b |
C 程序员最需要适应的两点:
① 类型全靠约定:参数不写类型,返回值不写类型。代码短了,但 IDE 类型提示比 C 弱,名字起好是关键。
② 多返回值------一行解包:
python
def min_max(arr):
return min(arr), max(arr)
lo, hi = min_max(nums) # 同时接收两个返回值
C 要做到同样的事,要么传两个指针当出参,要么定义一个 struct MinMax 返回。Python 原生支持多返回值(本质返回 tuple),算法题中极高频------比如 BFS 同时返回深度和节点。
基础语法这一章,每节约一张对照表。合起来 10 张不到------覆盖了 C 程序员最需要知道的全部语法差异。背 C 语法花了几年,Python 只需要记住这几张表。
三、输入输出---极简但 ACM 模式要知道几招
1. C vs Python 输入输出对照---scanf/printf → input/print,一张表覆盖语法迁移
Python 的输入输出比 C 简单一个数量级------不记格式符、不取地址、不区分类型 。C 的 scanf 需要 %d/%f/%s 指定类型,printf 需要同样格式符写出。Python 只有两个函数:input() 负责读,print() 负责写,格式全部靠 split() 和 f-string 搞定。
| 序号 | 操作 | C | Python | 要点 |
|---|---|---|---|---|
| 1 | 头文件 | #include <stdio.h> |
无需(或 import sys) |
裸写 print("hello") 就能跑 |
| 2 | 输入整数 | scanf("%d", &a) |
a = int(input()) |
手动转类型 |
| 3 | 输入多个 | scanf("%d %f", &a, &b) |
a, b = map(int, input().split()) |
链式操作一行 |
| 4 | 输出 | printf("a=%d", a) |
print(f"a={a}") |
f-string 比 %d 直觉得多 |
| 5 | 不换行 | printf(".") |
print(".", end="") |
参数控制 |
| 6 | 性能 | 最快 | 大输入慢,换 sys.stdin |
99% 的题不卡常 |
2. 基础输入 input()---读一行字符串,剩下的自己拆
input() 和 C 的 scanf 有本质区别------它只做一件事:从标准输入读一行,返回字符串。不解析类型、不跳过空白、不停在空格。 要什么类型,拿到字符串之后自己 split() + int() 手动转。这比 scanf 麻烦一步,但换来的是完全的解析控制权------想按什么规则拆就按什么规则拆。
python
# 最高频写法------两个整数
a, b = map(int, input().split()) # 输入 "3 5" → a=3, b=5
# 读一个整数
n = int(input())
# 读一行整数到列表
arr = list(map(int, input().split())) # 输入 "1 2 3 4" → [1, 2, 3, 4]
# 读到 EOF
while True:
try:
line = input()
except EOFError:
break
三个要点 :split() 默认按空白符切割 / map(int, ...) 对每个元素执行 int() / 读不到数据时抛出 EOFError
3. 基础输出 print()---默认换行,用参数调行为
print() 和 C 的 printf 最大区别------不记格式符 。print() 把传入的所有参数用空格拼接,末尾自动加换行。想调格式?用 end 改换行、用 sep 改分隔符、用 f-string 做格式化------三个参数覆盖了 printf 全部功能且不用记 %d/%f/%s。
python
x = 42
a, b = 1, 2
print(x) # 输出: 42(然后换行)
print(x, end='') # 输出: 42(不换行,下个 print 接在后面)
print(a, b) # 输出: 1 2(默认空格分隔)
print(a, b, sep='|') # 输出: 1|2
print(f"a={a}, b={b}") # 输出: a=1, b=2
print(f"x/3 = {x/3:.2f}") # 输出: x/3 = 14.00(保留两位小数)
f-string 是 Python 的 printf :f"{变量:格式}"。{x:.2f} 保留两位小数,{x:10d} 定宽对齐。比 %d/%f 直观------变量直接写花括号里,不用记格式符顺序。
4. ACM 四种输入模式---读到 EOF / 先读组数 / 不定长 / 混合输入,四段模板复制即用
LeetCode 不用自己写输入------函数参数就是解析好的数据。但 ACM 模式/牛客网/公司笔试需要自己从标准输入读数据。四种模式覆盖了所有常见输入格式,直接复制模板改变量名就能用。
| 模式 | Python 模板 | 典型题目 |
|---|---|---|
| 读到 EOF | for line in sys.stdin: |
A+B 多组数据 |
| 先读组数 | for _ in range(int(input())): |
第一行给 N,后面 N 组 |
| 每行不定长 | nums = list(map(int, input().split())) |
每行数字个数不同 |
| 混合输入 | n, s = input().split(); n = int(n) |
一行里既有数字又有字符串 |
python
# 模板 1:读到 EOF(最常用)
# 输入示例:
# 1 2
# 3 4
# 5 6
# (没有结束标志,读到文件末尾就停)
import sys
for line in sys.stdin:
a, b = map(int, line.split())
print(a + b)
# 模板 2:先告知组数
# 输入示例:
# 3 ← 接下来有 3 组
# 1 2
# 3 4
# 5 6
for _ in range(int(input())):
a, b = map(int, input().split())
print(a + b)
# 模板 3:每行不定长
# 输入示例:
# 1 2 3
# 4 5
# 6 7 8 9
# (每行数字个数不固定,求每行和)
while True:
try:
nums = list(map(int, input().split()))
print(sum(nums))
except EOFError:
break
# 模板 4:混合输入(数字 + 字符串)
# 输入示例:
# 3 hello ← 重复 3 次 "hello"
# 输出:
# hellohellohello
n, s = input().split()
n = int(n)
for _ in range(n):
print(s, end='')
"ACM 输入的本质是字符串解析。Python 不需要格式符,只需要
split()+int()。C 程序员最不适应的是------没有scanf的自动类型推导,但换来的是无上限的灵活性。"
四、基础容器---list / dict / str / set / tuple,搞懂这五个 = 算法题不卡壳
算法题 Python 代码里 90% 的数据都是在摆弄这五个容器。每节一张 C vs Python 对比表 + 初始化/访问/常用操作 + 一段 blockquote 收尾。
1. list------万能动态数组,malloc+realloc 换成一个 \[\]
list 是 Python 的动态数组,自动扩容、可存任意类型、支持下标随机访问 。C 里数组要么定长要么 malloc,扩容要手写 realloc。Python 的 list 只管 append------撑满了自己扩,缩了自己收。元素类型也可以混着放:[1, "hello", 3.14] 合法。
| 特性 | C (malloc) | Python (list) | 优势 |
|---|---|---|---|
| 内存 | 手动分配释放 | 自动管理 | 忘掉 malloc/free |
| 大小 | 自己维护 len 变量 | len(arr) |
直接获取 |
| 扩容 | realloc 手动 |
.append() 自动 |
只管加,不管容量 |
| 操作 | 手写 for 循环 | 切片 + 列表推导 | 一行顶 C 十行 |
python
# 初始化
arr = [] # 空列表
arr = [0] * 10 # 10 个 0
arr = [1, 2, 3, 4, 5] # 字面量
arr = [i for i in range(10)] # 列表推导:[0,1,2,...,9]
arr = [x for x in arr if x > 0] # 带过滤:[2,4,6,...]
# 访问------负索引是 C 没有的
arr[0] # 第一个
arr[-1] # 最后一个(C 做不到)
arr[1:3] # 切片:[arr[1], arr[2]]
# 遍历
for x in arr: # 只取值
print(x)
for i, x in enumerate(arr): # 取值 + 索引
print(f"arr[{i}] = {x}")
# 常用操作
arr.append(6) # 尾部加 O(1)
arr.pop() # 尾部弹 O(1)
arr.pop(2) # 指定位置弹 O(n)
arr.sort() # 升序
arr.sort(key=lambda x: x[1]) # 按第二个元素排序
arr.sort(reverse=True) # 降序
arr.reverse() # 原地反转
arr.insert(2, 100) # 位置 2 插入 O(n)
arr.remove(100) # 按值删除 O(n)
列表推导式------C 程序员第一次见会愣住的语法。一个完整的 for 循环 + 条件判断 + 变换操作,压成一行:
python
# C: 取数组里所有偶数,平方,放到新数组
# 需要 malloc + for + if + 赋值,至少 6 行
# Python:
squares = [x*x for x in arr if x % 2 == 0]
二维列表------避坑:
python
# ✅ 正确写法------每行独立
dp = [[0] * n for _ in range(m)] # m 行 n 列,每行是独立的新 list
dp[0][0] = 1 # 只改第一行第一列,其余行不变
# ❌ 陷阱写法------三行指向同一个 list!
dp = [[0] * 3] * 3
# 等价于:
# row = [0, 0, 0]
# dp = [row, row, row] ← 三个元素都是 row 的引用!
dp[0][0] = 1 # dp 变成 [[1,0,0], [1,0,0], [1,0,0]]------三行全变了!
"C 里写动态数组:
mallocn 个 → 满了realloc2n → 复制 → 释放旧空间。Python 里写动态数组:arr.append(x)。"
2. dict------哈希表,手写哈希函数 → {} 直接用
dict 是 Python 内置的哈希表,键值对(key → value)映射,插入和查找全部 O(1) 。C 要么手写哈希函数加冲突链,要么用 uthash 这种第三方宏------不管哪种都是不小的工程。Python 直接用 {} 或者 dict(),背后是优化过的工业级哈希表实现。
| 特性 | C (手写) | Python dict | 优势 |
|---|---|---|---|
| 实现 | 手写哈希函数+冲突链 | 内置 {} |
无需造轮子 |
| 插入 | 手写冲突处理 | d[key] = val |
一行 |
| 查找 | 手写遍历 | d.get(key, 0) O(1) |
即时 |
| 遍历 | 手写 | for k, v in d.items(): |
一行 |
python
# 初始化
d = {} # 空字典
d = {"a": 1, "b": 2} # 字面量
# 增删查
d["c"] = 3 # 增
d["a"] = 100 # 改
del d["b"] # 删
if "a" in d: # 查是否存在
print("found")
val = d.get("z", 0) # 安全查:有则返回,无则返回 0
# 遍历
for k, v in d.items(): # 最常用
print(f"{k} -> {v}")
for k in d: # 只遍历 key
print(k)
dict 本身只有增删查,但 Python 提供了两个高频变体,算法题里比裸 dict 更常用。
进阶 1:Counter------一行频次统计:
python
from collections import Counter
arr = [1, 1, 2, 2, 2, 3]
cnt = Counter(arr) # {1: 2, 2: 3, 3: 1}
cnt.most_common(1) # [(2, 3)] 最高频 Top1
C 要统计数组频次------定义哈希表、初始化、遍历、插入或自增。Counter 一行搞定,后面直接当 dict 用。
进阶 2:defaultdict------省去"这个 key 存在吗"的判断:
python
from collections import defaultdict
groups = defaultdict(list) # 不存在的 key 自动创建空 list
for word in words:
groups[word[0]].append(word) # 不需要 if word[0] not in groups
"C 程序员面试最怕手写哈希表。Python 程序员从
d = {}开始解题。这就是差距。"
3. str------不可变但切片是杀手级武器
Python 字符串是一个不可变的字符序列 。它和 C 的 char[] 有本质区别------不是"字符的数组",而是"一个整体值"。
三个核心特征:①不可变 ------一旦创建就不能原地修改,s[0] = 'x' 会报错,任何修改都返回一个新字符串;②自动管理 ------不需要分配固定大小,不用管缓冲区;③自带长度 ------len(s) 是 O(1) 的直接取值,不是 strlen 的遍历。
| 特性 | C (char\[\]) | Python (str) | 优势 |
|---|---|---|---|
| 内存 | 手动分配固定大小 | 自动管理 | 无缓冲区溢出 |
| 长度 | strlen() 遍历 O(n) |
len(s) O(1) |
即时 |
| 赋值 | strcpy() |
s = "hello" |
直接 = |
| 拼接 | strcat() |
s1 + s2 |
运算符 |
| 比较 | strcmp() |
s1 == s2 |
像比数字 |
| 切片 | 不存在 | s[2:5] s[::-1] |
革命性 |
python
s = "Hello World"
# 切片------Python 字符串最强大的操作
s[0:5] # "Hello"------从 0 到 5(不含 5)
s[6:] # "World"------从 6 到末尾
s[:5] # "Hello"------从头到 5
s[::-1] # "dlroW olleH"------反转!C 需要 5 行 for 循环
# 高频方法
s.split() # ["Hello", "World"]------按空白分割
s.split(",") # 按逗号分割
" ".join(["a","b"]) # "a b"------用空格拼接列表
s.find("World") # 6------返回位置,找不到返回 -1
s.replace("Hello","Hi") # "Hi World"
s.startswith("He") # True
s.endswith("ld") # True
s.strip() # 去除前后空白
# 字符判断------回文/数字题常用
"123".isdigit() # True
"abc".isalpha() # True
"a1b2".isalnum() # True
注意 :str 是不可变对象。s[0] = 'h' 会报错 !要改只能构造新字符串:s = 'h' + s[1:]。
"C 程序员对字符串最深的恐惧是缓冲区溢出。Python 的解决方案------让你根本不知道什么叫缓冲区。"
4. set------去重和 O(1) 查找,比 C 的 bool 数组更通用
set 是一个无序、不重复的元素集合,背后是哈希表 。插入、删除、查找全部 O(1)。和 C 最大差异:C 要判断一个值出现过没有,要么遍历数组 O(n),要么用 bool 数组打标记------bool 数组要求你提前知道值的范围。set 不在乎值多大、什么类型。
python
s = {1, 2, 3} # 字面量初始化
s = set() # 空集合(注意 {} 是 dict 不是 set)
s.add(4) # 加 O(1)
s.remove(3) # 删(不存在则报错)
s.discard(3) # 安全删(不存在不报错)
2 in s # O(1) 查找------True
# 集合运算------运算符直接写
a = {1, 2, 3}
b = {2, 3, 4}
a & b # {2, 3} 交集
a | b # {1, 2, 3, 4} 并集
a - b # {1} 差集
# 一行判重
has_dup = len(set(arr)) < len(arr)
和 C 的关键差异 :C 判断元素存在要么遍历 O(n),要么用 bool 数组标记------bool 数组要求值域已知且连续。Python 的 set 不在乎值多大什么类型,x in s 永远 O(1)。
"C 的 bool 数组标记法要求你知道值的范围。Python 的 set 不在乎------无论值多大、什么类型,
x in s永远 O(1)。"
5. tuple------不可变轻量序列,pair<int,int> 的 Python 版
tuple 和 list 长得一样,唯一的区别------不可变 。创建之后不能增、不能删、不能改元素。这恰好让它能做 dict 的 key(list 不行------因为可变就不能哈希)。C 里最像它的是 pair<int,int>,但 tuple 可以有任意多个元素。
python
t = (1, 2) # 创建
t = 1, 2 # 括号可省略
a, b = t # 解包:a=1, b=2
a, b = b, a # 一行交换两个变量的值
# 为什么需要 tuple?
# → 不可变 = 可哈希 → 能做 dict 的 key,list 不行
d = {(1, 2): "point"} # ✅ tuple 做 key
d = {[1, 2]: "point"} # ❌ list 不能做 key,会报错
# 多返回值本质就是 tuple
def f():
return 1, 2, 3 # 等价于 return (1, 2, 3)
"tuple 存在的唯一理由------不可变。不可变 = 可哈希 = 能做 dict 的 key。list 做不到这一点。"
五、专用工具---deque / heapq / bisect,特定场景用对工具
基础容器搞定了 90% 的需求。剩下三种情况需要专用工具------队列、堆、二分。
1. deque------双端队列,BFS 标配,list.pop(0) 的 O(1) 替代
deque(double-ended queue)是一个双端队列,头尾插入删除都 O(1) 。list 也能当队列用,但 list.pop(0) 是 O(n)------所有元素要往前挪。BFS、滑动窗口这类场景必须用 deque,否则数据量大直接超时。
python
from collections import deque
q = deque()
# 队列操作(先进先出)------ BFS 标准写法
q.append(1) # 入队尾 O(1)
q.append(2)
q.popleft() # 出队首 O(1) → 1
# 栈操作------直接用 list 更简单
stack = []
stack.append(1) # 压栈
stack.pop() # 弹栈 → 1
核心提醒 :BFS 用队列时,千万不要用 list.pop(0) ------那是 O(n),数据量大的时候直接超时。deque.popleft() 才是 O(1)。
python
# BFS 层序遍历模板
def bfs(root):
q = deque([root])
while q:
node = q.popleft()
for child in node.children:
q.append(child)
"Python 不给 stack 和 queue 单独的类型,不是不重要,而是 list 和 deque 已经做得足够好。"
2. heapq------堆,TopK 三行搞定,但只有小顶堆
heapq 是一个基于 list 的堆实现,默认是最小堆 ------堆顶(heap[0])永远是最小元素。压入弹出均 O(log n),原地建堆 O(n)。Python 没有最大堆(C++ 的 priority_queue 默认大顶),需要大顶堆时------元素取个负就行。
python
import heapq
heap = []
heapq.heappush(heap, 3) # 压入
heapq.heappush(heap, 1)
heapq.heappush(heap, 2)
heapq.heappop(heap) # 弹出最小的 → 1
# 原地建堆 O(n)
arr = [3, 1, 4, 1, 5]
heapq.heapify(arr)
# TopK------一行取最小/最大 K 个
heapq.nsmallest(3, arr) # 最小的 3 个
heapq.nlargest(3, arr) # 最大的 3 个
大顶堆 ------Python 只有小顶堆,大顶堆 = 取负:
python
heapq.heappush(heap, -x) # 压入取负
val = -heapq.heappop(heap) # 弹出再取负 → 原来的最大值
"C 手写堆要维护数组、上浮、下沉。Python heapq 只记一条------默认是小顶堆,大顶堆取个负就行。"
3. bisect------二分查找直接用,不用手写 left/right/mid 边界
bisect 模块提供标准的二分查找------在已排序数组中查找插入位置,O(log n) 。二分查找思路不难,难的是 left/right/mid 边界处理(<= 还是 <、mid+1 还是 mid)。bisect 把边界问题全封在库内,你只负责传数组和目标值。
python
import bisect
arr = [1, 3, 5, 7, 9]
bisect.bisect_left(arr, 5) # 2 → 第一个 >= 5 的位置(lower_bound)
bisect.bisect_right(arr, 5) # 3 → 第一个 > 5 的位置(upper_bound)
# 在有序数组中插入并保持有序
bisect.insort_left(arr, 6) # arr → [1, 3, 5, 6, 7, 9]
"二分查找的难点从来不是思路,是边界。Python 说------'边界的事交给库,你只负责传数组和目标值'。"
六、C → Python 避坑指南---最容易踩的 8 个坑
| # | 坑 | C 习惯 | Python 实际行为 | 正确写法 |
|---|---|---|---|---|
| 1 | 字符串不可变 | s[0] = 'x' |
报错! str 不可原地改 |
s = 'x' + s[1:] |
| 2 | = 是引用 |
= 拷贝值 |
a = b 后改 a[0],b[0] 也变 |
a = b[:] 浅拷贝 |
| 3 | 默认参数陷阱 | --- | def f(arr=[]) 所有调用共享同一 list |
def f(arr=None): |
| 4 | for 内改循环变量 | for(i=0;i<n;i++) i=10 生效 |
for i in range(n): i=10 无效 |
改用 while |
| 5 | 负索引不报错 | 越界 = 段错误 | arr[-1] 静默取最后一个 |
检查索引范围 |
| 6 | 整数除法 | 3/2 = 1(截断) |
3/2 = 1.5(浮点)/ 3//2 = 1(整除) |
整数除用 // |
| 7 | * 复制二维列表 |
--- | [[0]*3]*3 三行指向同一 list |
[[0]*3 for _ in range(3)] |
| 8 | 循环中删元素 | --- | for x in arr: arr.remove(x) 跳元素 |
[x for x in arr if cond] |
"这些坑的共同特征------C 程序员的肌肉记忆在 Python 中不起作用,甚至反向作用。提前知道比 debug 时发现高效得多。"