如果想在 V2EX 获得更好的推广效果,欢迎了解 PRO 会员机制:
https://www.v2ex.com/pro/about

如果你经常使用铜币置顶主题,持有 V2EX Solana Token 会在每日签到时获得额外铜币:
https://www.v2ex.com/solana
jarryli
V2EX  ›  推广

[一个“去重”写了 20 种方法?盘点 Python 列表去重的奇技淫巧]

  •  
  •   jarryli · 26 days ago · 491 views

    想起茴香的茴子有四种写法,孔乙己先生是高明的,他如果生在这个时代,就能用不同的思路让 AI 帮他解决问题了。

    Python 列表去重的 20 种实现方式

    列表(数组)去重是最常见的算法,非常简单,但不同实现方式背后的差异巨大。AI 时代,可以不手写代码了,但需要知道代码背后的原理,这样才能更好地指导 AI 编程。

    最简单的思路

    新建列表,遍历原列表,当原列表的元素不在新列表的,则添加到新列表中。

    def unique(data):
        # 新建 list
        new_list = []
        for item in data:
            # 原 list 中的项是否存在于新 list 中,不存在则添加。这是 O(n)操作
            if item not in new_list:
                new_list.append(item)
        return new_list
    

    这种写法最直观易懂,但每次 not in 都要遍历整个 new_list,复杂度为 O(n²)。

    如何降低复杂度呢?可以从以下角度思考:

    • 哈希集合 / 字典:把查询从 O(n) 可压到 O(1),整体 O(n)
    • 先排序:相同元素两两比较再去重,O(nlogn),但会破坏原顺序
    • 函数式 / 递归:写法上换一种风格,适用不同场景,本质仍是上面的方式

    第 1 类:基础循环(方法 1-8 )

    策略原理:遍历原数组,直接用双重循环或下标比较找出重复项。每一步判断"是否存在"都是 O(n),整体复杂度 O(n²)。

    适用场景:这里主要是展示算法原理,用于教学示例,弄懂编程原理。生产代码不建议使用。

    # 方法 1:最基础的线性查找
    def unique_v1(data):
        new_list = []
        for item in data:
            # in 在列表上是 O(n) 扫描,整体 O(n²)
            # 该元素不在新 list 则添加
            if item not in new_list:
                new_list.append(item)
        return new_list
    
    # 方法 2:用下标遍历
    def unique_v2(data):
        new_list = []
        for i in range(len(data)):
            # 与第 1 种相同,遍历方式换成 range ,复杂度不变
            if data[i] not in new_list:
                new_list.append(data[i])
        return new_list
    
    # 方法 3:列表推导式
    def unique_v3(data):
        new_list = []
        # 利用列表推导式的副作用追加元素,写法简化,本质与前面一样
        [new_list.append(i) for i in data if i not in new_list]
        return new_list
    
    # 方法 4:通过元素首次出现位置判断
    def unique_v4(data):
        new_list = []
        for i in range(len(data)):
            # data.index(x) 返回 x 在 data 里第一次出现的下标
            # 当前下标恰好等于该值时,说明该元素是首次出现,将首次出现的添加到新 list
            if i == data.index(data[i]):
                new_list.append(data[i])
        return new_list
    
    # 方法 5:原地删除(从右往左扫描)
    def unique_v5(data):
        l = len(data)
        while l > 0:
            l -= 1
            i = l
            while i > 0:
                i -= 1
                # 在 [0, l) 区间里寻找与 data[l] 相同的元素
                # 找到就删后面那个,保留前面的
                if data[i] == data[l]:
                    del data[l]
                    break
        return data
    # 修改原列表,空间 O(1)
    
    # 方法 6:原地删除(从左往右扫)
    def unique_v6(data):
        l = len(data)
        i = 0
        while i < l:
            j = i + 1
            while j < l:
                # 把 data[i] 后面所有等于它的元素删掉
                if data[i] == data[j]:
                    del data[j]
                    l -= 1   # 列表变短,长度同步更新
                else:
                    j += 1
            i += 1
        return data
    
    # 方法 7:用 try-except 替代 in
    def unique_v7(data):
        new_list = []
        for item in data:
            try:
                # index 找不到会抛 ValueError
                new_list.index(item)
            except ValueError:
                # 找不到才追加
                new_list.append(item)
        return new_list
    # 实际上不会这么使用——拿异常处理正常的控制流,性能和可读性都吃亏
    
    # 方法 8:双层循环+下标判断
    def unique_v8(data):
        new_list = []
        for i in range(len(data)):
            j = 0
            while j <= i:
                # 看 data[i] 在它之前是否出现过
                if data[i] == data[j]:
                    # 只有 j == i (前面都没遇到)时才追加
                    if i == j:
                        new_list.append(data[i])
                    break
                j += 1
        return new_list
    # 内层只跑到 i 而非 n ,比较次数约为方法 1 的一半,但渐进复杂度仍是 O(n²)
    

    第 2 类:哈希表(方法 9-12 )

    策略原理:利用 dictset 的键( Key )唯一性来记录"已经出现的元素"。哈希结构的查询是 O(1),因此整体降到 O(n)。代价是需要 O(n) 额外内存空间,且元素必须可哈希——数字、字符串、元组都可以,但 list 、dict 这类可变对象不行。

    适用场景:日常项目的首选。需要保留原顺序时尤其合适,因为一边查重一边按原序写入结果。

    # 方法 9:set 配合 list——工程最常见写法
    def unique_v9(data):
        seen = set()        # set 用于 O(1) 判重
        result = []         # list 用于保持原顺序
        for item in data:
            if item not in seen:
                seen.add(item)
                result.append(item)
        return result
    
    # 方法 10:dict.fromkeys()——最佳版本,实际使用首选
    def unique_v10(data):
        # dict 自 Python 3.7 起保持插入顺序
        # fromkeys 会自动用相同 key 覆盖,从而完成去重
        return list(dict.fromkeys(data))
    
    # 方法 11:filter + 列表,函数式风格
    def unique_v11(data):
        seen = []
        # 内部函数
        def check(item):
            # 闭包捕获 seen
            # 注意 seen 是 list ,in 仍是 O(n),整体仍是 O(n²)
            if item not in seen:
                seen.append(item)
                return True
            return False
        return list(filter(check, data))
    # 函数式风格,但不纯粹,seen 类型选错了,这里只是为了展示写法
    
    # 方法 12:filter + 字典,由 list 改为 dict ,仍然不是纯函数式
    def unique_v12(data):
        obj = {}
        def check(item):
            # 用 dict 替代上面的 list ,查询变成 O(1)
            if obj.get(item) is None:
                obj[item] = item
                return True
            return False
        return list(filter(check, data))
    

    第 3 类:集合与排序(方法 13-16 )

    策略原理:将list直接转 set,或者通过 sort() 让相同元素挨在一起再去重,从而简化查找逻辑。两种思路都不再保留原有顺序。集合方式 O(n),排序方式 O(nlogn),且要求元素可比较。

    适用场景:不关心顺序、只关心结果集合的场合,例如统计去重数量、做集合运算、把列表当作"无序集合"使用。

    # 方法 13:set 直接转列表,常见用法
    def unique_v13(data):
        # 哈希集合天然不重复
        return list(set(data))
    # 写法最短,但顺序会被打乱
    
    # 方法 14:map + filter 组合
    def unique_v14(data):
        seen = []
        def mark(item):
            # 第一次见到返回元素本身,后续返回 None
            if item not in seen:
                seen.append(item)
                return item
            return None
        # 先 map 标记,再 filter 把 None 去掉
        return list(filter(lambda x: x is not None, map(mark, data)))
    # 函数式风格,但 seen 用 list 仍是 O(n²)
    
    # 方法 15:先排序再相邻去重(从右往左删)
    def unique_v15(data):
        data.sort()  # 排序后,相同元素会聚到一起
        l = len(data)
        while l > 0:
            l -= 1
            # 相邻两两比较,相同就删后面那个
            if l > 0 and data[l] == data[l-1]:
                del data[l]
        return data
    # 复杂度由 sort 决定,O(nlogn);元素需要可比较
    
    # 方法 16:先排序再相邻去重(从左往右删)
    def unique_v16(data):
        data.sort()
        l = len(data) - 1
        i = 0
        while i < l:
            if data[i] == data[i+1]:
                del data[i]   # 删当前,i 不前进;同时长度减一
                i -= 1
                l -= 1
            i += 1
        return data
    

    第 4 类:函数式与递归(方法 17-20 )

    策略原理:用 reduce、外部库或递归换一种表达方式。reduce 配合元组累加器可以做到 O(n),但写法比直接 for 循环晦涩;递归则吃调用栈、numpy 需要库依赖。

    适用场景:numpy 适合大规模数值数据;其余几种主要用于练习函数式或递归思维,工程上一般直接用第 2 类。

    # 方法 17:reduce + 元组累加器(函数式风格但能跑到 O(n))
    import functools
    
    def unique_v17(data):
        def foo(acc, item):
            # 累加器是一个元组 (result, seen)
            # result 保留首次出现的顺序,seen 用集合实现 O(1) 判重
            result, seen = acc
            if item in seen:
                return (result, seen)
            # 这里直接修改累加器内部的 list 和 set
            # 严格的纯函数式应返回新对象 (result + [item], seen | {item})
            # 但那样每步都新建列表/集合,复杂度退回到 O(n²)
            # 在 reduce 内做"受控副作用",换取 O(n) 的性能
            seen.add(item)
            result.append(item)
            return (result, seen)
    
        # 初始累加器是空列表+空集合,最后取 [0] 即得到去重结果
        return functools.reduce(foo, data, ([], set()))[0]
    # O(n);保序;本质是用 reduce 重写了方法 9 的循环
    
    # 方法 18:调用 numpy.unique
    def unique_v18(data):
        import numpy as np
        # numpy 底层用 C 实现的排序+相邻去重
        return list(np.unique(np.array(data)))
    # O(nlogn);不保序;适合大规模数值数据
    
    # 方法 19:递归+原地删除
    def unique_v19(data, length=None):
        # 递归退出条件
        if length is None:
            length = len(data)
        if length <= 1:
            return data
    
        last_idx = length - 1
        # 看末尾元素是否在前面出现过
        is_repeat = False
        for i in range(last_idx):
            if data[i] == data[last_idx]:
                is_repeat = True
                break
    
        # 重复则删除
        if is_repeat:
            del data[last_idx]
    
        # 递归调用,处理前 length-1 项
        return unique_v19(data, length - 1)
    # 递归深度 = n ,大数据会栈溢出,仅作学习用
    
    # 方法 20:递归+拼接返回(不修改原列表)
    # 递归自后往前逐个调用,当长度为 1 时终止。与上一个递归不同,这里将不重复的项目作为结果拼接起来
    def unique_v20(data, length=None):
        if length is None:
            length = len(data)
        if length <= 1:
            return data
    
        last_idx = length - 1
        last_item = data[last_idx]
    
        is_repeat = False
        for i in range(last_idx):
            if data[i] == last_item:
                is_repeat = True
                break
    
        # 末尾元素重复就丢弃,否则拼到结果末尾
        result = [] if is_repeat else [last_item]
        # 切片 + 拼接都会产生新列表,空间开销大
        return unique_v20(data[:last_idx], length - 1) + result
    # 演示如何用递归构造结果,工程上没有实用价值
    

    这么多实现方式,如何选择?

    类别 时间复杂度 是否保序 主要场景
    基础循环 O(n²) 教学、极小规模
    哈希表 O(n) 日常项目首选
    集合 / 排序 O(n) / O(nlogn) 不在意顺序
    函数式 / 递归 视实现而定 看实现 学习、特定场景

    实际项目里怎么选

    绝大多数情况一行就够:

    # 保序、O(n)、对所有可哈希类型有效,Python 3.7+ 自带
    result = list(dict.fromkeys(data))
    

    不在意顺序:

    result = list(set(data))
    

    数据量很大且都是数值:

    import numpy as np
    result = list(np.unique(data))
    

    带逻辑干预的去重

    前面所有方法都把"重复的元素"直接丢掉。但实际工作里经常遇到这样的情况:遇到重复时不能简单丢弃,要根据某个条件做处理。比如:

    • id 去重,但要保留分数最高的那条记录
    • 去重的同时累加重复次数(即频次统计)
    • 数值在某个区间内才参与去重,区间外原样保留

    这类需求 setdict.fromkeys 都没法直接表达,需要把"判重"和"处理"两步拆开来写。

    def unique_with_rule(data, key=None, on_duplicate=None):
        """
        带逻辑干预的去重。
    
        key: 可哈希的去重键,默认拿元素本身
        on_duplicate: 遇到重复时如何处理 (旧值, 新值) -> 新的"代表值"
                      返回 None 时保持旧值不变(即等同于丢弃新值)
        """
        if key is None:
            key = lambda x: x
    
        chosen = {}     # 键 -> 当前选中的元素
        order  = []     # 记录键首次出现的顺序,保证保序
    
        for item in data:
            k = key(item)
            if k not in chosen:
                chosen[k] = item
                order.append(k)
            elif on_duplicate is not None:
                # 遇到重复时由调用方决定如何合并
                merged = on_duplicate(chosen[k], item)
                if merged is not None:
                    chosen[k] = merged
    
        return [chosen[k] for k in order]
    

    例 1 ,按 id 去重,保留分数最高的:

    students = [
        {'id': 1, 'name': '张三', 'score': 90},
        {'id': 1, 'name': '张三', 'score': 95},   # 同 id ,分数更高
        {'id': 2, 'name': '李四',   'score': 99},
    ]
    result = unique_with_rule(
        students,
        key=lambda x: x['id'],
        on_duplicate=lambda old, new: new if new['score'] > old['score'] else old,
    )
    # [{'id':1,'score':95,...}, {'id':2,'score':99,...}]
    

    例 2 ,去重的同时统计每个值的出现次数:

    from collections import Counter
    
    data = ['A', 'B', 'A', 'C', 'B', 'A']
    # Counter 本身就是"键->计数",遍历一次即可完成统计
    counts = Counter(data)
    # Python 3.7+ 起 dict / Counter 保留插入顺序,因此 keys 即首次出现顺序
    unique_keys = list(counts.keys())
    # unique_keys = ['A', 'B', 'C']
    # counts      = {'A': 3, 'B': 2, 'C': 1}
    

    例 3 ,区间过滤——只对 [0, 50] 区间内的值去重,区间外的原样保留:

    data = [5, 12, 5, 100, 12, 200]
    seen = set()
    result = []
    for x in data:
        if 0 <= x <= 50:
            # 区间内才参与去重
            if x in seen:
                continue
            seen.add(x)
        # 区间外或首次出现,都保留
        result.append(x)
    # [5, 12, 100, 200]
    

    这三个例子是同一种思路:把判重与业务规则分开。判重用哈希结构保证 O(n),规则部分留给回调或显式分支处理,这样既不丢性能,又能容纳各种业务变化。


    AI 时代,程序员不一定要手写代码,但一定要懂得编程思路,这样才能更好地指导 AI 。

    更多算法

    不同语言算法实现:https://github.com/microwind/algorithms

    AI 编程知识库:https://microwind.github.io

    1 replies    2026-05-02 11:28:25 +08:00
    Jinpen
        1
    Jinpen  
       25 days ago
    很有用,赞一个👍
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3328 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 51ms · UTC 10:47 · PVG 18:47 · LAX 03:47 · JFK 06:47
    ♥ Do have faith in what you're doing.