Python set 去重平均 O(1) 因其底层为哈希表:通过 hash() 定位桶,再用 eq 判重;仅可哈希对象(如 int、str、tuple)可加入,list/dict/set 不可;哈希碰撞时线性探测+等值比较;扩容时单次 O(n),均摊仍 O(1)。

Python set 为什么能 O(1) 平均时间复杂度去重
因为 set 底层是哈希表(hash table),不是链表或树。插入/查找元素时,先对对象调用 hash() 得到哈希值,再映射到内部数组的某个索引位置;只要哈希函数分布合理、负载因子不过高,就能在常数时间内定位——去重本质就是“查重 + 跳过插入”,而查重靠的就是这个哈希查找。
注意:只有可哈希对象才能放进 set,比如 int、str、tuple(且内容全可哈希);list、dict、set 自身不可哈希,直接塞进去会报 TypeError: unhashable type。
hash() 和 __eq__ 是怎么配合判断“重复”的
两个对象被认为“相同”(即去重时只留一个),需同时满足:
– 哈希值相等(hash(a) == hash(b))
– 且实际相等(a == b,即 a.__eq__(b) 返回 True)
这意味着:
立即学习“Python免费学习笔记(深入)”;
- 哈希碰撞不可避免,但 Python 会在同一桶(bucket)里用线性探测或开放寻址做二次比对,调用
__eq__ - 自定义类如果要放进
set,必须同时实现__hash__和__eq__,且逻辑一致:若a == b,则hash(a) == hash(b),否则去重行为不可靠 - 字符串和小整数有特殊优化(比如小整数池、字符串驻留),它们的
hash值固定且计算极快
set 去重不是“遍历后删”,而是“边插边判”
写 set(my_list) 不是先建空集合再循环 append 风格地加,而是逐个调用 set_add() 内部逻辑:对每个元素算哈希 → 找桶 → 若桶空则插入;若桶非空,则比对哈希 → 若哈希同,再比 __eq__ → 相等就跳过,不等就找下一个空位(可能触发扩容)。
所以:
- 顺序不保留(
set无序),因为插入位置由哈希值决定,不是原列表索引 - 去重结果依赖哈希值稳定性:CPython 中字符串、数字哈希默认开启随机化(
PYTHONHASHSEED影响),但同一进程内是稳定的;重启后可能不同,不影响去重正确性,但影响set迭代顺序 - 内存开销比 list 大,因为哈希表要预留空槽(默认负载因子约 2/3),且每个桶存的是指针+哈希值+键对象引用
对比 list(set()) 和 dict.fromkeys() 的实际差异
常见误以为 list(set(lst)) 是最简去重法,但它丢顺序;而 list(dict.fromkeys(lst)) 在 Python 3.7+ 能保序且同样高效,因为 dict 同样基于哈希表,且插入顺序被保留。
关键区别:
-
set只存值,dict存键值对,但fromkeys的 value 统一为None,不额外占空间 - 两者哈希逻辑完全一致,性能几乎没差别;但
dict.fromkeys多一次键赋值操作,微乎其微 - 如果原数据含不可哈希项(如嵌套 list),两种都会报错,无法绕过
真正容易被忽略的是:哈希表扩容时的 rehash 成本——当元素数接近容量上限,插入可能触发整体重建,此时单次操作退化为 O(n),但均摊仍是 O(1)。高频增删场景下,初始预估大小(用 set().__sizeof__() 观察或手动 set().update(...) 分批)有时比盲目构造更稳。










