Python 切片位置记忆法:把下标看成切割线

2026-09-07 00:00    #Python   #算法竞赛   #切片  

看到下面这些切片时,很容易把它们混在一起:

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 个切片位置。

因此,位置 k 左边恰好有 k 个元素。这就是快速理解 a[3:] 的关键。

核心口诀

a[left:right] 表示在位置 leftright 各切一刀,保留两刀之间的元素。

a[3:] 为什么表示跳过前三个

在位置 3 切一刀:

1切片位置: 0      1      2      3      4      5
2           |  10  |  20  |  30  |  40  |  50  |
3                                  ^
4                                从这里开始保留

位置 3 左边有三个元素:102030。冒号右边省略,表示一直保留到末尾:

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

保留两刀之间的 203040

1a = [10, 20, 30, 40, 50]
2
3assert a[1:4] == [20, 30, 40]

换成下标语言,就是保留下标:

11, 2, 3

也就是左端 left 包含,右端 right 不包含,通常写成区间 [left,right)[left,right)

切片长度也能直接从位置得到:

len(a[left:right])=rightleft \text{len}(a[left:right]) = right-left

例如 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]在两个位置切开,取中间保留 [left,right)[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,并且区间定义为 [left,right)[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]

如果只需要区间边界或区间和,可以保留 leftright,或者使用前缀和,不必真正复制子数组。

字符串和元组的切片也会产生新的序列,但结果类型保持不变:

1assert "python"[2:] == "thon"
2assert (10, 20, 30)[:2] == (10, 20)

快速判断步骤

看到一个切片时,按下面三步判断:

  1. 把数字放到元素之间,而不是元素上面;
  2. 冒号左边决定从哪一刀开始;
  3. 冒号右边决定在哪一刀停止。

例如 a[2:5]:从位置 2 开始,在位置 5 停止,所以取得下标 234

反过来,想写切片时也先问“我要保留哪一段”:

快速练习

固定:

1a = [10, 20, 30, 40, 50]

先不要运行代码,直接说出结果:

  1. a[2:]
  2. a[:2]
  3. a[1:4]
  4. a[-2:]
  5. a[:-2]
  6. a[1:-1]
  7. a[3:3]
  8. 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 个血泪坑点

参考资料