看到下面这些切片时,很容易把它们混在一起:
1a[3:]
2a[:3]
3a[-3:]
4a[:-3]
不需要分别背四条规则。切片中的数字表示元素之间的切割位置,而不是要取出的元素下标。只要先画出切割线,所有常用切片都可以直接看出来。
核心图:数字写在元素之间
假设:
1a = [10, 20, 30, 40, 50]
元素的下标写在元素下面,切片位置写在元素之间:
1切片位置: 0 1 2 3 4 5
2 | 10 | 20 | 30 | 40 | 50 |
3元素下标: 0 1 2 3 4
这里一共有 5 个元素,却有 6 个切片位置。
- 位置
0在第一个元素之前; - 位置
1在第一个元素之后; - 位置
3在前三个元素之后; - 位置
5在所有元素之后。
因此,位置 k 左边恰好有 k 个元素。这就是快速理解 a[3:] 的关键。
a[left:right] 表示在位置 left 和 right 各切一刀,保留两刀之间的元素。
a[3:] 为什么表示跳过前三个
在位置 3 切一刀:
1切片位置: 0 1 2 3 4 5
2 | 10 | 20 | 30 | 40 | 50 |
3 ^
4 从这里开始保留
位置 3 左边有三个元素:10、20、30。冒号右边省略,表示一直保留到末尾:
1a = [10, 20, 30, 40, 50]
2
3assert a[3:] == [40, 50]
所以可以把 a[k:] 直接读成:
跳过前
k个元素,保留剩余部分。
注意,更准确的说法是“得到从位置 k 开始的新序列”,并不是原列表已经被删除了。
a[:3] 为什么表示保留前三个
这次在位置 3 结束:
1切片位置: 0 1 2 3 4 5
2 | 10 | 20 | 30 | 40 | 50 |
3 ^
4 在这里停止
冒号左边省略,表示从开头开始。位置 3 左边恰好有三个元素:
1a = [10, 20, 30, 40, 50]
2
3assert a[:3] == [10, 20, 30]
所以 a[:k] 可以读成:
保留前
k个元素。
两刀之间:a[left:right]
例如 a[1:4]:
1切片位置: 0 1 2 3 4 5
2 | 10 | 20 | 30 | 40 | 50 |
3 ^ ^
4 left=1 right=4
保留两刀之间的 20、30、40:
1a = [10, 20, 30, 40, 50]
2
3assert a[1:4] == [20, 30, 40]
换成下标语言,就是保留下标:
11, 2, 3
也就是左端 left 包含,右端 right 不包含,通常写成区间 。
切片长度也能直接从位置得到:
例如 a[1:4] 的长度是 4 - 1 = 3。
从右边数位置
负数表示从右边寻找切割位置。先只记住一句话:
-k表示最后k个元素之前的位置。
对于同一个列表:
1 最后 3 个元素
2 <-------------->
3 | 10 | 20 | 30 | 40 | 50 |
4 ^
5 -3
a[-3:]:保留最后三个
从最后三个元素之前切开,然后保留右边:
1a = [10, 20, 30, 40, 50]
2
3assert a[-3:] == [30, 40, 50]
a[:-3]:去掉最后三个
仍然在同一位置切开,这次保留左边:
1a = [10, 20, 30, 40, 50]
2
3assert a[:-3] == [10, 20]
因此,正着数和倒着数其实是同一套规则:先确定切割位置,再看冒号哪一侧被省略。
常用写法速查
下面的 k 默认是正整数:
| 写法 | 切割方法 | 结果 |
|---|---|---|
a[k:] | 在前 k 个元素之后切开,取右边 | 跳过前 k 个 |
a[:k] | 在前 k 个元素之后切开,取左边 | 保留前 k 个 |
a[-k:] | 在最后 k 个元素之前切开,取右边 | 保留最后 k 个 |
a[:-k] | 在最后 k 个元素之前切开,取左边 | 去掉最后 k 个 |
a[left:right] | 在两个位置切开,取中间 | 保留 |
a[:] | 从开头取到末尾 | 浅拷贝整个序列 |
把表格压缩成一句话:
k写在冒号左边,就从k开始;写在冒号右边,就在k停止。
切片不会自动修改原列表
1a = [10, 20, 30, 40, 50]
2b = a[3:]
3
4assert a == [10, 20, 30, 40, 50]
5assert b == [40, 50]
a[3:] 创建了一个新列表,原来的 a 没有变化。因此,“a[3:] 删除前三个”只是方便记忆的简称。
如果确实要让变量只保留后面的元素,可以重新赋值:
1a = [10, 20, 30, 40, 50]
2a = a[3:]
3
4assert a == [40, 50]
如果要原地删除列表的前三个元素,使用 del:
1a = [10, 20, 30, 40, 50]
2del a[:3]
3
4assert a == [40, 50]
两者的区别是:a = a[3:] 创建新列表并让变量 a 指向它;del a[:3] 修改原来的列表对象。
OJ 中的常用场景
取前缀和后缀
1a = [4, 8, 15, 16, 23, 42]
2k = 3
3
4prefix = a[:k]
5suffix = a[-k:]
6
7assert prefix == [4, 8, 15]
8assert suffix == [16, 23, 42]
取半开区间
题目给出左端点 left 和右端点 right,并且区间定义为 :
1a = [4, 8, 15, 16, 23, 42]
2left, right = 2, 5
3
4segment = a[left:right]
5
6assert segment == [15, 16, 23]
7assert len(segment) == right - left
Python 切片与前缀和常用的半开区间完全一致:
1from itertools import accumulate
2
3a = [4, 8, 15, 16, 23, 42]
4prefix = list(accumulate(a, initial=0))
5
6left, right = 2, 5
7assert sum(a[left:right]) == prefix[right] - prefix[left]
枚举相邻元素
a[1:] 跳过第一个元素,所以它与原列表错开一位:
1a = [4, 8, 15, 16]
2pairs = list(zip(a, a[1:]))
3
4assert pairs == [(4, 8), (8, 15), (15, 16)]
保存回溯路径的快照
回溯中的 path 会不断修改。存答案时使用 path[:] 创建当前列表的浅拷贝:
1answers = []
2path = [1, 3, 5]
3
4answers.append(path[:])
5path.pop()
6
7assert answers == [[1, 3, 5]]
8assert path == [1, 3]
如果直接 answers.append(path),答案和搜索过程会指向同一个列表,后续 pop() 也会改变已经保存的内容。
去掉首尾元素
在位置 1 开始,在最后一个元素之前停止:
1a = [10, 20, 30, 40, 50]
2
3assert a[1:-1] == [20, 30, 40]
这个写法常用于去掉括号、边界哨兵或字符串两端字符。
k = 0 的陷阱
公式 a[-k:] 和 a[:-k] 在 k > 0 时很好用,但 k = 0 需要单独考虑,因为 Python 没有独立的“负零”:
1a = [10, 20, 30]
2k = 0
3
4assert -k == 0
5assert a[-k:] == a[0:] == [10, 20, 30]
6assert a[:-k] == a[:0] == []
如果“取最后 k 个”在 k = 0 时应该得到空列表,可以写:
1last_k = a[-k:] if k > 0 else []
如果“去掉最后 k 个”在 k = 0 时应该保留整个列表,可以写:
1without_last_k = a[:-k] if k > 0 else a[:]
切片的时间和空间
列表切片会创建一个新列表。若结果中有 k 个元素,通常需要:
- 时间复制元素引用;
- 额外空间保存新列表。
例如:
1segment = a[left:right]
适合代码清晰、小数据验证或确实需要新列表的场景。但在大规模双重循环中反复切片,可能产生不必要的复制:
1# 每次切片都会创建新列表
2for left in range(len(a)):
3 for right in range(left + 1, len(a) + 1):
4 segment = a[left:right]
如果只需要区间边界或区间和,可以保留 left、right,或者使用前缀和,不必真正复制子数组。
字符串和元组的切片也会产生新的序列,但结果类型保持不变:
1assert "python"[2:] == "thon"
2assert (10, 20, 30)[:2] == (10, 20)
快速判断步骤
看到一个切片时,按下面三步判断:
- 把数字放到元素之间,而不是元素上面;
- 冒号左边决定从哪一刀开始;
- 冒号右边决定在哪一刀停止。
例如 a[2:5]:从位置 2 开始,在位置 5 停止,所以取得下标 2、3、4。
反过来,想写切片时也先问“我要保留哪一段”:
- 跳过前
k个:在位置k切开,取右边,写a[k:]; - 保留前
k个:在位置k切开,取左边,写a[:k]; - 保留最后
k个:找到最后k个之前的位置,取右边,写a[-k:]。
快速练习
固定:
1a = [10, 20, 30, 40, 50]
先不要运行代码,直接说出结果:
a[2:]a[:2]a[1:4]a[-2:]a[:-2]a[1:-1]a[3:3]a[:]
查看答案
1assert a[2:] == [30, 40, 50]
2assert a[:2] == [10, 20]
3assert a[1:4] == [20, 30, 40]
4assert a[-2:] == [40, 50]
5assert a[:-2] == [10, 20, 30]
6assert a[1:-1] == [20, 30, 40]
7assert a[3:3] == []
8assert a[:] == [10, 20, 30, 40, 50]
a[3:3] 的两刀在同一个位置,中间没有元素,因此结果为空。
总结
不要把切片记成许多独立公式,只记住一张图:
1切片位置: 0 1 2 3 4 5
2 | 10 | 20 | 30 | 40 | 50 |
3元素下标: 0 1 2 3 4
然后记住:
a[left:right]在两个位置切开,保留中间;左边开始,右边停止。
这样就能自然推出:
1a[k:] # 跳过前 k 个
2a[:k] # 保留前 k 个
3a[-k:] # 保留最后 k 个,k > 0
4a[:-k] # 去掉最后 k 个,k > 0
5a[left:right] # 保留半开区间 [left, right)
切片枚举区间的完整例子见用 Python 快速编写算法暴力验证程序。回溯路径浅拷贝的陷阱见C++ 选手转 Python 竞赛的 4 个血泪坑点。