上篇把Python基础语法过了一遍。今天深入聊聊数据结构——list、dict、tuple、set这四个内置容器的选型。
为什么要专门聊选型?因为在C++里,你选vector还是map,考虑的是底层实现——连续内存还是红黑树。Python里也一样,只是关注点不太一样。
先说个面试场景。
面试官问:"假设你要存一批传感器数据,每个数据有ID、时间戳和数值,你会用什么数据结构?"
有个候选人想都没想:"用list。"
面试官追问:"如果要根据ID快速查找某条数据呢?"
候选人:"遍历list,找ID匹配的。"
面试官:"如果有一万条数据呢?"
候选人沉默了。
这就是数据结构选型的重要性。选错了容器,算法复杂度可能差几个数量级。
list:最通用的容器
Python的list对应C++的vector,底层是动态数组。它的特点是:有序、可变、支持索引和切片。
适合的场景:需要保持插入顺序、需要按索引访问、数据量不大或者操作以遍历为主。
在机器人开发中,list最常见的用途是存储传感器读数序列、轨迹点列表、日志记录等。
# 存储最近10帧的激光雷达距离
recent_distances = []
for scan in lidar_scans:
recent_distances.append(scan.avg_distance)
if len(recent_distances) > 10:
recent_distances.pop(0)
注意:list的头部插入和删除是O(n)的,因为后面的元素都要移动。如果你需要频繁的头部操作,应该用collections.deque,它是双端链表实现,两端操作都是O(1)。
from collections import deque
buffer = deque(maxlen=100) # 固定长度的缓冲区
buffer.appendleft(new_data) # O(1)
这个在机器人数据处理中特别有用——滑动窗口、FIFO缓冲区,用deque比list高效得多。
dict:键值查找的利器
dict是Python里最重要的数据结构之一,底层是哈希表,查找、插入、删除平均都是O(1)。
适合的场景:需要根据键快速查找、数据有明确的键值关系、不需要保持顺序(Python 3.7+的dict保持插入顺序,但别依赖这个特性做算法设计)。
# 传感器配置
sensor_params = {
"lidar": {"max_range": 30.0, "min_range": 0.1},
"camera": {"width": 640, "height": 480},
"imu": {"sample_rate": 200}
}
# O(1)查找
lidar_max_range = sensor_params["lidar"]["max_range"]
面试中常问的一个点:dict的key必须是不可变类型(immutable)。字符串、数字、tuple都可以做key,但list不行。因为dict底层是哈希表,key的哈希值必须稳定。
还有一个容易忽略的细节:dict.get()和直接用[]访问的区别。d[key]在key不存在时会抛KeyError,d.get(key, default)会返回默认值。在不确定key是否存在的场景下,用get更安全:
# 不安全
max_range = sensor_params["lidar_2"]["max_range"] # KeyError!
# 安全
max_range = sensor_params.get("lidar_2", {}).get("max_range", 30.0)
tuple:不可变的价值
tuple看起来就是一个不可变的list,好像没什么特别的。但它的不可变性带来了两个重要特性。
第一,可以做dict的key。比如用坐标(x, y)做键:
grid_map = {
(0, 0): "free",
(0, 1): "occupied",
(1, 0): "unknown"
}
第二,函数返回多个值时,实际上返回的就是tuple。Python的解包语法让多返回值用起来很自然:
def get_pose():
return 1.0, 2.0, 0.5 # x, y, theta
x, y, theta = get_pose()
还有一个性能上的优势:tuple比list占用更少的内存,创建速度也更快。如果你的数据确实不需要修改,用tuple是更好的选择。Python内部还会缓存小的tuple对象,进一步减少内存分配。
set:去重和集合运算
set底层也是哈希表,和dict类似,但只存key不存value。查找O(1),自动去重。
适合的场景:成员检查(比list快得多)、去重、集合运算(交并差)。
# 成员检查
known_objects = {"cup", "bottle", "keyboard"}
if detected_name in known_objects:
grasp(detected_name)
# 去重
unique_ids = list(set(all_ids))
# 集合运算
detected = {"cup", "bottle", "phone"}
known = {"cup", "bottle", "keyboard"}
new_objects = detected - known # {"phone"}
common = detected & known # {"cup", "bottle"}
在机器人开发中,set常用于:已知物体的类别集合、已探索的区域ID集合、需要排除的障碍物ID等。
顺便提一下collections模块里的两个实用工具。defaultdict——访问不存在的key时自动创建默认值,不用每次判断key是否存在:
from collections import defaultdict
sensor_readings = defaultdict(list)
sensor_readings["lidar"].append(3.5) # key不存在时自动创建空list
Counter——统计元素出现次数,做数据分析时特别好用:
from collections import Counter
labels = ["cup", "bottle", "cup", "phone", "cup"]
counts = Counter(labels) # {"cup": 3, "bottle": 1, "phone": 1}
选型决策树
面试的时候,面对一个数据存储的需求,可以按这个思路选:
需要键值查找?用dict。需要去重或成员检查?用set。数据不需要修改?用tuple。其他情况?用list。需要频繁的头部插入删除?用deque。
这个决策过程如果能有条理地讲出来,面试官会觉得你的基础很扎实。另外补充一点,Python 3.6之后dict的插入顺序保持已经是语言规范的一部分了,不再是实现细节,可以放心依赖这个特性。
给正在准备面试的你
数据结构选型在Python面试中出现频率很高,因为它直接反映你对语言特性的理解深度。建议你把这四个容器的时间复杂度、适用场景、常见陷阱都理清楚。
面试中回答数据结构选型问题时,关键是展示你的决策过程。比如"为什么用dict而不是list来存储物体识别结果"——因为需要按类别名称快速查找,dict的O(1)查找比list的O(n)快得多。"为什么用set管理已探索区域"——因为需要频繁检查某个区域是否已经访问过,set的O(1)成员检查是最佳选择。这种有理有据的回答比单纯说"我用dict"强太多。
另外补充一点,Python 3.6之后dict的插入顺序保持已经是语言规范的一部分了,不再是实现细节,可以放心依赖这个特性。
建议你动手写一个小项目来练习选型,比如用dict做一个物体识别的结果缓存,用set管理已探索的区域ID,用deque处理传感器数据的滑动窗口。实际用过之后,对各个容器的优缺点会有更深的理解。
下篇聊Python的面向对象——和C++的对比理解。Python的类和C++的类有很多不同,搞清楚这些差异,面试的时候才能答得准确。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第67篇 Python基础语法速览——给C++程序员的快速入门 下一篇预告:第69篇 Python面向对象——和C++的对比理解
有任何问题欢迎评论区留言,我会尽量回复。


被折叠的 条评论
为什么被折叠?



