MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python集合与字典的比较

2021-12-257.9k 阅读

Python 集合与字典的比较

一、数据结构基础

在 Python 中,集合(set)和字典(dictionary)都是非常重要的数据结构,它们有着各自独特的特点和应用场景。理解这两种数据结构的本质差异,对于编写高效、准确的 Python 代码至关重要。

  1. 集合的定义与特点 集合是一个无序的、不包含重复元素的数据集合。在 Python 中,可以使用花括号 {} 或者 set() 函数来创建集合。例如:
my_set1 = {1, 2, 3}
my_set2 = set([4, 5, 6])

集合的主要特点如下: - 无序性:集合中的元素没有固定的顺序,每次迭代集合时,元素的顺序可能不同。 - 唯一性:集合中的元素不能重复,如果尝试添加已存在的元素,集合不会发生变化。 - 可变性:集合是可变的数据结构,可以添加或删除元素。

  1. 字典的定义与特点 字典是一种无序的键值对(key - value)集合。在 Python 中,使用花括号 {} 来创建字典,每个键值对之间用逗号分隔。例如:
my_dict = {'name': 'John', 'age': 30, 'city': 'New York'}

字典的主要特点如下: - 无序性:与集合类似,字典中的键值对没有固定顺序。 - 键的唯一性:字典中的键必须是唯一的,如果试图使用相同的键来创建字典,后面的键值对会覆盖前面的。 - 可变性:字典是可变的数据结构,可以添加、删除或修改键值对。

二、存储与实现原理

  1. 集合的存储原理 集合在 Python 内部是基于哈希表(hash table)实现的。哈希表是一种以键值对形式存储数据的数据结构,通过哈希函数将元素映射到特定的位置。对于集合来说,每个元素本身就是键,通过计算元素的哈希值来确定其在哈希表中的存储位置。

由于哈希表的特性,集合在查找、插入和删除操作上具有平均 $O(1)$ 的时间复杂度。这意味着,在大多数情况下,这些操作的执行时间不会随着集合大小的显著增加而显著增加。然而,在哈希冲突(不同元素计算出相同的哈希值)的情况下,查找、插入和删除操作的时间复杂度可能会退化到 $O(n)$,其中 $n$ 是集合中元素的数量。

例如,考虑以下代码:

my_set = set()
my_set.add(1)
my_set.add(2)
print(1 in my_set)  

在这个例子中,add 操作将元素添加到集合中,in 操作检查元素是否存在于集合中。这两个操作的平均时间复杂度都是 $O(1)$。

  1. 字典的存储原理 字典同样是基于哈希表实现的。与集合不同的是,字典存储的是键值对,每个键通过哈希函数映射到哈希表中的一个位置,对应的值存储在该位置。

字典在查找、插入和删除操作上也具有平均 $O(1)$ 的时间复杂度,原因与集合类似,都是基于哈希表的快速查找能力。然而,与集合一样,哈希冲突可能会导致时间复杂度退化。

例如:

my_dict = {'a': 1, 'b': 2}
print(my_dict['a'])  
my_dict['c'] = 3  

在这个例子中,通过键 'a' 查找值的操作以及插入新键值对 ('c', 3) 的操作,平均时间复杂度都是 $O(1)$。

三、数据访问与操作

  1. 集合的数据访问与操作
    • 访问元素:集合本身是无序的,不能像列表或元组那样通过索引来访问单个元素。但是,可以通过迭代集合来访问所有元素。例如:
my_set = {1, 2, 3}
for element in my_set:
    print(element)
- **添加元素**:使用 `add()` 方法可以向集合中添加单个元素,如果元素已存在,集合不会发生变化。例如:
my_set = {1, 2}
my_set.add(3)
print(my_set)  

还可以使用 update() 方法来添加多个元素,update() 方法接受一个可迭代对象作为参数,如列表、元组或另一个集合。例如:

my_set = {1, 2}
my_set.update([3, 4])
print(my_set)  
- **删除元素**:使用 `remove()` 方法可以删除集合中的指定元素,如果元素不存在,会引发 `KeyError` 异常。例如:
my_set = {1, 2, 3}
my_set.remove(2)
print(my_set)  

使用 discard() 方法也可以删除元素,但如果元素不存在,不会引发异常。例如:

my_set = {1, 2, 3}
my_set.discard(4)
print(my_set)  

pop() 方法用于随机删除并返回集合中的一个元素,如果集合为空,会引发 KeyError 异常。例如:

my_set = {1, 2, 3}
element = my_set.pop()
print(element)  
print(my_set)  
  1. 字典的数据访问与操作
    • 访问值:通过键来访问字典中的值,如果键不存在,会引发 KeyError 异常。例如:
