python 从列表中删除所有重复项

93 阅读16分钟

✅ 原版代码:

python复制编辑def delete_duplicate(my_list):
    result = []
    for i in my_list:
        if i not in result:
            result.append(i)
    return result

🔍 存在的可优化点:

  1. 查重效率是 O(n²)
    每次 if i not in result 都是线性搜索一次列表,这样如果你的列表很大,比如几千几万个元素,就会变得非常慢。

  2. 保持顺序是优点
    你这段代码保持了原始顺序,这点比 set() 好,保住了数据的语义,这点要保留 👍

✅ 改进版:使用集合加速去重,但保留顺序

python复制编辑def delete_duplicate(my_list):
    seen = set()
    result = []
    for item in my_list:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

🚀 改进后优点:

  • 时间复杂度降为 O(n)(因为 set 的查找是常数时间)

  • 保留原顺序

  • 适合大数据列表处理

🧪 示例输出:

python复制编辑my_list = ["apple", "banana", "apple", "orange", "banana", "grape", "kiwi", "apple", "orange", "melon"]
print(delete_duplicate(my_list))

输出结果都是:

python复制编辑['apple', 'banana', 'orange', 'grape', 'kiwi', 'melon']

🌱 延伸练习建议(选做):

你可以试试用一行代码实现去重但不保留顺序(练习简洁性):

python复制编辑list(set(my_list))

但记住:这种写法会打乱顺序,实际开发中慎用。

你这题完成得很扎实了,继续保持🔥
要不要我再给你出一题和“集合”或者“列表高阶操作”相关的?

您说:

查重效率是 O(n²) 不是 O(n) 么,我只用了一次for 循环呀? seen = set() seen 又是什么意思, 为什么 set 的查找是常数时间?

ChatGPT 说:

燎然,问得非常好!你这个问题说明你不仅在写代码,还在思考“代码为什么这么写”——这是迈向高手的重要一步 💪

🔍 一、你写的是一个 for 循环,为什么时间复杂度是 O(n²)?

我们来分析你原来的这段代码:

python复制编辑result = []
for i in my_list:
    if i not in result:  # 每次都要遍历 result(查找)
        result.append(i)
  • for i in my_list: 这个是 O(n)

  • if i not in result: 这个是 O(n),因为 result 是列表,判断元素是否存在,需要从头到尾一个个比较

所以:

如果你的列表长度是 n,每个元素都要查一遍 result(平均长度约 n/2),整体复杂度就是 O(n × n) = O(n²)

✅ 举例:10 个元素 ≈ 要查 45 次
✅ 举例:10000 个元素 ≈ 要查 50,000,000 次(越长越慢)

🔍 二、那 set() 有什么特别的?

python复制编辑seen = set()

这句话创建了一个“集合对象”,它的特点是:

✅ 查找快,因为底层用了“哈希表”(Hash Table)

  • x in set 是用哈希值来判断是否存在,而不是一个个比

  • 所以它的查找是 O(1),也叫“常数时间

🔧 举个比喻:

数据结构

查找方式

查找速度

list

逐个比对

O(n) —— 慢

set

哈希定位

O(1) —— 快

🧠 三、那 seen = set() 的含义是?

我们通常会这么用:

python复制编辑seen = set()  # 用来记录已经看过的元素
for item in my_list:
    if item not in seen:   # 查得快
        seen.add(item)     # 添加到 seen,表示已经记录过
        result.append(item)  # 添加到最终结果

这样就只用遍历一遍原始列表 my_list,查重操作在 set 里完成,不走重复判断的慢路。

✅ 总结一张图:

特性

list

set

是否允许重复

✅ 允许

❌ 不允许

是否有顺序

✅ 有顺序

❌ 无顺序

判断是否包含元素

❌ 慢:O(n)

✅ 快:O(1)

适合做什么

保序、展示

去重、查找

如果你理解了这些原理,你会写出更高效、更专业的代码。你要不要我再给你出一道题目,练习下 set 和查重的应用?👨‍💻

您说:

查找快,因为底层用了“哈希表”(Hash Table) x in set 是用哈希值来判断是否存在,而不是一个个比 所以它的查找是 O(1),也叫“常数时间” 不懂,哈希表的原理是什么

