第 4 章 字符串¶
配套例题:PIO10 单组_字符串、PIO11 多组_字符串_T组形式、PIO12 单组_二维字符数组、PIO13 多组_带空格的字符串_T组形式、BISHI10 小红的字符串修改 来源:菜鸟教程 Python3 字符串
字符串是笔试里出现频率最高的数据类型——输入是字符串、输出是字符串、一半的题面本身就在讲字符串。
从 C++ 转过来要先记住一条:Python 的 str 是不可变的。
它不是 std::string(可以 s[0] = 'x'),而更像 Java 的 String。
这一条决定了本章后半段几乎所有的性能讨论。
4.1 字面量与转义¶
四种引号¶
Python 没有字符类型。'a' 就是一个长度为 1 的字符串,s[0] 拿到的也是长度为 1 的字符串。
这跟 C++ 的 char 是完全不同的东西——见 4.7 的 ord/chr。
选单引号还是双引号的唯一标准是哪个能少写转义:
转义字符表¶
| 转义 | 含义 | 竞赛中的用处 |
|---|---|---|
\n |
换行 | "\n".join(...) 拼接输出 |
\t |
制表符 | 少数题目要求 Tab 分隔 |
\\ |
反斜杠本身 | 路径、正则 |
\' \" |
引号 | 引号冲突时 |
\r |
回车 | Windows 换行残留,见下面的坑 |
\0 |
空字符 | 几乎用不到 |
\xhh |
十六进制字符 | "\x41" 就是 "A" |
\ooo |
八进制字符 | "\101" 就是 "A" |
行尾 \ |
续行 | 见 01-语法与执行模型 |
\r是 OJ 上最阴的 WA 来源。如果测试数据是 Windows 格式(\r\n),input()只去掉\n,留下的\r会跟着答案一起输出,肉眼看不见但评测机看得见。 读进来的字符串一律.strip()或.rstrip(),代价可以忽略。
原始字符串 r"..."¶
前缀 r 让反斜杠失去转义能力:
竞赛里基本只在写正则时用得上(re.compile(r"\d+"))。
注意 r"..." 不能以奇数个反斜杠结尾,r"\" 是语法错误。
字符串拼接的字面量特性¶
相邻的字符串字面量会在编译期自动拼接,这是写长提示串的惯用法:
注意这只对字面量成立,变量之间必须用 +。
4.2 索引与切片¶
索引与 C++ 一样从 0 开始,但 Python 额外支持负索引(从末尾数,-1 是最后一个):
s = "Python"
s[0] # 'P'
s[-1] # 'n' ← 取最后一个字符,不用写 s[len(s)-1]
s[6] # IndexError: string index out of range
切片 s[start:stop:step]¶
切片是 Python 最重要的语法之一,永远左闭右开,且越界不报错:
s = "abcdefg"
s[1:4] # 'bcd' [1, 4)
s[:3] # 'abc' 省略 start = 0
s[3:] # 'defg' 省略 stop = len(s)
s[:] # 'abcdefg' 完整副本
s[-3:] # 'efg' 最后三个
s[:-3] # 'abcd' 去掉最后三个
s[::2] # 'aceg' 步长 2
s[::-1] # 'gfedcba' ← 反转字符串的标准写法
s[100:200] # '' 越界不报错,返回空串
| 需求 | 写法 |
|---|---|
| 反转 | s[::-1] |
| 去掉首尾各一个字符 | s[1:-1] |
| 前 \(k\) 个 | s[:k] |
| 后 \(k\) 个 | s[-k:](注意 \(k=0\) 时会返回整串!) |
| 每隔一个取 | s[::2] |
| 判断回文 | s == s[::-1] |
s[-k:]在k == 0时是s[0:],也就是整个串,不是空串。 需要「取最后 k 个,k 可能为 0」时写s[len(s) - k:]。
切片的复杂度是 \(O(\text{切片长度})\),因为它复制。
在循环里反复 s[i:j] 是隐蔽的 \(O(n^2)\):
# ❌ 每次切片都复制,总复杂度 O(n²)
for i in range(n):
if s[i:i + m] == t:
...
# ✅ 用 find / 或者哈希,见第 36、71 章
pos = s.find(t)
4.3 字符串是不可变的¶
要「修改」只能造新串:
需要频繁按下标改字符时,先
a = list(s)变成字符列表,改完再"".join(a)。 这是竞赛里处理字符串修改的标准套路:list可变、\(O(1)\) 随机写,join一次 \(O(n)\) 收尾。
循环拼接是 \(O(n^2)\) 陷阱¶
这是 Python 算法题的第二大 TLE 来源(第一是 in list,见 03-运算符与位运算):
第 \(i\) 次拼接复制 \(i\) 个字符,总代价 \(1 + 2 + \dots + n = O(n^2)\)。 \(n = 10^5\) 时是 \(5 \times 10^9\) 次字符复制,必然 TLE。
正确写法是先收集到列表,最后 join 一次:
# ✅ O(总长度)
parts = []
for x in a:
parts.append(str(x))
res = " ".join(parts)
# ✅ 更短
res = " ".join(map(str, a))
join 会先遍历一遍算出总长度,一次性分配内存,然后逐段拷贝,总复杂度 \(O(\sum |s_i|)\)。
有人会说「CPython 对
s += t做了原地扩容优化,实测不慢」——这是真的, 但只在s的引用计数为 1 时生效。一旦把中间结果存进列表、传给函数、或者换成 PyPy, 优化立刻失效,退化回 \(O(n^2)\)。这种「看运气」的性能不能依赖。记住结论:任何时候拼接字符串都用
join,没有例外。
同理,输出大量行时不要在循环里 print:
4.4 字符串方法全表¶
Python 的 str 有 40 多个方法,全部返回新串,绝不修改原串。
按用途分成六类,竞赛高频的用 ★ 标出。
查找与定位¶
| 方法 | 作用 | 找不到时 |
|---|---|---|
★ s.find(t[, b, e]) |
返回 t 首次出现的下标 |
返回 -1 |
s.rfind(t) |
从右找 | 返回 -1 |
s.index(t) |
同 find |
抛 ValueError |
s.rindex(t) |
同 rfind |
抛 ValueError |
★ s.count(t) |
t 出现次数(不重叠) |
返回 0 |
★ t in s |
是否包含 | 返回 False |
★ s.startswith(t) |
是否以 t 开头,参数可以是元组 |
|
★ s.endswith(t) |
是否以 t 结尾,参数可以是元组 |
s = "abcabc"
s.find("bc") # 1
s.rfind("bc") # 4
s.find("bc", 2) # 4 从下标 2 开始找
s.count("abc") # 2
"aaaa".count("aa") # 2 ← 不重叠!不是 3
s.startswith(("ab", "xy")) # True 参数可以是元组,等于「或」
count统计的是不重叠出现次数。求重叠出现次数要自己写循环或用 KMP, 见 71-字符串匹配KMP。
判定(全是 is*,返回 bool)¶
| 方法 | 含义 | 空串时 |
|---|---|---|
★ s.isdigit() |
全是数字字符(含 ² 这类上标) |
False |
s.isdecimal() |
全是十进制数字(最严格) | False |
s.isnumeric() |
全是数值字符(含 ½、汉字「一」) |
False |
★ s.isalpha() |
全是字母(含汉字) | False |
★ s.isalnum() |
全是字母或数字 | False |
s.islower() / s.isupper() |
全小写 / 全大写 | False |
s.isspace() |
全是空白 | False |
s.istitle() |
是否每个单词首字母大写 | False |
s.isascii() |
全是 ASCII(3.7+) | True |
"²".isdigit() # True ← 上标 2 也算 digit
"²".isdecimal() # False ← 但不是 decimal
"½".isnumeric() # True
"一".isnumeric() # True ← 汉字数字
"-5".isdigit() # False ← 负号不是数字!
"-5".isdigit()是False。想判断「这个 token 是不是合法整数」, 老老实实try: int(s) except ValueError:,不要用isdigit。 另外要严格判十进制阿拉伯数字请用isdecimal,isdigit会放过全角/上标字符。
大小写转换¶
| 方法 | 作用 |
|---|---|
★ s.lower() / s.upper() |
全转小写 / 大写 |
s.capitalize() |
首字母大写,其余小写 |
s.title() |
每个单词首字母大写 |
s.swapcase() |
大小写互换 |
s.casefold() |
比 lower() 更激进的折叠(处理德语 ß 等) |
大小写不敏感的比较统一写 a.lower() == b.lower()。
修剪与填充¶
| 方法 | 作用 |
|---|---|
★ s.strip([chars]) |
去掉首尾指定字符集(默认空白) |
★ s.lstrip() / s.rstrip() |
只去左 / 只去右 |
★ s.zfill(w) |
左补 0 到宽度 w,正确处理负号 |
s.ljust(w[, c]) / s.rjust(w[, c]) |
左 / 右对齐填充 |
s.center(w[, c]) |
居中填充 |
s.expandtabs(n) |
Tab 展开成空格 |
s.removeprefix(p) / s.removesuffix(p) |
去掉前缀 / 后缀(3.9+) |
" hi ".strip() # 'hi'
"xxhixx".strip("x") # 'hi' ← 参数是字符集合,不是子串
"ab".zfill(5) # '000ab'
"-12".zfill(6) # '-00012' ← 负号留在最前面
"abc".center(9, "*") # '***abc***'
strip("abc")去掉的是「首尾所有属于 {a,b,c} 的字符」,不是去掉子串"abc"。 想去前缀用removeprefix(3.9+)或切片。这是极高频的误用。
分割与拼接¶
| 方法 | 作用 |
|---|---|
★ s.split() |
按任意空白切分,自动忽略连续空白和首尾空白 |
★ s.split(sep[, maxsplit]) |
按 sep 切分,连续 sep 会产生空串 |
s.rsplit(sep, k) |
从右切,最多切 k 次 |
s.splitlines() |
按行切分(认 \n \r \r\n) |
s.partition(sep) |
切成三元组 (前, sep, 后),只切第一次 |
s.rpartition(sep) |
同上,从右切 |
★ sep.join(iterable) |
用 sep 连接可迭代对象里的字符串 |
split() 和 split(' ') 的差异是竞赛输入解析的高频坑:
"a b c".split() # ['a', 'b', 'c'] ← 连续空格合并
"a b c".split(" ") # ['a', 'b', '', 'c'] ← 多出一个空串!
"a,b,,c".split(",") # ['a', 'b', '', 'c']
读输入永远用无参数的 split(),它同时处理空格、Tab、换行,且天然忽略首尾空白。
"-".join(["a", "b", "c"]) # 'a-b-c'
"".join(map(str, [1, 2, 3])) # '123'
"".join([1, 2]) # ❌ TypeError: 元素必须是 str,先 map(str, ...)
替换与映射¶
| 方法 | 作用 |
|---|---|
★ s.replace(old, new[, cnt]) |
替换,cnt 限制次数 |
str.maketrans(a, b[, del]) |
造字符映射表 |
s.translate(table) |
按映射表批量替换单字符 |
"abcabc".replace("a", "X") # 'XbcXbc'
"abcabc".replace("a", "X", 1) # 'Xbcabc'
"a b c".replace(" ", "") # 'abc' ← 删空格的标准写法
tab = str.maketrans("abc", "xyz")
"aabbcc".translate(tab) # 'xxyyzz'
一次替换多种字符时用
translate,不要链式replace。s.replace('a','x').replace('b','y').replace('c','z')要扫 3 遍串,translate只扫 1 遍,且是 C 实现。凯撒密码、字符映射类题目直接用它。
复杂度速查¶
| 操作 | 复杂度 | 备注 |
|---|---|---|
len(s) |
\(O(1)\) | 长度是存好的 |
s[i] |
\(O(1)\) | |
s[i:j] |
\(O(j-i)\) | 复制 |
s + t |
\(O(\|s\|+\|t\|)\) | 循环里用就是 \(O(n^2)\) |
s * k |
\(O(k\|s\|)\) | |
t in s、s.find(t) |
最坏 \(O(\|s\|\|t\|)\),实测接近线性 | CPython 用了 two-way 算法 |
s.count(t) |
同上 | |
s.split() |
\(O(\|s\|)\) | |
sep.join(a) |
\(O(\sum \|a_i\|)\) | |
s[::-1] |
\(O(\|s\|)\) | 比 "".join(reversed(s)) 快 |
s.replace / translate |
\(O(\|s\|)\) | |
s == t |
\(O(\min(\|s\|,\|t\|))\) | 长度不等直接 False,\(O(1)\) |
hash(s) |
\(O(\|s\|)\) 首次,之后 \(O(1)\) | 结果会缓存在字符串对象里 |
ord(c) / chr(i) |
\(O(1)\) |
4.5 三种格式化¶
Python 有三套字符串格式化语法,都还活着,都要认识。
| 写法 | 语法 | 何时用 |
|---|---|---|
% 旧式 |
"%d" % x |
只输出一个浮点数时最短 |
str.format |
"{}".format(x) |
需要重复使用同一参数时 |
| f-string | f"{x}" |
默认选择,最快最短(3.6+) |
一、% 格式化¶
"%d" % 42 # '42'
"我叫 %s 今年 %d 岁" % ("小明", 10) # 多个参数必须用元组
"%.3f" % 3.14159 # '3.142'
"%5.2f|%-5d|%+d" % (3.14159, 42, 7) # ' 3.14|42 |+7'
"%o %x %X %e" % (64, 255, 255, 12345.7) # '100 ff FF 1.234570e+04'
"%%" # 输出一个百分号
| 占位符 | 含义 |
|---|---|
%d %i |
整数 |
%s |
任意对象(调用 str()) |
%f |
浮点,默认 6 位小数 |
%e %E |
科学计数法 |
%g %G |
自动在 %f 和 %e 之间选 |
%o %x %X |
八 / 十六进制 |
%c |
按 ASCII 码或单字符 |
辅助指令:%[标志][宽度][.精度]类型,标志有 -(左对齐)、+(显示正号)、0(补零)、#(加 0x/0o 前缀)。
"%d" % x里x如果是元组会被当成多个参数, 想输出一个元组必须写"%s" % (t,)。这是%格式化最容易踩的坑,也是 f-string 胜出的原因之一。
二、str.format¶
"{} {}".format("a", "b") # 'a b' 按顺序
"{0} {1} {0}".format("a", "b") # 'a b a' 按下标,可重复
"{x} {y}".format(x=1, y=2) # '1 2' 按名字
"{:>8}".format("hi") # ' hi' 右对齐
"{:.3f}".format(1 / 3) # '0.333'
"{:08.3f}".format(1 / 3) # '0000.333'
三、f-string(推荐)¶
f-string 在编译期就展开成字符串拼接指令,运行时不需要解析格式串, 所以它是三者中最快的。花括号里可以放任意表达式:
n, x = 255, 1 / 3
f"{n}" # '255'
f"{n + 1}" # '256' ← 里面可以写表达式
f"{x:.3f}" # '0.333'
f"{n:08b}" # '11111111' ← 二进制补零到 8 位
f"{n:#x}" # '0xff'
f"{n:,}" # '255' 千位分隔符
f"{n:^9}" # ' 255 ' 居中
f"{'YES' if n else 'NO'}" # 三元也能塞
f"{x=}" # 'x=0.3333333333333333' ← 3.8+,调试神器
格式说明符的完整形式:
| 部分 | 取值 | 例子 |
|---|---|---|
| 对齐 | < 左 > 右 ^ 中 = 符号后填充 |
f"{s:*^10}" |
| 符号 | + 总显示 - 只负数 正数留空格 |
f"{n:+d}" |
| 宽度 | 整数 | f"{n:5d}" |
| 精度 | .k |
f"{x:.6f}" |
| 类型 | d b o x X f e g % s |
f"{n:b}" |
竞赛里最常用的三个:
f-string 内部不能出现反斜杠(3.12 之前)。 想在 f-string 里放换行要先定义
NL = "\n"再写f"{NL}",或者干脆别这么写。
4.6 三引号与多行字符串¶
三引号保留其中的所有换行和空格,所见即所得。用途:
- 多行注释(本质是一个没被赋值的字符串表达式,见 01-语法与执行模型)。
- docstring——本教程的题解都用它写「这题考什么」。
- 本地造测试数据:
DATA = """3
1 2
3 4
5 6
"""
for line in DATA.strip().split("\n"):
print(sum(map(int, line.split())))
三引号字符串的第一行如果紧跟引号换行,会多出一个开头的
\n:"""\nabc"""的第一个字符是换行。要么用"""\续行,要么.strip()。
4.7 字符与 ASCII:ord 与 chr¶
Python 没有 char,字符和整数之间的桥梁是这两个内置函数:
竞赛里的标准用法——把字母映射成 0..25 的下标:
idx = ord(c) - ord("a") # 'a'→0, 'b'→1, ..., 'z'→25
idx = ord(c) - 97 # 同上,少一次函数调用,更快
c = chr(idx + 97) # 反向
cnt = [0] * 26 # 26 个字母的桶
for c in s:
cnt[ord(c) - 97] += 1
记住这几个常数就不用查表了:
| 字符 | 码点 |
|---|---|
'0' |
48 |
'A' |
65 |
'a' |
97 |
'a' - 'A' |
32 |
因为大小写只差第 5 位(\(32 = 2^5\)),有个位运算小技巧:
批量转换用 map:
codes = list(map(ord, s)) # 字符串 → 码点列表
s = "".join(map(chr, codes)) # 码点列表 → 字符串
vals = [ord(c) - 97 for c in s] # 字母 → 0..25
更快的写法:直接用
从bytes。s.encode()得到bytes,而bytes迭代出来的就是整数:sys.stdin.buffer读进来的本身就是bytes,连encode都省了。见 20-输入输出处理。
字符串之间的大小比较就是按码点逐位比较(字典序):
4.8 bytes 与编码¶
Python 3 里 str 是 Unicode 码点序列,bytes 是字节序列,两者不能混用:
s = "你好"
b = s.encode("utf-8") # str → bytes: b'\xe4\xbd\xa0\xe5\xa5\xbd'
s2 = b.decode("utf-8") # bytes → str: '你好'
len(s) # 2 ← 2 个字符
len(b) # 6 ← 6 个字节(UTF-8 里一个汉字 3 字节)
| 类型 | 字面量 | 元素类型 | 可变? |
|---|---|---|---|
str |
"abc" |
长度 1 的 str |
否 |
bytes |
b"abc" |
int(0–255) |
否 |
bytearray |
bytearray(b"abc") |
int |
是 |
b = b"abc"
b[0] # 97 ← 是整数,不是 b'a'!
b[0:1] # b'a' ← 切片才是 bytes
list(b) # [97, 98, 99]
ba = bytearray(b"abc")
ba[0] = 120 # 可以原地改
bytes(ba) # b'xbc'
竞赛里用到 bytes 的场景只有两个:
sys.stdin.buffer.read()返回bytes。int(b"123")合法, 但要当字符串输出必须先.decode():
data = sys.stdin.buffer.read().split()
n = int(data[0]) # ✅ 数字不用解码
s = data[1].decode() # ✅ 字符串必须解码
print(data[1]) # ❌ 输出 b'abc',带引号,直接 WA
bytearray当高速字节数组用。它比list省内存(1 字节 vs 8 字节指针), 且find/count/rfind都是 C 级别扫描,适合做超大的 0/1 标记数组:
Python 3 里所有字符串都是 Unicode,没有 Python 2 的
u"..."前缀之说 (虽然为了兼容仍然允许写u"abc")。len(s)数的是码点,不是字节;这跟 C++ 的std::string::size()完全不同。 好在竞赛数据几乎全是 ASCII,两者等价。
4.9 例题¶
PIO10 单组_字符串(入门)¶
给定长度 \(n \le 10^5\) 的小写字符串 \(s\),将其倒置输出。 题面见 PIO10 原题(牛客)。
三个语言点:
s[::-1]是反转的唯一推荐写法。它在 C 层做一次反向内存拷贝, 比"".join(reversed(s))快 2–3 倍,比for循环快一个数量级。strip()不能省,理由见 4.1 的\r陷阱。n这一行必须读掉,否则第二个input()拿到的还是n。
PIO11 多组_字符串_T组形式(入门)¶
\(t \le 10^5\) 组,每组给 \(n\) 和串 \(s\),\(\sum n \le 10^5\)。
数据量上来了,改用 token 流。因为串内保证无空格,一个字符串就是一个 token:
import sys
def main():
data = sys.stdin.buffer.read().split()
t = int(data[0])
out = []
p = 1
for _ in range(t):
p += 1 # 跳过 n
out.append(data[p].decode()[::-1])
p += 1
sys.stdout.write("\n".join(out) + "\n")
main()
data[p] 是 bytes,.decode() 之后才是 str。
其实 bytes 也支持 [::-1](反转字节),对纯 ASCII 结果一样,
但 "\n".join(...) 要求元素是 str,所以解码不能省。
也可以反过来:全程不解码,最后用
b"\n".join(out)写进sys.stdout.buffer。 这样更快,但可读性差,数据量到 \(10^6\) 再考虑。
PIO12 单组_二维字符数组(入门)¶
\(n \times m\) 的字符矩阵(\(n, m \le 10^3\)),行和列都倒置后输出。
「行列都倒置」拆成两步,两步都是切片:
- 每一行内部反转 →
row[::-1] - 行的顺序反转 →
reversed(rows)
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
# 每行不含空格,所以一行就是一个 token;data[0]、data[1] 是 n 和 m,
# 矩阵第 i 行因此落在 data[2 + i],这个 2 就是跳过 n、m 的偏移量
rows = [data[2 + i].decode() for i in range(n)]
# r[::-1] 步长为 -1,从尾扫到头,即行内字符反转;
# reversed(rows) 再把行的先后顺序翻过来,两者合起来就是「行列都倒置」
sys.stdout.write("\n".join(r[::-1] for r in reversed(rows)) + "\n")
main()
reversed() 返回迭代器,不复制列表;配合生成器表达式喂给 join,全程不产生多余的中间列表。
注意 m 读进来也用不上——每行本身就是一个完整 token。
PIO13 多组_带空格的字符串_T组形式(入门)¶
\(t\) 组,每组给一个含空格的串,去掉空格后倒置输出。保证首尾不是空格。
这题是 token 流的反例。split() 会把 "one space" 切成两个 token,
行边界信息丢失后无法还原,必须按行读:
import sys
def main():
inp = sys.stdin
t = int(inp.readline())
out = []
for _ in range(t):
inp.readline() # 跳过 n
s = inp.readline().rstrip("\n")
out.append(s.replace(" ", "")[::-1])
sys.stdout.write("\n".join(out) + "\n")
main()
- 删空格用
s.replace(" ", ""),\(O(n)\) 一遍扫描。 不要用"".join(s.split())——那会把连续空格合并成一个再删,结果虽然一样但绕了一圈。 rstrip("\n")只去换行,不能用strip():题目保证首尾无空格, 但一旦哪天不保证,strip()会连有意义的空格一起吃掉。- 用
sys.stdin.readline而不是input(),快 5–10 倍。
判断口诀:串内可能含空格 → 按行读;确定无空格 → token 流。 完整讨论见 20-输入输出处理。
BISHI10 小红的字符串修改(简单,字符串 / 模拟)¶
给定小写串 \(s\) 与 \(t\)(\(|s| \le |t| \le 10^3\))。每次操作可把某个字母换成字母表中相邻的字母 (
a可换成b或z,即字母表首尾相连)。求最少操作次数,使 \(s\) 成为 \(t\) 的子串 (这里的「子串」= 连续子串)。 题面见 BISHI10 原题(牛客)。
拆成两层。
第一层,单个字符的代价。把 a–z 编号 \(0\)–\(25\),字母表首尾相连构成一个环,
从 \(a\) 走到 \(b\) 的最少步数就是环上距离:
第二层,s 长度固定,所以它只能对齐到 t 的某个起点 \(i \in [0, m-n]\),
枚举起点、逐位累加即可:
import sys
def main():
data = sys.stdin.buffer.read().split()
s = data[0].decode()
t = data[1].decode()
n, m = len(s), len(t)
# 预处理 26x26 代价表:字母表是环形的,a 和 z 相邻
cost = [[min(abs(a - b), 26 - abs(a - b)) for b in range(26)]
for a in range(26)]
# 转成 0..25 的下标数组,循环里就不用反复调 ord
sa = [ord(c) - 97 for c in s]
ta = [ord(c) - 97 for c in t]
print(min(
sum(cost[sa[j]][ta[i + j]] for j in range(n))
for i in range(m - n + 1)
))
main()
复杂度:\(O(n(m-n+1))\)。乍看是 \(O(nm) = 10^6\),但 \(n(m-n+1)\) 在 \(n = m/2\) 时取到最大值 \(\approx m^2/4 = 2.5 \times 10^5\),实测 0.06 秒,完全不用担心。
四个本章知识点在这里全用上了:
| 用到的点 | 位置 |
|---|---|
ord(c) - 97 把字母映射成下标 |
sa、ta |
| 字符串按下标随机访问是 \(O(1)\) | ta[i + j] |
| 预处理代价表避免循环内重复计算 | cost |
生成器表达式喂给 min/sum,全程不建中间列表 |
最后三行 |
常见错误:忘了字母表是环形的,直接写
abs(a - b)。 示例 2 里z→a只要 1 步而不是 25 步,正是用来卡这个的。
4.10 本章速查¶
| 场景 | 写法 |
|---|---|
| 反转字符串 | s[::-1] |
| 判回文 | s == s[::-1] |
去首尾空白 / 去 \r |
s.strip() |
| 循环拼接字符串 | 禁止 +=,用 "".join(parts) |
| 需要按下标改字符 | a = list(s) → 改 → "".join(a) |
| 切分输入 | 永远用无参 s.split() |
| 删除某字符 | s.replace(" ", "") |
| 批量字符映射 | s.translate(str.maketrans(a, b)) |
| 字母 → 0..25 | ord(c) - 97 |
| 0..25 → 字母 | chr(i + 97) |
| 26 字母计数 | cnt = [0]*26; cnt[ord(c)-97] += 1 |
| 保留 \(k\) 位小数 | f"{x:.6f}"(严格四舍五入见第 23 章) |
| 补前导零 | s.zfill(k) 或 f"{n:09d}" |
| 数转二进制串 | f"{n:b}",不带 0b 前缀 |
| 判合法整数 | try: int(s),不要用 isdigit() |
bytes 转 str |
.decode();数字可以直接 int(b"123") |
| 大量输出 | sys.stdout.write("\n".join(out) + "\n") |
| 子串查找 | s.find(t) 返回 -1;s.index(t) 抛异常 |
| 重叠计数 | s.count(t) 不重叠,需重叠请用 KMP |
| 去前缀 | s.removeprefix(p)(3.9+),不是 strip(p) |