一、位运算:直接和二进制打交道

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.sort()

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)

复杂度:

操作

复杂度

heapify

O(N)

heappush

O(log N)

heappop

O(log N)

查看堆顶 heap[0]

O(1)


六、用负数模拟最大堆

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 常见语法。

知识点

本质

& 1

查看二进制最低位

>> 1

二进制右移,近似除以 2

range()

惰性生成,节省内存

try...except

异常路径保护

sort() / sorted()

原地修改 vs 创建新列表

heapq

用数组模拟完全二叉树

*args / **kwargs

参数自动打包

列表推导式

先筛选,再加工

[0] * N

快速复制引用

zip()

惰性配对

学这些知识,不只是为了记 API,而是为了看懂代码背后的运行方式:

数据有没有复制?
对象是不是共享?
操作是惰性的还是立即执行的?
时间和空间分别花在哪里?

这才是写出稳定、高效代码的关键。