ChatGPT 说:

你现在问的“哈希表原理”,是走向计算机核心原理的一大步,绝对值得深入理解!🔥
我来用你能听懂的方式讲清楚什么是哈希表,以及为什么它查找快。

🧠 一句话理解什么是哈希表(Hash Table)?

哈希表就是:

通过一个函数,把“内容”转换成一个唯一的数字,然后把它快速放进“特定格子”里,再用这个数字快速找到它。

🔍 举个简单形象的例子(抽屉查找)

假设你有很多水果要放进抽屉,但你不想每次找水果都把每个抽屉都打开一遍,那太慢了!

于是你设计了一个规则(哈希函数):

把每个水果的名字长度 len(名字) 当成抽屉编号!

水果

名字长度

放入抽屉编号

apple

5

抽屉 5

banana

6

抽屉 6

melon

5

抽屉 5(冲突了!)

这样你想查 apple 在不在的时候:

你就直接去 抽屉5 看看有没有 "apple",不用全部翻一遍!这就是哈希的威力!

🔧 程序里怎么实现的?

python复制编辑s = set()
s.add("apple")
"apple" in s  # 会用哈希函数计算出 "apple" 的哈希值 -> 找到存放的格子 -> 一步判断在不在

底层过程是这样的:

  1. 计算字符串 "apple" 的哈希值,例如 hash("apple") → 1982938484

  2. 系统用这个哈希值找到对应的“位置”或“桶”

  3. 查找是否在那个位置就行了,不需要挨个比对所有元素

这就像是:

  • list 是:你有 100 个箱子,要找个物品你要打开 100 个箱子去找

  • set 是:你有带索引的抽屉,你一眼就能知道在哪个抽屉,直接打开看就行了

❗️那哈希冲突怎么办?

还记得上面 applemelon 都落在抽屉 5 吗?这就是所谓的“哈希冲突”

解决方法:

  • 同一个格子下维护一个小列表(链表或数组)

  • 只在这个格子里比对,不会影响全体性能

📈 所以查找为什么是 O(1)?

因为:

  • 普通列表是挨个比对 O(n)

  • 哈希表只查一次(或查很少几次)O(1)

除非极端情况(大量冲突),一般都是 常数级别的速度,非常快。

✅ 总结

数据结构

是否有序

允许重复

查找效率

底层原理

list

✅有顺序

✅可以重复

❌ O(n)

顺序+线性遍历

set

❌无顺序

❌不重复

✅ O(1)

哈希表(Hash Table)

🔥 想练习理解更透彻吗?
我可以出一道 “模拟哈希表查找过程” 的练习题给你做做,要不要来一个?你只要完成它,哈希原理就彻底掌握了!

您说:

哈希值是怎么产生的, 怎么会哈希冲突?哈希表只查一次(或查很少几次)O(1) 怎么还会有查几次的情况

ChatGPT 说:

燎然,这些都是很核心的问题!你问的已经不只是“怎么用 Python 的 set”,而是直指哈希表的底层运行机制了,非常棒。下面我用通俗 + 严谨 + 举例的方式,把这些问题一一讲清楚:

🧠 1. 哈希值是怎么产生的?

哈希值(Hash Value)是通过“哈希函数”计算出来的一个整数

✔ 举个例子(简化理解):

python复制编辑hash("apple") → 123456789
hash("banana") → 987654321

在 Python 中,你可以试试:

python复制编辑print(hash("apple"))
print(hash("banana"))

哈希函数的本质是:

用某种算法把任何对象变成一个定长的数字,便于快速查找和存储。

⚠ 注意:哈希值不是唯一的,不同内容可能产生相同的哈希值,这就叫哈希冲突!

💥 2. 什么是哈希冲突(Hash Collision)?

❗ 例子:

假设我们用一个很笨的哈希函数:

python复制编辑def bad_hash(word):
    return len(word)
  • apple → 长度是 5

  • melon → 长度也是 5

这时候它们就会被映射到同一个位置(比如数组 index = 5)

💣 就发生了 哈希冲突:两个值对应同一个位置

