English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

nu

访客
1 / ?
返回课程列表

列表的优势

列表:你的第一个数据结构

一个列表(在某些语言中称为数组)按特定顺序存储项目。你可以:

- 按位置访问任何项目:names[0] 获取第一项。O(1) — 瞬间。

- 在末尾添加项目:names.append("Zoe")O(1) — 瞬间。

- 按顺序遍历每个项目:for name in namesO(N) — 访问每个项目一次。

列表保留你放入项目的顺序。第一项保持第一。最后一项保持最后。

# A list of pets
pets = ["cat", "dog", "cat", "fish", "dog"]

print(pets[0])   # "cat"  — O(1) instant
print(pets[3])   # "fish" — O(1) instant
print(len(pets)) # 5 — duplicates kept

注意:"cat" 出现了两次。"dog" 出现了两次。列表允许重复。 它按你给定的顺序精确存储你给定的内容。

但列表有个弱点。看看当你问:"fish" 是否在这个列表中?

# Membership check — is "fish" in the list?
if "fish" in pets:    # scans ["cat", "dog", "cat", "fish"...] one by one
    print("found!")   # had to check 4 items before finding it

计算机检查 "cat" — 否。"dog" — 否。"cat" — 否。"fish" — 是!它扫描了 4 个项目才找到答案。有 1,000 个项目时,它可能会扫描全部 1,000 个。那个成员检查花费 O(N)

预测扫描成本

你有 500 个学生姓名存储在一个随机顺序的列表中。

students = ["Alex", "Maria", ..., "Zara", ...]  # 500 names
if "Zara" in students:
    print("enrolled")
要确定 "Zara" 是否在列表中,计算机最多需要检查多少个名字?最坏情况需要多少个?平均需要多少个?这个操作的大O等级是什么?请解释你的推理。

集合的工作方式有何不同

集合:为成员检查而构建

一个集合使用称为哈希的技术存储唯一项目。当你添加一个项目时,计算机计算一个哈希(一个数字指纹),告诉它正好在哪里存储该项目。

当你检查成员资格时,计算机计算相同的哈希并直接跳到正确的位置。无需扫描。

列表与集合:数据如何在内存中存储 — 列表扫描,集合跳转

# A set of pets
pets = {"cat", "dog", "cat", "fish", "dog"}

print(pets)      # {"cat", "dog", "fish"} — duplicates removed
print(len(pets)) # 3 — only unique items

注意与列表相比的三个差异:

1. 无重复。 "cat" 在输入中出现了两次,但集合只保留一份副本。

2. 无保证的顺序。 集合可能以任何方式打印项目。

3. 无索引访问。 你不能写 pets[0] — 集合没有位置。

权衡:你失去了顺序和重复。你获得了 O(1) 成员测试 — 检查项目是否存在花费相同的时间,无论集合有 5 个项目还是 500 万个。

# Membership check — is "fish" in the set?
if "fish" in pets:    # hash("fish") → jump to bucket → found
    print("found!")   # checked exactly 1 spot, not 4

集合与列表成员检查

你将 500 个学生姓名存储在一个集合中,而不是列表。

students = {"Alex", "Maria", ..., "Zara", ...}  # 500 names
if "Zara" in students:
    print("enrolled")
计算机需要检查多少个项目来确定 "Zara" 是否存在于 500 个名字的集合中?这个操作的大O等级是什么?为什么它与列表版本不同?

并排比较

操作速查表

操作成本:列表与集合 — 成员检查、添加、索引、去重

在以下情况下使用列表:

- 项目处于特定顺序(播放列表、食谱、队列)

- 按位置访问 (items[3])

- 保留重复("cat" 出现 3 次 = 3 只猫)

在以下情况下使用集合:

- 快速成员检查("我之前见过这个吗?")

- 自动去重

- 不关心顺序

当你需要顺序和快速查找时使用两者:

seen = set()     # O(1) membership checks
result = []      # preserves insertion order
for item in data:
    if item not in seen:  # O(1) check
        seen.add(item)    # O(1) add
        result.append(item)  # O(1) append
# Total: O(N) — one pass, each step O(1)

这个 "集合 + 列表" 模式为你提供两者的优点:有序结果加快速去重检测。

选择正确的结构

三个问题。对于每一个,决定:列表、集合,还是两者?

1. 你需要按用户添加歌曲的顺序存储播放列表中的歌曲。

2. 你需要快速检查用户名是否已被占用。

3. 你需要从邮件列表中删除重复的电子邮件,但保持它们注册的顺序。

