跳转至

第 2 章 数据类型与转换

配套例题:BISHI30 二进制数1、BISHI31 二进制不同位数、BISHI14 特殊的科学计数法 来源:菜鸟教程 基本数据类型、数据类型转换、数字(Number)


2.1 六大标准数据类型

类型 关键字 可变? 有序? 竞赛角色
数字 int float bool complex 不可变 一切的基础
字符串 str 不可变 有序 字符串题、输入解析
列表 list 可变 有序 数组、栈、邻接表
元组 tuple 不可变 有序 可哈希的复合键、堆中元素
集合 set 可变 无序 去重、\(O(1)\) 判存在
字典 dict 可变 插入有序(3.7+) 哈希表、计数、记忆化

「可变 / 不可变」是 Python 里最重要的一条分界线,它决定了:

  1. 能不能作为 dict 的键或 set 的元素——只有不可变对象可哈希。

为什么?dictset 用哈希值决定元素存放在哪个槽位。 如果一个对象放进去之后还能被改,它的哈希值就会跟着变, 而它的实际位置不会自动搬家——再去查它时会算出新槽位、扑空, 于是「明明放进去了却找不到」。禁止可变对象当键,就是从源头堵死这种情况。 2. 赋值时是复制还是共享引用——见 2.6。

d = {(1, 2): "ok"}       # ✅ 元组可以作键
d = {[1, 2]: "no"}       # ❌ TypeError: unhashable type: 'list'

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\)

进制表示

0b1011        # 二进制 = 11
0o17          # 八进制 = 15
0xff          # 十六进制 = 255
1_000_000     # 下划线分隔符,纯粹为了可读性,值是 1000000

整数除法与取模的负数语义

这是 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++ 的 / 是向零截断,% 的符号跟被除数一致。

两个实战后果:

  1. 取模永远是非负的(当模数为正),所以 (x % MOD) 不需要再 + MOD 修正:
MOD = 10 ** 9 + 7
print((-5) % MOD)      # 1000000002,已经是正的

C++ 里必须写 ((x % MOD) + MOD) % MOD,Python 不用。这是个好处。

  1. 向零截断要自己写
import math
math.trunc(-7 / 2)          # -3
int(-7 / 2)                 # -3(int() 对 float 是截断)
-(-7 // 2) if ... else ...  # 别这么绕

或者用 math.floor / math.ceil 明确表达意图。整数除法向上取整的惯用写法:

(a + b - 1) // b            # a/b 向上取整(a, b 均为正)
-(-a // b)                  # 同上,且对负数也正确

其它整数操作

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 位有效数字。

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

永远不要用 == 比较浮点数

EPS = 1e-9
if abs(x - y) < EPS:            # ✅
    ...

竞赛里的浮点原则:

  1. 能用整数就别用浮点。判断 \(a/b = c/d\) 应写成 \(a \cdot d = b \cdot c\)
  2. 需要精确十进制时用 decimal.Decimal(见 23-浮点与科学计数法)。
  3. 需要精确分数时用 fractions.Fraction

float 的特殊值:

float("inf")     # 正无穷,常用作初始最小值
float("-inf")    # 负无穷
float("nan")     # 非数,nan != nan 恒成立
math.inf         # 同 float("inf"),更常用

DP 初始化和最短路初始化都用 inf

dist = [math.inf] * n

2.4 布尔 bool

boolint子类True == 1False == 0

>>> True + True
2
>>> sum([True, False, True])
2
>>> isinstance(True, int)
True

这带来一个极其好用的技巧——布尔值可以直接求和计数

cnt = sum(x > 0 for x in a)             # 统计正数个数
cnt = sum(s[i] != t[i] for i in range(n))   # 统计不同位置数

假值(Falsy)清单

以下对象在布尔上下文里视为假,其余全为真:

False   None   0   0.0   0j   ''   []   ()   {}   set()   range(0)

所以判空可以直接写:

if not a:            # a 是空列表 / 空串 / 0 / None
    ...
while stack:         # 栈非空
    ...

if not a 无法区分「空列表」和「None」和「0」。 当 0 是合法值时,必须写 if a is None


2.5 复数 complex

z = 3 + 4j
z.real, z.imag       # 3.0, 4.0
abs(z)               # 5.0,即模长

竞赛中几乎只有一个用途:当二维点/向量用

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 可变性、idis

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 判断值是否相等

a = [1, 2]; b = [1, 2]
a == b        # True   值相等
a is b        # False  不是同一个对象

is 只应该用于和 NoneTrueFalse 比较:if x is None。 拿 is 比较数字或字符串会产生「有时对有时错」的诡异行为,根源是两个优化:

  • 小整数缓存:解释器启动时就把 \(-5\)\(256\) 这些常用小整数各建好一个对象, 之后所有用到它们的地方共享同一份。所以 a = 100; b = 100a is bTrue, 换成 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 前缀):

int("0b1011", 0)     # 11
int("0xff", 0)       # 255

数字 ↔ 字符串

方向 函数 例子
数 → 十进制串 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 = int(input())
print(bin(n).count("1"))            # 转成二进制串再数 1,最直白

等价写法(任选其一,都只输出一行):

# [片段]
format(n, "b").count("1")           # 不带 0b 前缀,效果相同
n.bit_count()                       # 最快,但 Python 3.10+ 才有,3.9 环境不可用

如果要手写(面试常问),用 n & (n - 1) 消去最低位的 1:

cnt = 0
while n:
    n &= n - 1                       # 每次消掉一个 1
    cnt += 1

原理见 46-位运算

BISHI31 二进制不同位数(简单,位运算)

求两个数二进制表示中不同的位数。

异或后数 1 —— 异或的定义就是「相同为 0,不同为 1」:

a, b = map(int, input().split())
print(bin(a ^ b).count("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