my_dict = {'name': 'John', 'age': 30}
print(my_dict['name'])  

可以使用 get() 方法来访问值,如果键不存在,get() 方法不会引发异常,而是返回 None 或指定的默认值。例如:

my_dict = {'name': 'John', 'age': 30}
print(my_dict.get('city'))  
print(my_dict.get('city', 'Unknown'))  
- **添加或修改键值对**:通过赋值语句可以添加新的键值对,如果键已存在,则会修改对应的值。例如:
my_dict = {'name': 'John', 'age': 30}
my_dict['city'] = 'New York'  
print(my_dict)  
my_dict['age'] = 31  
print(my_dict)  
- **删除键值对**:使用 `del` 语句可以删除指定键的键值对,如果键不存在,会引发 `KeyError` 异常。例如:
my_dict = {'name': 'John', 'age': 30}
del my_dict['age']
print(my_dict)  

pop() 方法用于删除指定键的键值对,并返回对应的值,如果键不存在,会引发 KeyError 异常。例如:

my_dict = {'name': 'John', 'age': 30}
age = my_dict.pop('age')
print(age)  
print(my_dict)  

popitem() 方法用于随机删除并返回字典中的一个键值对,以元组形式返回。如果字典为空,会引发 KeyError 异常。例如:

my_dict = {'name': 'John', 'age': 30}
item = my_dict.popitem()
print(item)  
print(my_dict)  

四、应用场景

  1. 集合的应用场景
    • 去重:集合的唯一性特点使其非常适合用于数据去重。例如,有一个包含重复元素的列表,要获取其中不重复的元素,可以将列表转换为集合。
my_list = [1, 2, 2, 3, 4, 4]
unique_set = set(my_list)
print(unique_set)  
- **成员检测**:由于集合的查找操作平均时间复杂度为 $O(1)$,在需要频繁检查某个元素是否存在的场景下,集合是一个很好的选择。例如,判断一个单词是否在某个单词列表中。
word_list = ['apple', 'banana', 'cherry']
word_set = set(word_list)
print('apple' in word_set)  
- **集合运算**:集合支持多种数学集合运算,如并集、交集、差集等。这些运算在处理数据集合关系时非常有用。
set1 = {1, 2, 3}
set2 = {3, 4, 5}
union_set = set1.union(set2)  
intersection_set = set1.intersection(set2)  
difference_set = set1.difference(set2)  
print(union_set)  
print(intersection_set)  
print(difference_set)  
  1. 字典的应用场景
    • 数据映射:字典最常见的应用场景是数据映射,即通过一个键来查找对应的值。例如,存储学生的成绩,以学生姓名作为键,成绩作为值。
student_scores = {'Alice': 85, 'Bob': 90, 'Charlie': 78}
print(student_scores['Alice'])  
- **配置文件**:字典可以用于表示配置文件中的设置。每个设置项可以作为键,对应的值则是设置的具体内容。
config = {'server': '192.168.1.1', 'port': 8080, 'debug': False}
- **统计计数**:在统计某个数据集中不同元素出现的次数时,字典非常方便。例如,统计一篇文章中每个单词出现的次数。
text = "this is a sample text. this text is for testing"
word_count = {}
words = text.split()
for word in words:
    if word not in word_count:
        word_count[word] = 1
    else:
        word_count[word] += 1
print(word_count)  

五、内存占用与性能

  1. 内存占用
    • 集合:集合的内存占用主要取决于元素的数量和每个元素的大小。由于集合基于哈希表实现,需要额外的空间来存储哈希表的元数据,如哈希桶的数量、负载因子等。对于包含少量元素的集合,相对来说内存占用可能较高,但随着元素数量的增加,哈希表的空间利用率会提高。
    • 字典:字典除了存储键值对本身,也需要额外的空间来存储哈希表的相关信息。与集合相比,字典每个元素包含键和值两部分,因此在存储相同数量元素的情况下,字典通常会占用更多的内存。

例如,创建一个包含 1000 个整数的集合和一个包含 1000 个键值对(键和值都是整数)的字典,并比较它们的内存占用:

import sys

my_set = set(range(1000))
my_dict = {i: i for i in range(1000)}

print(sys.getsizeof(my_set))  
print(sys.getsizeof(my_dict))  

