第 2 章 数据类型与转换¶
配套例题:BISHI30 二进制数1、BISHI31 二进制不同位数、BISHI14 特殊的科学计数法 来源:菜鸟教程 基本数据类型、数据类型转换、数字(Number)
2.1 六大标准数据类型¶
| 类型 | 关键字 | 可变? | 有序? | 竞赛角色 |
|---|---|---|---|---|
| 数字 | int float bool complex |
不可变 | — | 一切的基础 |
| 字符串 | str |
不可变 | 有序 | 字符串题、输入解析 |
| 列表 | list |
可变 | 有序 | 数组、栈、邻接表 |
| 元组 | tuple |
不可变 | 有序 | 可哈希的复合键、堆中元素 |
| 集合 | set |
可变 | 无序 | 去重、\(O(1)\) 判存在 |
| 字典 | dict |
可变 | 插入有序(3.7+) | 哈希表、计数、记忆化 |
「可变 / 不可变」是 Python 里最重要的一条分界线,它决定了:
- 能不能作为
dict的键或set的元素——只有不可变对象可哈希。
为什么?dict 和 set 用哈希值决定元素存放在哪个槽位。
如果一个对象放进去之后还能被改,它的哈希值就会跟着变,
而它的实际位置不会自动搬家——再去查它时会算出新槽位、扑空,
于是「明明放进去了却找不到」。禁止可变对象当键,就是从源头堵死这种情况。
2. 赋值时是复制还是共享引用——见 2.6。
2.2 整数 int:Python 最大的竞赛优势¶
Python 的 int 没有位数上限,只受内存限制。这一条直接消灭了 C++ 选手的两大痛点:
>>> 2 ** 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
>>> 12345678901234567890 * 98765432109876543210
1219326311370217952237463801111263526900
不会溢出,不需要开 long long,不需要写高精度。
10^18 级别的乘法在 C++ 里要小心翻倍溢出,Python 直接算。
代价是慢。大整数运算是 \(O(\text{位数})\) 甚至更高,不是 \(O(1)\)。 一个 \(10^6\) 位的数做一次乘法可能要几秒。所以:
能取模就取模。写
(a * b) % MOD而不是先算完整乘积再取模—— 虽然结果一样,但前者的中间值始终不超过 \(\text{MOD}^2\)。
进制表示¶
整数除法与取模的负数语义¶
这是 C++ 选手最容易栽的地方:
>>> 7 // 2 # 3
>>> -7 // 2 # -4 ← C++ 是 -3
>>> 7 // -2 # -4
>>> -7 % 2 # 1 ← C++ 是 -1
>>> 7 % -2 # -1
Python 的 // 是向下取整(floor division),% 的结果符号跟除数一致。
C/C++ 的 / 是向零截断,% 的符号跟被除数一致。
两个实战后果:
- 取模永远是非负的(当模数为正),所以
(x % MOD)不需要再+ MOD修正:
C++ 里必须写 ((x % MOD) + MOD) % MOD,Python 不用。这是个好处。
- 向零截断要自己写:
import math
math.trunc(-7 / 2) # -3
int(-7 / 2) # -3(int() 对 float 是截断)
-(-7 // 2) if ... else ... # 别这么绕
或者用 math.floor / math.ceil 明确表达意图。整数除法向上取整的惯用写法:
其它整数操作¶
divmod(17, 5) # (3, 2) 一次拿到商和余数
abs(-3) # 3
pow(2, 10) # 1024
pow(2, 100, 1000) # 快速幂取模!内置的,C 实现,见第 81 章
(255).bit_length() # 8 表示该数需要多少位
int.from_bytes(...) # 字节转整数
pow(a, b, m)是内置三参数快速幂,不要自己写——内置版是 C 实现, 比手写的 Python 快速幂快一个数量级。详见 81-快速幂与逆元。
2.3 浮点数 float¶
float 是 IEEE 754 双精度,约 15–17 位有效数字。
永远不要用 == 比较浮点数:
竞赛里的浮点原则:
- 能用整数就别用浮点。判断 \(a/b = c/d\) 应写成 \(a \cdot d = b \cdot c\)。
- 需要精确十进制时用
decimal.Decimal(见 23-浮点与科学计数法)。 - 需要精确分数时用
fractions.Fraction。
float 的特殊值:
float("inf") # 正无穷,常用作初始最小值
float("-inf") # 负无穷
float("nan") # 非数,nan != nan 恒成立
math.inf # 同 float("inf"),更常用
DP 初始化和最短路初始化都用 inf:
2.4 布尔 bool¶
bool 是 int 的子类,True == 1、False == 0:
这带来一个极其好用的技巧——布尔值可以直接求和计数:
假值(Falsy)清单¶
以下对象在布尔上下文里视为假,其余全为真:
所以判空可以直接写:
坑:
if not a无法区分「空列表」和「None」和「0」。 当0是合法值时,必须写if a is None。
2.5 复数 complex¶
竞赛中几乎只有一个用途:当二维点/向量用。
p = complex(x, y)
q = complex(u, v)
abs(p - q) # 两点距离,比 sqrt((x-u)**2 + (y-v)**2) 简洁
(q - p) * 1j # 向量逆时针旋转 90°
((q - p).conjugate() * (r - p)).imag # 叉积(可判左右转)
见 110-计算几何入门。
2.6 可变性、id 与 is¶
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表
b.append(4)
print(a) # [1, 2, 3, 4] ← a 也变了!
c = a[:] # 切片总是新建一个列表,把原来的元素引用逐个抄过去(浅拷贝)
c.append(5)
print(a) # [1, 2, 3, 4] ← 这次 a 没变
id(x) 返回对象的身份标识(CPython 里是内存地址)。
x is y 判断是不是同一个对象,x == y 判断值是否相等:
is只应该用于和None、True、False比较:if x is None。 拿is比较数字或字符串会产生「有时对有时错」的诡异行为,根源是两个优化:
- 小整数缓存:解释器启动时就把 \(-5\) 到 \(256\) 这些常用小整数各建好一个对象, 之后所有用到它们的地方共享同一份。所以
a = 100; b = 100时a is b是True, 换成1000就成了False——两次分别建了新对象。- 字符串驻留:源码里形如标识符的字符串字面量会被登记进一张全局表并共享, 但运行时拼出来的字符串通常不进这张表。
两者都是为了省内存和加快比较,并不保证哪些值一定被共享; 依赖它写判断,换个 Python 版本就可能翻车。判相等永远用
==。
浅拷贝的经典陷阱见 05-列表。
2.7 类型转换全表¶
Python 的类型转换是显式的函数调用,没有隐式类型转换(除了数字之间的自动提升)。
数字之间¶
| 函数 | 作用 | 注意 |
|---|---|---|
int(x) |
转整数 | 对 float 是向零截断,不是四舍五入 |
int(s, base) |
按进制解析字符串 | int("ff", 16) → 255,int("1011", 2) → 11 |
float(x) |
转浮点 | float("1e9") 合法 |
complex(re, im) |
转复数 | |
bool(x) |
转布尔 | 按 2.4 的假值表 |
>>> int(3.9) # 3 截断,不是 4
>>> int(-3.9) # -3 向零,不是 -4
>>> round(3.5) # 4
>>> round(2.5) # 2 ← 银行家舍入!见第 23 章
int(s, base) 的 base 可以是 2–36,也可以是 0(自动识别 0b/0o/0x 前缀):
数字 ↔ 字符串¶
| 方向 | 函数 | 例子 |
|---|---|---|
| 数 → 十进制串 | str(n) |
str(255) → "255" |
| 数 → 二进制串 | bin(n) |
bin(11) → "0b1011" |
| 数 → 八进制串 | oct(n) |
oct(15) → "0o17" |
| 数 → 十六进制串 | hex(n) |
hex(255) → "0xff" |
| 串 → 数 | int(s, base) |
int("1011", 2) → 11 |
bin/oct/hex 带前缀,去掉前缀用切片或格式化:
bin(11)[2:] # "1011"
format(11, "b") # "1011" 推荐
format(11, "08b") # "00001011" 补零到 8 位
format(255, "X") # "FF" 大写十六进制
int(s)的位数上限:出于安全考虑(超长数字串的进制转换是平方复杂度, 可被用来发起拒绝服务攻击),字符串转整数默认最多 4300 位。 这个限制在 3.11 引入,并回溯到了 3.9.14、3.10.7 等安全更新版本。 超了会抛ValueError。需要时用sys.set_int_max_str_digits(0)解除。 竞赛中很少触发,但处理超长数字串时要知道。
容器之间¶
| 函数 | 作用 | 注意 |
|---|---|---|
list(x) |
转列表 | list("abc") → ['a','b','c'] |
tuple(x) |
转元组 | |
set(x) |
转集合 | 会去重、丢失顺序 |
frozenset(x) |
转不可变集合 | 可作 dict 键 |
dict(pairs) |
由键值对建字典 | dict([(1,'a'),(2,'b')]) |
str(x) |
转字符串 | 对容器返回其 repr,如 "[1, 2]" |
list("hello") # ['h','e','l','l','o']
"".join(['h','e','l','l','o']) # 'hello' ← 反向操作
list(map(int, "12345")) # [1, 2, 3, 4, 5] 按位拆数字
dict(zip(keys, values)) # 两个列表压成字典
str(list)不是把列表转成字符串!str([1,2])得到的是"[1, 2]"(含方括号)。 想拼接元素用"".join(map(str, a))。
2.8 例题¶
BISHI30 二进制数1(简单,位运算)¶
给定整数,统计其二进制表示中 1 的个数(数位相关,具体题面见 BISHI30 原题(牛客))。
三种写法,都用到本章的类型转换:
等价写法(任选其一,都只输出一行):
如果要手写(面试常问),用 n & (n - 1) 消去最低位的 1:
原理见 46-位运算。
BISHI31 二进制不同位数(简单,位运算)¶
求两个数二进制表示中不同的位数。
异或后数 1 —— 异或的定义就是「相同为 0,不同为 1」:
BISHI14 特殊的科学计数法(简单)¶
解析科学计数法表示的数(题面见 BISHI14 原题(牛客))。
科学计数法的解析是类型转换的实战场景。关键判断:结果需要精确,还是允许浮点误差?
- 允许误差 →
float("1.23e5")直接可用,Python 原生支持。 - 需要精确 → 用
Decimal("1.23e5"),避免二进制浮点丢精度。
# [片段] 这里只演示「大整数不经过 float 也能安全解析」,完整题解见第 23 章
from decimal import Decimal
s = input().strip()
d = Decimal(s) # Decimal 原生支持 e 记号,且从字符串构造是精确的
完整题解与那道题的舍入/进位溢出细节,见 23-浮点与科学计数法 §23.6。
2.9 本章速查¶
| 场景 | 写法 |
|---|---|
| 大整数 | 直接算,不需要高精度;但能取模就取模 |
| 向上取整 | (a + b - 1) // b 或 -(-a // b) |
| 模运算取正 | 直接 x % MOD,Python 保证非负 |
| 快速幂取模 | pow(a, b, m),别手写 |
| 浮点比较 | abs(x - y) < 1e-9 |
| 无穷大 | math.inf |
| 布尔计数 | sum(cond for x in a) |
| 判空 | if not a;但 0 是合法值时用 is None |
| 数转二进制串 | format(n, "b"),不带前缀 |
| 串按进制转数 | int(s, base) |
| 数字逐位拆开 | list(map(int, str(n))) |
| 列表拼成串 | "".join(map(str, a)),不是 str(a) |
| 判同一对象 | is 只用于 None/True/False |