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

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

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

  •  
  •   jarryli · May 1 · 519 views
    This topic created in 35 days ago, the information mentioned may be changed or developed.

    想起茴香的茴子有四种写法,孔乙己先生是高明的,他如果生在这个时代,就能用不同的思路让 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  
       May 2
    很有用,赞一个👍
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3943 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 46ms · UTC 00:10 · PVG 08:10 · LAX 17:10 · JFK 20:10
    ♥ Do have faith in what you're doing.