在实际运行中,可以看到字典的内存占用通常会比集合高。

  1. 性能
    • 查找性能:在平均情况下,集合和字典的查找操作(判断元素是否存在于集合中,或通过键查找值)都具有 $O(1)$ 的时间复杂度。然而,当出现哈希冲突时,查找性能会下降。在哈希冲突较少的情况下,两者的查找性能都非常高效。
    • 插入和删除性能:同样,在平均情况下,集合的 addremove 操作以及字典的插入、删除操作都具有 $O(1)$ 的时间复杂度。但哈希冲突可能会导致性能退化。此外,字典在插入新键值对时,如果哈希表达到一定的负载因子,可能会触发哈希表的扩容操作,这会导致额外的性能开销。

为了比较插入性能,可以进行如下测试:

import timeit

setup = "my_set = set(); my_dict = {}"
stmt_set = "my_set.add(1)"
stmt_dict = "my_dict[1] = 1"

set_time = timeit.timeit(stmt=stmt_set, setup=setup, number=100000)
dict_time = timeit.timeit(stmt=stmt_dict, setup=setup, number=100000)

print(f"Set insertion time: {set_time}")
print(f"Dictionary insertion time: {dict_time}")

在实际测试中,可能会发现两者的插入时间在不同情况下有所差异,但在正常情况下,性能都较为接近。

六、可迭代性与遍历

  1. 集合的可迭代性与遍历 集合是可迭代的对象,可以使用 for 循环来遍历集合中的元素。由于集合的无序性,每次遍历的顺序可能不同。例如:
my_set = {1, 2, 3}
for element in my_set:
    print(element)

也可以使用 iter() 函数获取集合的迭代器,然后通过 next() 函数逐个获取元素。例如:

my_set = {1, 2, 3}
my_iter = iter(my_set)
print(next(my_iter))  
print(next(my_iter))  
print(next(my_iter))  
  1. 字典的可迭代性与遍历 字典同样是可迭代的对象。默认情况下,遍历字典会返回键。例如:
my_dict = {'name': 'John', 'age': 30}
for key in my_dict:
    print(key)

如果要同时获取键和值,可以使用 items() 方法。例如:

my_dict = {'name': 'John', 'age': 30}
for key, value in my_dict.items():
    print(key, value)

如果只需要获取值,可以使用 values() 方法。例如:

my_dict = {'name': 'John', 'age': 30}
for value in my_dict.values():
    print(value)

七、嵌套结构

  1. 集合的嵌套结构 集合可以包含其他可哈希的对象,因此可以创建嵌套集合。例如,可以创建一个包含多个集合的集合:
set1 = {1, 2}
set2 = {3, 4}
nested_set = {set1, set2}
print(nested_set)  

需要注意的是,由于集合本身是可变的,不能直接将集合作为另一个集合的元素,除非将其转换为不可变的 frozenset。例如:

set1 = {1, 2}
frozen_set1 = frozenset(set1)
nested_set = {frozen_set1}
print(nested_set)  
  1. 字典的嵌套结构 字典可以实现非常复杂的嵌套结构。例如,可以创建一个字典,其值又是一个字典:
person = {
    'name': 'John',
    'age': 30,
    'address': {
      'street': '123 Main St',
        'city': 'Anytown',
        'zip': '12345'
    }
}
print(person['address']['city'])  

也可以创建一个字典,其值是列表,或者列表中包含字典等复杂结构。例如:

students = [
    {'name': 'Alice', 'age': 20,'scores': [85, 90]},
    {'name': 'Bob', 'age': 21,'scores': [78, 88]}
]
print(students[0]['scores'][1])  

八、与其他数据结构的交互

  1. 集合与其他数据结构的交互 集合可以与列表、元组等数据结构相互转换。例如,可以将列表转换为集合进行去重,然后再转换回列表。
my_list = [1, 2, 2, 3]
my_set = set(my_list)
unique_list = list(my_set)
print(unique_list)  

集合还可以与其他集合进行运算,如并集、交集、差集等,这些运算的结果也是集合。

  1. 字典与其他数据结构的交互 字典可以与列表结合使用,例如将字典作为列表的元素,或者将列表作为字典的值。例如:
my_dict = {'students': [{'name': 'Alice', 'age': 20}, {'name': 'Bob', 'age': 21}]}
print(my_dict['students'][0]['name'])  

字典的键和值也可以是其他数据结构,如集合、元组等。例如:

my_dict = {('a', 'b'): {1, 2, 3}}
print(my_dict[('a', 'b')])  

通过以上对 Python 集合和字典在多个方面的比较,可以更深入地理解它们的本质差异和适用场景,从而在实际编程中根据需求选择最合适的数据结构,提高代码的效率和可读性。无论是简单的数据去重、数据映射,还是复杂的嵌套结构和集合运算,集合和字典都为 Python 开发者提供了强大而灵活的工具。