一、位运算:直接和二进制打交道
1. 用 & 1 判断奇偶
奇偶性只取决于二进制最低位。
if x & 1:
print("奇数")
else:
print("偶数")
最低位是 1,说明是奇数;最低位是 0,说明是偶数。
这比 x % 2 更贴近底层二进制逻辑。
2. 用 >> 1 理解除以 2
对于非负整数:
x >> 1
大致等价于:
x // 2
因为二进制右移一位,相当于所有位的权重减半。
例如:
12 = 1100
12 >> 1 = 0110 = 6
在二分查找里经常看到:
mid = (left + right) >> 1
不过 Python 里写成下面这样通常更直观:
mid = (left + right) // 2
二、range():看起来很大,其实很省内存
很多人看到:
for i in range(100000000):
pass
会担心它是不是一次性生成一亿个数字。
其实不会。
Python 3 里的 range() 是惰性的。它并不会提前创建完整列表,而是保存:
起点
终点
步长
所以它更像一个“数字生成规则”,不是一个真正装满数字的容器。
这也是为什么 range() 在循环里非常常用:它空间开销很低。
三、try...except:程序的安全气囊
写算法时,我们经常追求速度,但代码也要能安全处理边界情况。
例如:
try:
result = 10 / x
except ZeroDivisionError:
result = None
try...except 就像程序的安全气囊:
正常情况下不触发,一旦遇到异常,可以防止整个程序直接崩掉。
不过不要随便写:
try:
...
except:
pass
这会把真正的 bug 也吞掉,后期很难排查。
更好的原则是:只捕获你明确知道怎么处理的异常。
四、sort() 和 sorted():原地修改还是创建新列表?
Python 排序常用两种方式:
nums.sort()
和:
new_nums = sorted(nums)
区别如下:
示例:
nums = [3, 1, 2]
nums.sort()
print(nums) # [1, 2, 3]
而:
nums = [3, 1, 2]
new_nums = sorted(nums)
print(nums) # [3, 1, 2]
print(new_nums) # [1, 2, 3]
总结:
想省内存,且允许修改原列表:用
sort()想保留原列表:用
sorted()
排序的常见时间复杂度是:
O(N log N)
这是比较排序里非常重要的复杂度级别。
五、heapq:用普通列表实现堆
Python 的堆模块叫:
import heapq
它不在 collections 里。
Python 也没有单独的 Heap 类型,heapq 是直接操作普通列表。
import heapq
nums = [5, 3, 8, 1, 2]
heapq.heapify(nums)
print(nums[0]) # 最小值
堆在逻辑上是一棵完全二叉树,但物理上存储在列表里。
对于下标 i:
左子节点:2i + 1
右子节点:2i + 2
父节点:(i - 1) // 2
常用操作:
heapq.heappush(heap, x)
heapq.heappop(heap)
复杂度:
六、用负数模拟最大堆
Python 的 heapq 默认是最小堆。
如果想要最大堆,可以把数字全部取反:
import heapq
nums = [5, 3, 8, 1, 2]
heap = [-x for x in nums]
heapq.heapify(heap)
max_value = -heapq.heappop(heap)
print(max_value) # 8
原来的最大值,取负后会变成最小值。
所以最小堆弹出的负数,再取反,就得到了最大值。
七、*args 和 **kwargs:函数参数分拣系统
*args 用来收集多余的位置参数。
def func(*args):
print(args)
func(1, 2, 3)
输出:
(1, 2, 3)
args 是一个元组。
**kwargs 用来收集多余的关键字参数。
def func(**kwargs):
print(kwargs)
func(name="Tom", age=18)
输出:
{"name": "Tom", "age": 18}
kwargs 是一个字典。
常见函数参数顺序:
def func(a, b, *args, **kwargs):
pass
可以理解为:
普通参数 -> *args -> **kwargs
因为 Python 需要从左到右分拣参数。**kwargs 像一个关键字参数黑洞,所以必须放在最后。
八、列表推导式:先筛选,再加工
列表推导式:
[x * x for x in nums if x % 2 == 0]
它的执行逻辑是:
先遍历
再判断 if
最后加工表达式
等价于:
result = []
for x in nums:
if x % 2 == 0:
result.append(x * x)
所以可以记成:
先 filter,后 process
这个顺序很重要,因为 if 可以充当安全护栏。
例如:
nums = [2, 1, 0, -1]
result = [10 / x for x in nums if x != 0]
这里 if x != 0 会先过滤掉 0,避免除零错误。
九、[0] * N:快,但要小心浅拷贝
初始化一维列表:
arr = [0] * n
这是非常常见且高效的写法。
因为整数是不可变对象,所以这样写通常没有问题。
但是二维列表不能这样写:
matrix = [[0] * 3] * 3
因为这会复制同一个内部列表的引用。
matrix = [[0] * 3] * 3
matrix[0][0] = 1
print(matrix)
结果是:
[
[1, 0, 0],
[1, 0, 0],
[1, 0, 0]
]
三行其实指向同一个列表。
正确写法是:
matrix = [[0] * n for _ in range(m)]
这样每一行都是新创建的独立列表。
十、zip():惰性拉链
zip() 可以把多个序列按位置配对。
names = ["Tom", "Jerry", "Spike"]
ages = [18, 20, 22]
print(list(zip(names, ages)))
输出:
[("Tom", 18), ("Jerry", 20), ("Spike", 22)]
如果长度不同,zip() 会以最短的为准:
a = [1, 2, 3]
b = ["x", "y"]
print(list(zip(a, b)))
输出:
[(1, "x"), (2, "y")]
常见用法:
keys = ["name", "age"]
values = ["Tom", 18]
info = dict(zip(keys, values))
zip() 本身是惰性的,额外空间很小。
但最终创建出来的 dict 仍然需要 O(N) 空间。
总结
这一篇的核心,是用底层思维理解 Python 常见语法。
学这些知识,不只是为了记 API,而是为了看懂代码背后的运行方式:
数据有没有复制?
对象是不是共享?
操作是惰性的还是立即执行的?
时间和空间分别花在哪里?
这才是写出稳定、高效代码的关键。
Python 算法实战中的底层思维:从位运算到 heapq、zip 和列表陷阱
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法