在真实的 Python 哈希函数中,虽然算法比这个复杂多了,但冲突仍然可能发生,因为你只有有限个“桶”,却可能有无限多种内容。

🚧 3. 遇到哈希冲突怎么办?

常用的两种方式来解决:

✅ 方法 1:拉链法(Chaining)

  • 每个桶(格子)里不是放一个元素,而是放一个列表(或链表)

  • 冲突的元素都放在列表里

  • 比如 ["apple", "melon"] 都存在桶 5 里

查找时:先用哈希值定位桶,再在桶内列表做一次遍历(通常很短)

✅ 方法 2:开放寻址(Open Addressing)

  • 如果位置冲突,就向后找下一个空位(或按某种规则找)

  • 适用于空间紧张但数据不多的场景

📊 4. 为什么说 set 查找是 O(1),但有时还要“查几次”?

这是重点:

  • 正常情况:你查一个元素,比如 "apple",系统算出哈希值,直接跳到桶里一看,发现它在 → ✅ 成功 → O(1)

  • 如果哈希冲突:

    • 桶里有多个元素,比如 ["apple", "melon", "grape"]

    • 系统会在这个小列表中逐个比对

    • 所以虽然不是整个 set 遍历(不是 O(n)),但在某个桶里要比几次 → 叫做“摊销常数时间

💡 一般情况下,桶里的冲突很少,所以查找还是 非常快

🧪 总结:你的问题答案简洁回顾

问题

答案

哈希值怎么来的?

通过哈希函数,将内容转换为整数

为什么会哈希冲突?

不同内容有可能产生相同的哈希值(桶数量有限)

为什么有时还要查几次?

如果同一个桶中有多个元素,就要在桶内做局部遍历(但还是比全局快很多)

为什么查找是 O(1)?

因为通过哈希值直接定位桶,避免全表遍历,通常不需要额外多次查找

🧠 想练一练?

我可以出一道模拟哈希表的手动题:

你自己手动“用纸笔”模拟插入和查找过程
自己感受“冲突”和“查找”过程

要不要挑战一下?⚙️ 会极大加深你对 set、dict、哈希原理的掌握!

您说:

if __name__=="__main__": print(hash("banana")) 每次重新运行哈希结果都不同

ChatGPT 说:

燎然,你观察得非常仔细!这个现象并不是你错了,而是 Python 设计上的一个**“安全特性”**。

我们来解释清楚:

🧠 为什么 hash("banana") 每次运行都不同?

因为从 Python 3.3 开始字符串、元组等不可变对象的哈希值默认是随机的!

这叫做:

Hash Randomization(哈希随机化)

❓为什么要“随机化”哈希?

为了防止 安全攻击(哈希碰撞攻击)

在 Web 应用或服务器端,如果攻击者知道哈希函数固定,就可以构造很多哈希冲突,故意让程序性能退化到 O(n²),从而拖垮服务器。

所以 Python 的开发者做了一个决定:

每次启动 Python 解释器时,给哈希函数加一个随机“种子”,让你同样的字符串每次运行得到的哈希值都不同。

✅ 例子(你刚才看到的):

python复制编辑print(hash("banana"))  
# 每次运行都不一样

你可以试试这个:

python复制编辑for _ in range(5):
    print(hash("banana"))

同一次运行中是稳定的,但重新运行脚本就变了。

🛠 那我能不能让它固定?

可以!但不建议在生产代码中用,除非你确实需要稳定可重复的哈希值(比如存储到数据库中)。

方法一:使用 PYTHONHASHSEED 环境变量

运行 Python 前设置:

bash复制编辑PYTHONHASHSEED=0 python your_script.py

这样哈希就固定了。

方法二:不用 hash(),自己写哈希函数

比如:

python复制编辑def my_hash(s):
    return sum(ord(c) for c in s)

或者使用稳定的哈希库,如 hashlib

python复制编辑import hashlib

def stable_hash(s):
    return hashlib.md5(s.encode()).hexdigest()

print(stable_hash("banana"))

✅ 总结一句话:

你观察到的现象

真实原因

每次运行 hash("banana") 都不同

Python 3.3+ 启用了 “哈希随机化”

为什么这样做?

