列表去重这事,看着是个再小不过的操作,但真做起来,几乎每个人都在某个场景里被它坑过。前几天后台有人留言拿“5-7列表去重”当关键词来问,其实不用管这个数字具体指什么,把它理解成“五到七种核心去重方案”就顺了:从 Python 写脚本处理数据、SQL 查报表时剔除重复行,到前端数组去重、Excel 做下拉列表,再到 RPA 抓回来的列表带了一堆空值和方括号,底层要解决的问题是同一个——怎么从一堆重复、脏乱的数据里,又快又稳地捞出干净且符合业务预期的那一份。
这篇文章我按自己的实际经验,把 7 个最常用的场景拆开讲,每个场景都给出能直接抄的写法、选型理由和翻车记录。不保证是最华丽的解法,但保证是跑过、对比过、踩过坑之后沉淀下来的。
1. 动手之前,先搞清楚你要哪种去重
很多人一上来就写list(set(data)),结果交上去被同事反手一个 bug:顺序全乱了。这不是写法错了,而是需求理解错了。去重的第一原则不是“怎么写”,而是“你要的是哪种去重”。
1.1 三种典型诉求决定方案方向
第一种诉求是只关心有哪些元素,不关心顺序。典型场景是统计用户 ID 总量、把关键词列表去重后做词云,这时候set是最优解,简单粗暴、性能最好。
第二种诉求是去重后必须保持原有顺序。典型场景是日志流水、商品推荐位,第一个出现的元素要保留,后面的重复项删掉,顺序不能变。这类需求占据实际开发中的八成,最容易被 set 方案坑到。
第三种诉求是按对象的某个字段去重,比如一个用户列表里有重复的 email,要根据 email 去重,但用户的其他信息(昵称、注册时间)不能丢。这种用单纯的值去重完全没法处理,必须借助字典或 Map 结构。
说句不好听的,我看到过很多人把三种诉求混为一谈,在“保序去重”的需求里用了 set,在“对象字段去重”里用了JSON.stringify硬转字符串,最后要么顺序不对,要么性能和正确性两边都没占到。所以动手前先问自己一句:重复的定义到底是什么?顺序要不要保留?
1.2 数据规模决定算法选型
第二个问题更具杀伤力:数据量有多大?这直接决定了方案在不在你的内存里。
| 数据规模 | 推荐方案 | 时间复杂度 | 内存成本 |
|---|---|---|---|
| < 1万条 | 几乎任意方法 | O(n) | 可忽略 |
| 10万 ~ 1000万条 | 哈希类结构(set / dict / Map) | O(n) | 较高,约是数据本身的 2~5 倍 |
| 千万条以上 | 排序 + 相邻去重,或分批哈希后合并 | O(n log n) | 低,可用外排控制 |
| 百亿条以上 | 近似去重(布隆过滤器)或分区合并 | O(n) | 可控但不精确 |
我最早做数据处理的时候,接过一个千万级用户行为去重的需求,想都没想直接list(set(rows)),结果服务器内存直接被打满,进程被 OOM Kill 了。后来换成先按时间字段分区,每个区用哈希去重,最后再合并内存,问题当场解决。算法课上说的“空间换时间”,在去重这个场景里体现得非常直接。
2. Python 列表去重:从 set 到 dict,把性能跑满
Python 是处理列表去重最常见的语言,也是坑最多的语言。下面这套组合我用了很多年,基本覆盖 99% 的场景。
2.1 set 一步去重适合不保序场景
最朴素的写法大家都懂:
nums = [5, 7, 5, 2, 7, 9, 2] unique = list(set(nums)) print(unique)set底层就是散列表,每个元素哈希后落桶,重复元素写不进新桶,天然去重。查找和插入的平均复杂度都是 O(1),整个去重过程 O(n)。对 100 万整数去重,实测 0.2 秒左右,这个性能在多数场景里足够了。
但这里有个隐蔽问题:输出顺序是“不确定”的。Python 字符串的哈希带有随机化机制(PYTHONHASHSEED),同一组字符串,每次运行生成的 set 内部排列都可能不同。你要是拿这个结果去对比线上文件、做数据比对,会越对越懵。所以我的习惯是:只要产品上没人明确说“顺序无所谓”,我默认都写保序版本。
2.2 保序去重:dict.fromkeys 与列表推导式
保序去重有两个经典写法,一个简洁一个刁钻,都推荐记下来。
# 写法一:dict.fromkeys,Python 3.7+ 字典有序,天然保序 nums = [5, 7, 5, 2, 7, 9, 2] unique = list(dict.fromkeys(nums))dict.fromkeys(nums)会把列表元素当作字典的键,字典键唯一,所以重复元素自动被丢弃,同时字典在 Python 3.7 之后保证插入顺序,因此得到的就是“第一次出现顺序”的去重结果。这是我最常用的方案,代码简洁到不用注释。
# 写法二:列表推导式 + set 记录已见元素,兼容性和可读性更好 seen = set() unique = [x for x in nums if not (x in seen or seen.add(x))]这个写法很多人第一次看会愣住,解释一下:seen.add(x)返回None,None是假值,所以not (x in seen or seen.add(x))的意思是——如果 x 没出现过,就把它加进 seen 并保留到新列表;如果出现过,表达式变成not True,丢弃。这段代码是面试高频题,也是新手最容易抄错的一行。
顺带回应一下热搜词里的“列表切片”:如果你需要在去重的同时保留原列表给后续逻辑用,记得先复制一份再操作,比如copy_nums = nums[:]。因为遍历列表同时pop或remove会破坏索引,导致跳项,切片复制是成本最低的保护手段。
2.3 进阶:对象列表按字段去重
真实业务里,列表里装的很少是裸的值,更多是字典或对象。比如从接口拉回来的用户列表,里面同一个id可能出现多条记录:
users = [ {"id": 1, "name": "a"}, {"id": 2, "name": "b"}, {"id": 1, "name": "a-again"}, ] # 按 id 去重,保留第一次出现的记录(dict 推导式天然保序) unique_users = list({u["id"]: u for u in users}.values()) print(unique_users)如果你要保留的是“最新一条”,把遍历顺序改成倒序,或者每次用新记录覆盖旧记录,都能达到目的。对比一下 SQL 里的ROW_NUMBER() OVER (PARTITION BY id ORDER BY time DESC),思路完全一致:先定义“重复键”,再定义“保留哪一条”。这个思想在 Python、SQL、JavaScript 里是通用的,能想通这个,后面所有的去重问题都会好办很多。
3. JavaScript 数组去重:前端的高频操作
前端拿到的数据经常是后端吐出来的 JSON 数组,重复项没少过。处理数组去重,JavaScript 的写法和 Python 很像,但细节坑更多。
3.1 数组去重的三种常见写法
// 写法一:Set + 展开运算符,最简洁,保序且支持原始值 const arr = [1, 2, 2, 3, 1, 4]; const unique = [...new Set(arr)];Set 在 JavaScript 里同样基于哈希表,O(n) 复杂度,并且遍历顺序就是插入顺序,所以这个方案天然保序,可以说集合了简洁、性能和正确性三个优点。
// 写法二:filter + indexOf,经典面试写法,注意是 O(n^2) const unique = arr.filter((value, index) => arr.indexOf(value) === index);indexOf每次都要从头扫描数组,两层嵌套下来就是 O(n^2)。数组小(几百条)无所谓,数组一大就明显卡顿,别在 10 万条数据上硬跑。
// 写法三:reduce + includes,适合在对象数组需要额外判断的时候扩展 const unique = arr.reduce((acc, cur) => { if (!acc.includes(cur)) acc.push(cur); return acc; }, []);老实说,原始值数组去重,[...new Set(arr)]大概率就是最终答案,写法二和三更多是用来应对“不能直接用 Set”的扩展场景,比如要根据对象属性去重。
3.2 对象数组按属性去重:reduce + Map
对象数组去重是前端面试的高频题。直接new Set对对象数组没意义,因为每个对象都是独立引用,Set会认为{ id: 1 }和{ id: 1 }是两个不同元素。
const users = [ { id: 1, name: "a" }, { id: 2, name: "b" }, { id: 1, name: "c" }, ]; const map = new Map(users.map((user) => [user.id, user])); const result = [...map.values()];核心思路是借助 Map 的键唯一性:把要判断的字段当作键,对象本身当作值,重复键自动覆盖。users.map((user) => [user.id, user])先把对象数组变成键值对数组,new Map()吃掉重复键,最后map.values()取出去重后的对象。保序性、性能、代码量三个指标都很好。
如果想去掉重复项之后保留的“不是最后一条而是第一条”,可以用循环加if (!map.has(user.id)) map.set(user.id, user),和 Python 那边的思路一模一样。
3.3 NaN、引用类型与稀疏数组的隐藏坑
三个坑值得单独拿出来说。
第一个是NaN。Set里NaN === NaN,所以[...new Set([NaN, NaN])]得到[NaN],这是正确的。但用filter + indexOf时,NaN的indexOf返回 -1,因为底层用的是严格相等判断且NaN !== NaN,这个方案会把两个NaN全保留下来,去重失败。
第二个是引用类型。[{a: 1}, {a: 1}]用 Set 去重会得到两个,因为对象比较的是引用地址,不是内容。如果要按内容去重,得先序列化,比如:
const unique = [...new Set(arr.map((item) => JSON.stringify(item)))].map((str) => JSON.parse(str));但 JSON 序列化对键顺序敏感,{a:1, b:2}和{b:2, a:1}会被当成两个对象,而且如果有Date、RegExp这类对象,序列化结果也不可靠。所以这个方案只适合简单对象。
第三个是稀疏数组。[, 1, 2, 1]这种数组,filter会跳过空位,而Set会把空位当作undefined,两者得到的结果数量不一样。做数据处理之前先补洞arr.flat()或把空位显式过滤掉,能避免很多无意义的分歧。
4. SQL 去重:不只是 DISTINCT
SQL 里的去重是被问得最多、也是最容易“表面会了,实际不会”的领域。很多人只知道DISTINCT,但在多表查询和数据清洗场景里,DISTINCT往往救不了你。
4.1 单表去重:DISTINCT 与 GROUP BY 怎么选
单表去重最简单:
SELECT DISTINCT user_id FROM orders; SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id;DISTINCT是对整行进行去重,如果SELECT后面只有一列,那它和GROUP BY user_id在结果上一致;但如果你要顺带统计每个用户的下单数、总金额,就必须用GROUP BY配合聚合函数。
还有个常用的巧劲是COUNT(DISTINCT col):
SELECT COUNT(DISTINCT user_id) AS active_users FROM orders WHERE created_at >= '2025-01-01';这种写法的优势是只关心数量,不关心明细,数据库会针对 DISTINCT 做专门的优化,比先把所有 user_id 捞出来再在应用层去重高效得多。做报表的人尤其爱用这个写法人话总结就是:DISTINCT 管“有哪些”,GROUP BY 管“各有多少”,COUNT(DISTINCT) 管“一共有几种”。
4.2 多表查询去重:JOIN 产生重复行后的处理
多表查询里的重复,往往不是“原表有重复”,而是 JOIN 把行数放大了。举个例子:订单表orders和订单明细表order_items关联,一个订单有多条明细,一次 JOIN 之后,每个订单会变成多行。你按照订单号去重,发现DISTINCT order_id可以去掉;但如果你想把订单的所有字段都带出来,明细又不一样,DISTINCT *根本去不掉,因为每行的 items 内容不同。
MSSQL 和 PostgreSQL 里最通用、也最精确的解法是窗口函数:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY created_at DESC ) AS rn FROM orders ) SELECT * FROM ranked WHERE rn = 1;PARTITION BY user_id定义“重复组”,ORDER BY created_at DESC定义组内排序,rn = 1表示每组只留第一条。这套写法和第 2 章对象字段去重的思路是同一个,只是换成了 SQL 语法。
如果数据库不支持窗口函数(比如 MySQL 5.7 及更老版本),可以用自连接 + EXISTS 来替代,代价是性能差一些:
SELECT o.* FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM orders o2 WHERE o2.user_id = o.user_id AND o2.created_at > o.created_at );这段逻辑是:找那些“不存在比它更新时间更晚的同 user_id 记录”的行,也就是组内最新一条。理解起来稍微绕,但正确性没问题。在大厂面试里,这道题几乎年年出现,能把两个版本的差异讲清楚,面试官基本就能确认你是真做过 SQL 的。
4.3 数据清洗中保留最新记录的去重方式
报表和数仓里最常见的清洗动作:同一业务主键存在多条历史记录,要按时间保留最新一条,老的全部删掉。最危险的做法是先SELECT *看几眼,然后手写DELETE。我建议的节奏是:先查数量,再备份,最后删除。
MySQL 8.0 的窗口函数写法:
DELETE FROM orders WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) tmp WHERE rn > 1 );需要注意中间套了一层子查询tmp,因为 MySQL 不允许在DELETE的WHERE子句中直接引用窗口函数派生表。老版本 MySQL 可以用多表更新语法:
DELETE o1 FROM orders o1 JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at < o2.created_at;这个写法会删除每组中不是最大created_at的所有行,如果同一时间戳有多行,建议在条件里加上主键比较,比如AND o1.id < o2.id,保证能收敛到一个确定结果。执行前一定先跑一遍等价的 SELECT 统计影响行数,否则手一抖,半小时后你就在恢复数据了。
5. 回归本质:散列表如何决定去重效率
聊到这一步,去重方案的底层其实都指向同一个数据结构:散列表(哈希表)。理解它,你才能理解为什么 Set 那么快,为什么哈希函数设计不好会把 O(n) 变成 O(n^2),以及什么时候该放弃哈希改用排序。
5.1 哈希原理与拉链法实现
散列表的基本操作很简单:用哈希函数把任意值映射成数组下标,写入时如果发现下标位置已经被值占满,就冲突了。数据工程和算法竞赛里经常要手写散列表,比如很常见的”模拟散列表“题目,本质就是帮你把这层原理扎扎实实跑一遍。
用拉链法实现一个最小可用的整数散列表:
#include <cstring> const int N = 100003; int h[N], e[N], ne[N], idx; void init() { memset(h, -1, sizeof h); idx = 0; } void insert(int x) { int k = (x % N + N) % N; // 保证下标非负 e[idx] = x; ne[idx] = h[k]; h[k] = idx++; } bool find(int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1; i = ne[i]) { if (e[i] == x) return true; } return false; }这里的核心是一个数组加一个链表数组:先通过取模定位到桶,再用链把同一个桶里的冲突元素串起来。去重操作就是不断insert,插入前先find,找不到才写进去。生活类比就是图书馆的储物柜:按姓名首字母分柜子,首字母相同的人排成一串,找的时候沿着链往下翻。
5.2 冲突与扩容对去重性能的影响
哈希表的性能取决于一个关键指标:装载因子,也就是已存元素数和桶数的比值。比值超过 0.7 左右,冲突概率会陡增,链越来越长,查找从 O(1) 退化到 O(k),k 是平均链长。
这就是为什么 Python 的 set 和 dict 会“自动扩容”:元素多了之后,重新分配更大的桶数组,把已有元素重新哈希一遍。扩容是一次成本很高但均摊可接受的操作,这也是为什么你往 set 里灌几百万元素,它会偶尔卡一下的原因。
另一个要命的问题是哈希函数选得差。比如你取模用的是 100000,而你的数据全是偶数的 userId,哈希结果全落在偶数桶里,桶分布极不均匀,性能直接崩盘。这就是为什么竞赛和工程里手写哈希表的时候,常量 N 通常选一个大质数,比如 100003、200003,取模结果更不容易出现周期性聚集。
5.3 大数据量下:哈希去重 vs 排序去重
当内存装不下全部数据时,哈希方案就失灵了。这时候排序去重反而更靠谱:把所有数据排好序,相同的元素必然相邻,然后一趟扫描,后一个等于前一个就跳过。排序可以走外部排序,磁盘顺序读写,内存只占一小块,这是哈希做不到的。
| 维度 | 哈希去重 | 排序去重 |
|---|---|---|
| 时间复杂度 | O(n) | O(n log n) |
| 内存占用 | 高,约数据量 2~5 倍 | 低,可外排 |
| 是否保序 | 看实现 | 天然有序 |
| 适用规模 | 千万内 | 亿级以上 |
工程里还有个常见的折中方案:先按照某个字段拆成多个小分区,每个分区能装进内存,用哈希去重,再把各个分区的结果合并,合并时再做一次排序去重。这其实就是 MapReduce 里 shuffle 阶段的老思路,很多离线数仓的去重任务底层就是这么跑的。知道这一层,你在遇到“几亿条日志去重”的时候就不会慌着写 set 了,而是会先估算内存,再决定方案。
6. 办公场景实战:Excel 与 RPA 中的列表清理
写代码的人容易忽略另一侧的“去重需求”:Excel 表格、RPA 抓取、甚至下拉列表里的重复数据,每天都在消耗职场人的时间。这些场景没有代码那么丝滑,但也有很成熟的套路。
6.1 Excel 下拉列表制作与同步整列的去重细节
做 Excel 下拉列表经常遇到一个问题:数据源列里有重复值,下拉框里跟着显示一堆重复项,用户选起来很累。正确流程是:
先把源列去重:选中数据列,工具栏“数据 - 删除重复值”,这个操作会物理删除重复行,直接改写原列。注意如果你后续还要完整数据集,先复制一份到别的 sheet 再做。
接着做下拉列表:选中目标单元格区域,依次点“数据 - 数据验证 - 设置 - 序列”,来源框里填写去重后的区域。如果希望新增行自动继承下拉验证,直接选中整列应用验证规则,或者把数据区域定义成“表格(Table)”,新行会自动带出验证规则。
关于“怎么把下拉列表同步到整列”,很多人的误区是只在生成下拉的那个单元格设置验证,之后复制粘贴、拉填充柄,发现新格子没有下拉。解决方式很简单:验证规则在应用时,确保范围覆盖整列(比如=$A$2:$A$1000),或者用表格功能让区域自适应。再把“输入无效数据时显示出错警告”勾上,能挡住很多手误。
如果你不想动原始数据,还有一种“提取唯一值”的公式流玩法:
=IFERROR(INDEX($A$2:$A$100, MATCH(0, COUNTIF($E$1:E1, $A$2:$A$100), 0)), "")这是一个数组公式,新版本 Excel 会自动溢出输出,老版本要按Ctrl+Shift+Enter结束。原理是每次都找“前面已经提取出来的值中没出现过的下一个值”,一步步榨出唯一列表。公式稍微复杂,但不需要写 VBA,也不用破坏原列,适合做临时下拉源。
6.2 影刀 RPA 处理列表中的 [] 与空元素
用 RPA 工具从网页抓列表是常见需求,热搜里那个“影刀 RPA 如何将列表中的 [] 去掉”很有代表性。先说结论:如果你在 RPA 工具里看到一个列表打印出来是['a', '', 'b', ' ', 'c'],那对[]不是元素,是列表本身的外壳;你需要清理的对象是里面的空字符串和带空白的内容。
在影刀里用 Python 表达式处理的话:
# 去掉空字符串、纯空白元素 items = ["a", "", "b", " ", "c", None] clean = [str(i).strip() for i in items if str(i).strip()]这行代码的逻辑是先strip去掉首尾空白,再判断是否为空,两步合一,空串、空格串、None全部过滤掉。之后再去重就顺理成章:
seen = set() result = [x for x in clean if not (x in seen or seen.add(x))]还有一种情况,列表不是 RPA 直接返回的数组,而是一个字符串,长这样:
["a", "b", "c"]这个是 JSON 格式的数组文本,字段是带引号的字符串,外面还有方括号。这种必须先做一次 JSON 解析,转成真正的数组,才能用上面的逻辑:
import json real_list = json.loads(raw_text)同理,如果元素文本里本身就夹杂着方括号字符,比如"[abc]",那就要用正则把中括号剥掉再进清洗流程:re.sub(r"[\[\]]", "", text)。这个大原则适用于所有 RPA 列表处理:先搞清数据类型,再谈清洗,最后去重。
6.3 一句话讲透其他列表场景的通用套路
这个问题给我的搜索词里,还躺着很多奇奇怪怪的列表场景:AHK 下拉列表用正则清理重复选项、Discuz 列表页 SEO 需要避免重复标题、舆情情感词列表去重后入库、全视频列表去重后排序、网络电台地址列表去重校验等等。表面上天差地别,归纳起来就三句话:
先判断重复的标准是什么(是全文相同,还是某个主键字段相同);再确认顺序是否要保留(大部分业务都要求保序);最后评估数据量和工具能力(写脚本、写 SQL、还是手工 Excel)。这三问一过,方案基本自动浮出水面。很多非技术岗位的人卡在两个地方:一是没想到“去重前要先去空格”,二是没想到“HTML 列表或网页表格里的可见重复,底层往往是两份结构一样但文本不一致的数据”。先统一格式,再定义重复键,这个问题就解决了一半。
7. 高频翻车现场与问题排查速查表
最后把这几年高频遇到、以及网上高频被搜索的“列表去重翻车问题”整理成一张速查表。这张表是给团队内部分享用的,放在这里希望能帮你少走点弯路。
7.1 高频问题速查
| 现象 | 常见原因 | 解决方案 |
|---|---|---|
| 去重后顺序乱了 | 用了 set 等无序结构 | 改用 dict.fromkeys / Set 且输出用展开操作 |
| 去重后数量不对 | 未考虑字符串空格、大小写 | 先去空白和统一大小写,再做去重 |
| 去重把不相同的元素合并了 | 重复键定义不精确,比如用整行而不是主键 | 明确业务重复键,按字段去重 |
| SQL JOIN 后重复越来越多 | 一对多关联放大了行数 | 用 ROW_NUMBER() PARTITION BY 去重 |
| Excel 下拉列表一堆重复项 | 数据源列没有清理重复值 | 先删除重复值,再应用数据验证 |
RPA 列表里有[]和空值 | 字符串未解析,或包含空白元素 | json.loads 后再 strip 过滤 |
| 更新源列表报错、提示失效源 | 源地址失效或网络不可达 | 检查网络连接,删除失效源条目,换官方源重试 |
| 标签列表页生成重复内容 | 列表页归档规则未限制 | 设置 SEO 列表规则,按分类且不重复抓取 |
| 大数组去重内存爆掉 | set 占用过大 | 改用排序 + 相邻去重 或分批处理 |
这里特别提一下“更新源列表报错”这个场景。无论是 Linux 系统的软件源还是各类工具的内置源,更新时报错大多是两个原因:一是本机网络确实不通,二是列表里面躺着已经失效的历史源地址。排查顺序是:先确认当前网络是否正常,通不通目标站点;再打开源列表文件,把重复的或者已经失效的条目注释或删除掉,尤其是那些来源不明、格式异常的条目;最后切换到该软件的官方源,重新执行更新。整个过程和列表去重的关系在于:源列表本身也是数据,冗余和脏源同样需要清洗。
7.2 我的几条独家避坑经验
这些经验没有写在任何文档里,都是实打实踩出来的。
第一条,去重前先统一数据类型。数字1和字符串"1",在 Python 和 JavaScript 的严格比较里是两个完全不同的元素,但在很多业务口径里它们就是同一个东西。不统一类型就做哈希去重,你会得到“看似去重成功、实则两个都留着”的结果,而且这种 bug 极难排查,因为打印出来肉眼几乎一模一样。
第二条,上手写方案之前,先确认“重复”的定义。业务方口中的“重复”往往不是“完全相同”,而是“某一个维度相同”。比如用户表里同名同姓算不算重复?要结合手机号、邮箱、身份证号去判断。先把这个定义问清楚,再动代码,不然你辛辛苦苦写出来的去重脚本,上线第一天就会被业务方打回来。
第三条,去重后的数据一定要做“数量校验”。我自己的习惯是同时输出原始数量和去重后数量,再抽查前 10 条。如果数量比例不对,多半是你的清洗逻辑把不该合并的合了;如果顺序不对,多半是方案选错了。这种几秒钟的校验动作,能救回你好几个小时的问题排查时间。
第四条,无论是写代码还是写 SQL,去重之前先想清楚 “保留哪一条”。是保留最早的一条,还是最新的一条,还是字段拼起来最长的一条?没有定义清楚,任何去重都是耍流氓。这个点上翻车过太多次,现在我做任何列表去重需求,第一件事不是写代码,而是拿张纸把“输入的重复长什么样”和“输出到底要留哪条”写清楚。
结尾
写到这儿,回到“5-7列表去重”这个词本身,我的理解是:不管你在哪个技术栈里,把五到七种主流的去重思路琢磨透了,就足够覆盖你职业生涯里绝大多数列表清洗需求。而我个人的体会是,去重这事看着是技巧题,其实是定义题。方案永远可以查,但“重复的标准是什么、顺序要不要保留、数据量多大、保留哪一条”这四个问题,只有你自己能回答清楚。
最后分享一个小技巧,也是我每次列表去重后必做的一步:不要只输出去重后的列表,顺手生成一个“重复次数统计”,看看哪些重复值出现了多少次。这能帮你快速验证重复键是否定义合理,很多人做着做着就会发现,原来自己以为的“同一批重复”,其实是两批不同形态的数据。方法永远是死的,对数据的敏感度,才是干这一行真正值钱的东西。