千万级用户去重不可用O(n²)双重循环,应以哈希表实现单次扫描+O(1)查表,关键在标准化业务键、流式处理、内存防护(布隆过滤器/分块/落地缓存)及去重结果校验。

直接用双重循环处理千万级用户数据,基本不可行——时间复杂度 O(n²),哪怕每轮比较仅 100 纳秒,1000 万条数据也要跑上数月。真正能落地的“双重循环 + 哈希”实战,本质不是嵌套遍历,而是用哈希做快速判重主干,把双重循环降维成一次扫描 + 常数级查表。关键在结构设计和边界控制。
先明确数据形态再选哈希载体
异构系统导入的用户数据,字段常不统一:有的含 {"uid": "U1001", "name": "李四", "email": null},有的只有 ["U1001", "李四"],甚至混着 JSON 字符串。不能直接扔进 HashSet——字典/列表不可哈希,字符串又可能因空格、大小写、字段顺序不同导致逻辑重复但哈希值不同。
建议做法:
- 定义唯一业务键(如
uid),优先按该字段去重;若无,则构造标准化签名:对非空字段排序后拼接,例如sorted_tuple = tuple(sorted((k,v) for k,v in user.items() if v is not None)),再转为hashable_key = str(sorted_tuple) - 避免用原始字典或列表作 key;也不建议直接
json.dumps(user, sort_keys=True)——序列化开销大,且浮点数精度、None与null映射易出错 - 若字段含敏感信息(如手机号脱敏规则不一),清洗前先统一规整:比如全转小写、去首尾空格、补默认值,再生成哈希键
用哈希表替代内层循环,单次遍历完成去重
所谓“双重循环配合哈希”,真实逻辑是:外层遍历每条记录,内层本该逐个比对历史记录——现在改用哈希表查键是否存在,O(1) 替代 O(n)。这不是语法上的双 for,而是语义上的“判断+存档”两步合并。
Python 示例(兼顾内存与速度):
seen = set()
unique_users = []
for user in raw_user_iter: # 流式读取,不全加载
key = generate_dedup_key(user) # 上一步定义的标准化函数
if key not in seen:
seen.add(key)
unique_users.append(user)
Java 类似(注意重写 equals 和 hashCode):
- 自定义
UserKey类,只包含用于判重的字段,且确保hashCode()稳定、equals()逻辑正确 - 用
HashSet<UserKey>存已见 key,遍历时构造临时UserKey判重 - 初始化容量设为预估唯一数 ÷ 0.75,避免扩容抖动
应对千万级规模的内存防护策略
直接 new 一个装千万字符串的 HashSet,JVM 很可能 OOM。必须分层控量:
- 流式处理:从数据库游标、文件分块或 Kafka 分区拉取,每次处理 1–10 万条,处理完清空局部集合
- 布隆过滤器初筛:对 uid 等高基数字段,先过一层
BloomFilter,确认“大概率不存在”才进精确哈希;误判率设 1%,内存占用可压到 MB 级 - 落地缓存兜底:若内存实在吃紧,把
seen换成 Redis 的SET或本地 LevelDB,用磁盘换内存,速度略降但稳定 - 监控实际重复率:如果发现
len(seen) / total_processed < 0.05(即 95% 重复),说明数据源问题更大,应前置拦截而非硬洗
验证去重结果不丢不滥
清洗后必须校验,不能只信代码逻辑:
- 统计原始条数、去重后条数、哈希集合 size,三者应一致;若不等,检查 key 构造是否漏字段或误判 null
- 抽样反查:随机取 100 条去重后数据,在原始数据中搜索其 key,确认所有副本都被剔除
- 字段一致性检查:对保留的每条记录,核对其关键字段(如手机号格式、邮箱域名)是否符合业务规范,避免清洗引入脏数据

