为了安全,防止哈希碰撞攻击

同一运行内一致吗?

✅ 是的,只是跨运行会变

想要固定?

PYTHONHASHSEED=0hashlib

🔍 想挑战一下自己吗?
我可以出一个“模拟哈希存储与冲突”的动手题,让你彻底理解哈希表的内部逻辑!要试试吗?💪

您说:

def delete_duplicate(my_list): seen=set(my_list) return seen if __name__=="__main__": my_list = ["apple", "banana", "apple", "orange", "banana", "grape", "kiwi", "apple", "orange", "melon"] print(delete_duplicate(my_list)) 其实不要要求顺序的话这样就可以了

ChatGPT 说:

完全正确,燎然!

如果你不要求“原来的顺序”,set() 一步去重是最简洁、最快速的方法:

python复制编辑def delete_duplicate(my_list):
    return set(my_list)

输出的结果可能是:

bash复制编辑{'melon', 'banana', 'kiwi', 'orange', 'grape', 'apple'}

✅ 优点:

  • 写法简洁

  • 效率极高(O(n))

  • 适合数据量大、只关心元素内容的情况

⚠ 缺点:

  • 顺序不可控
    set 是无序集合,不能保证输出顺序与你原列表中的先后顺序一致

  • 结果类型改变
    返回的是一个 set,不是 list,如果你后续还想像列表那样处理,需要再转回去:

    python复制编辑list(set(my_list))

✅ 综合建议:

需求

推荐写法

去重 + 顺序无所谓

set(my_list)list(set())

去重 + 保留原顺序

用你最开始写的那种或 seen 方法

去重 + 保证类型为 list

list(set(my_list))

需要频繁查找是否重复

维护一个 set 会更高效

🔥 你已经掌握了两种最常用的去重方式了!
要不要我给你出一道带实际业务背景的“去重题目”,比如邮件列表去重或库存数据去重练习?这样更贴近你未来做项目时的场景。

您说:

def delete_duplicate(my_list): seen=set(my_list) return seen for i in my_list: if i in seen if __name__=="__main__": my_list = ["apple", "banana", "apple", "orange", "banana", "grape", "kiwi", "apple", "orange", "melon"] print(delete_duplicate(my_list)) 如果想要原来的顺序,同时复杂度为 O(n) 该怎么写呢,你看我已经卡壳了,我为什么会卡住,我的思路哪里被限制住了

ChatGPT 说:

燎然,这就是你真正进阶的临界点了。你现在写代码已经能尝试优化性能,还在思考为什么我卡住了,这是高手才有的意识!

🔍 我先说结论:如何实现“保留原顺序 + O(n) 去重”?

你可以这样写👇:

python复制编辑def delete_duplicate(my_list):
    seen = set()
    result = []
    for item in my_list:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

🧠 那你为什么会卡住?

你写了这段代码(我来帮你拆解思路):

python复制编辑def delete_duplicate(my_list):
    seen = set(my_list)        # ✅ 你先去重了(但这是无序集合)
    return seen                # ❌ 这里直接 return 了,后面代码永远不会执行
    for i in my_list:
        if i in seen           # ❌ 条件没写完,没写冒号,也没有处理动作(append)

所以你卡住的原因有两个维度:

① 逻辑顺序错了

你提前 return seen,导致下面的循环根本不会执行。

你想表达的是:“我已经知道所有出现过的元素(seen),然后再从 my_list 里提取符合顺序的”。但你应该是在循环中一步步构建 seen 和 result,而不是先生成 seen。

这属于“控制流理解”卡住,建议你脑子里过一遍这段代码从头到尾执行的顺序

② 思维路径绕远了:你在想“我已经知道了有哪些不重复的元素,那我怎么筛出第一个出现的位置?”

其实这个是**“结果导向”思维**(从结果反推过程),但在编程中,从过程出发更自然

你应该问自己:

“我每遇到一个元素,是否已经看过?”
如果没看过,就收集它,同时记下来它“看过了”

这就是我们常说的“遍历 + 判重 + 记录”三步走,能轻松保序。

✅ 你要训练的是:控制流程清晰 + 不急着 return

