上篇把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++的对比理解

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