对于三个问题中的每一个,你应该使用哪种数据结构(列表、集合还是两者)?为每一个解释原因。

去重问题

问题:删除重复项,保留顺序

你收到一份电子邮件注册列表。有些人注册了两次。你需要给每个人发送一封电子邮件,按他们第一次注册的顺序。

尝试 1:仅列表(缓慢)

signups = ["alice@a.com", "bob@b.com", "alice@a.com", "carol@c.com"]
unique = []
for email in signups:
    if email not in unique:  # O(N) scan of unique list
        unique.append(email)
# Works! But: O(N) check × N items = O(N²)

有 100 个注册,这做约 5,000 次检查。有 10,000 个注册:约 50,000,000 次检查。

尝试 2:集合 + 列表(快速)

signups = ["alice@a.com", "bob@b.com", "alice@a.com", "carol@c.com"]
seen = set()
unique = []
for email in signups:
    if email not in seen:   # O(1) hash lookup
        seen.add(email)     # O(1) add to set
        unique.append(email) # O(1) append to list
# O(1) check × N items = O(N)

相同结果。相同顺序。但:10,000 个注册现在需要约 10,000 次检查,而不是约 50,000,000 次。

计算加速

仅列表版本以 O(N²) 运行。集合+列表版本以 O(N) 运行。

你有 10,000 个电子邮件注册 要去重。

在 N=10,000 时,每个版本执行多少约检查?集合+列表版本快多少倍?显示你的计算。

查找共同元素

问题:查找两个列表中的项目

你有两个班级名单。你需要找到两个班级中都注册的学生。

缓慢方法:嵌套循环 O(N × M)

class_a = ["Alice", "Bob", "Carol", "Diana"]
class_b = ["Bob", "Diana", "Eve", "Frank"]
both = []
for student in class_a:        # N iterations
    if student in class_b:     # M scans each time
        both.append(student)
# O(N × M) — each student scanned against all of class_b

快速方法:将一个列表转换为集合 O(N + M)

class_a = ["Alice", "Bob", "Carol", "Diana"]
class_b_set = set(class_b)     # O(M) to build
both = []
for student in class_a:        # N iterations
    if student in class_b_set: # O(1) each time
        both.append(student)
# O(N + M) — build the set once, check N times at O(1) each

或者更简单地使用 Python 的内置集合交集:

both = set(class_a) & set(class_b)  # O(N + M)

& 操作符找到出现在两个集合中的所有项目。

重写以加速

一所学校有两个俱乐部,各有 200 个成员。这个代码找到两个俱乐部中的学生:

chess_club = [...]   # 200 names
art_club = [...]     # 200 names
both_clubs = []
for student in chess_club:
    if student in art_club:
        both_clubs.append(student)
这个代码的大O是什么?使用集合重写它以更快地运行。你的改进版本的大O是什么?解释差异。

决策框架

你的数据结构决策框架

每次你写代码存储项目集合时,问:

1. 我需要项目有顺序吗? → 列表

2. 我需要快速成员检查吗? → 集合

3. 我既需要顺序又需要快速检查吗? → 集合 + 列表一起

4. 我需要重复吗? → 列表(集合丢弃重复)

---

本课程的关键洞察: 相同的程序,解决相同的问题,只需选择正确的数据结构就可以快 1,000 倍到 1,000,000 倍。无需聪明的技巧。无需复杂的数学。只需问:我的代码最常做什么操作? 然后选择在这些操作中花费 O(1) 而不是 O(N) 的结构。

本课程涵盖了列表和集合。其他数据结构存在(字典、树、堆)解决其他问题。决策框架保持不变:理解你的操作,然后选择使它们便宜的结构。

---

想要更深入? 我们的大O课程详细涵盖增长率和扩展计算。查看 大O:多快才足够快?。我们的 unhamming 课程涵盖真实世界的缺陷(MOAD-0001),其中这个确切的列表到集合修复在生产软件中产生了 1,000 倍加速。查看 缺失的章节:算法复杂度

调试这个代码

一个学生写了这个代码来计算一本书中有多少个独特的单词:

words = book.split()           # list of all words
unique_count = 0
checked = []
for word in words:             # N words
    if word not in checked:    # scans checked list
        checked.append(word)
        unique_count += 1
print(unique_count)

这本书有 100,000 个单词。

这个代码的大O是什么?为什么它在大书上运行缓慢?使用集合重写它以 O(N) 解决相同的问题。然后解释:你能用一行集合解决这个问题吗?那会是什么样子?