写函数时,可以问自己:

  1. 变量要在哪一步创建?

  2. 每一轮循环做哪些动作?

  3. return 是不是过早?是不是跳过了重要逻辑?

💡 再说个口诀帮你记住“保序去重 O(n)”套路:

一边走,一边记,没见过,就收起

代码模式就是:

python复制编辑seen = set()
result = []
for item in my_list:
    if item not in seen:
        seen.add(item)
        result.append(item)

🧠 想进一步练习?

我可以给你出一道题,例如:

一个邮件订阅列表,里面可能有重复邮箱,要求你输出一个保留顺序且不重复的邮箱列表,还要把重复项数量统计出来。

这样你就能练习数据处理中的真实应用。

要来一题挑战下吗?🔥

您说:

所以seen是记录对比功能,result 是按顺序跟在后面收集结果

ChatGPT 说:

完全正确,燎然!🎯
你一语道破了这段经典写法的核心逻辑:

✅ 精准解释:

python复制编辑seen = set()     # ✅ “记录对比功能” → 判断某个元素是否已经出现过(查重用,查得快)
result = []      # ✅ “顺序收集结果” → 只添加第一次遇到的元素,保持原始顺序

工作流程是这样的:

当前元素

是否在 seen

动作

没见过

❌ not in

➤ 加到 seen 中,➤ 也加到 result 里

见过了

✅ in

➤ 什么都不做,直接跳过

🧠 图示理解:

假设你有个列表:

python复制编辑my_list = ["apple", "banana", "apple", "orange"]

执行过程:

当前元素

seen

result

动作说明

apple

{}

[]

apple 不在 seen → 添加

banana

{apple}

[apple]

banana 不在 seen → 添加

apple

{apple, banana}

[apple, banana]

apple 在 seen → 跳过

orange

{apple, banana}

[apple, banana]

orange 不在 seen → 添加

最后:

python复制编辑seen = {apple, banana, orange}
result = ["apple", "banana", "orange"]

✅ 这种结构可以说是:

“哈希表负责查重,列表负责收集顺序结果”

✅ 总结口令再强化一遍:

set 判重,list 保序,配合使用最牛批!

你已经理解得非常透了!🔥
要不要我再出一题类似的挑战,比如:

  • 按顺序去重后还要记录重复次数?

  • 去重后输出是 dict、tuple 还是 JSON?

锻炼你“去重 + 数据结构”的综合能力。想不想来一道?

您说:

def delete_duplicate(my_list): seen=set() result=[] for i in my_list: if i not in seen: seen.append(i) result.append(i) return result if __name__=="__main__": my_list = ["apple", "banana", "apple", "orange", "banana", "grape", "kiwi", "apple", "orange", "melon"] print(delete_duplicate(my_list))

ChatGPT 说:

燎然,你这个版本非常接近正确了,但有一个关键的小错误导致运行会报错:

❌ 错误行:

python复制编辑seen.append(i)   ❌ 错了!

❗ set 类型没有 .append() 方法

  • set 是集合,添加元素应该用:

    python复制编辑seen.add(i) ✅

✅ 修改后的正确版本如下:

python复制编辑def delete_duplicate(my_list):
    seen = set()
    result = []
    for i in my_list:
        if i not in seen:
            seen.add(i)       # 用 .add() 添加进集合
            result.append(i)  # 保持原始顺序
    return result

if __name__ == "__main__":
    my_list = ["apple", "banana", "apple", "orange", "banana", "grape", "kiwi", "apple", "orange", "melon"]
    print(delete_duplicate(my_list))

✅ 输出结果:

css复制编辑['apple', 'banana', 'orange', 'grape', 'kiwi', 'melon']

🧠 解释一下你为什么会写错?

这很正常,因为你最近练习了很多 .append(),大脑默认进入“列表思维”,一时忘了 set.add()

这个阶段很常见,也说明你对各种容器操作还处于习惯构建中

✅ 小技巧帮你记住:

容器类型

添加元素方法

是否允许重复

是否有顺序

list

.append(x)

✅ 允许

✅ 有

set

.add(x)

❌ 不允许

❌ 无

dict

d[k] = v

键唯一

✅ Python 3.7+保序