尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python数据结构:栈、队列、set、map、list用法与避坑
写代码久了你会发现真正天天在用的反而不是那些花哨的框架而是几个最基础的数据结构栈、队列、set、map、list。很多性能问题、代码可读性问题本质上是数据结构用错了。今天我把这几个容器的常用方法、适用场景、还有我踩过的坑一次性捋清楚全程用Python演示最后附上Java/C的关键差异保证你在任何一个语言里都能快速迁移。1. 先从底层世界观聊起容器三兄弟的分类逻辑很多人学数据结构觉得枯燥是因为把每个容器当成孤立的API在背。其实你只需要记住一件事所有的容器本质上是换着花样解决“怎么存、怎么取”的问题。我的理解框架是这样的线性存储数据排成一条线list、栈、队列都属于这一类。区别在于允许从哪头进、从哪头出。哈希查找set和map的核心用哈希表实现查找复杂度平均是O(1)牺牲了顺序换速度。有序变体TreeSet、TreeMap或者Python里的SortedList用树结构实现换来的是“有序”这个能力但操作复杂度退化为O(log n)。这个分类底下选择容器的逻辑其实很简单你需要“顺序”还是需要“速度”你需要“唯一”还是需要“映射关系”你需要“先进先出”还是“后进先出”下面我把每一个容器掰开揉碎从常用方法到源码特性再到实战场景逐个过一遍。2. 栈常用方法后进先出是它的命根子2.1 栈的场景直觉栈的经典口号叫“后进先出”LIFO。什么时候能想到用栈我自己的经验是三个字可回溯。比如浏览器的后退按钮你访问的顺序是 A - B - C后退的时候先回到B再回到A这不就是栈吗再比如文本编辑器的Undo撤销你打得越多撤销的顺序越逆着来也是栈。另外函数调用本身就是在用栈。你写递归的时候系统一直在帮你做入栈和出栈的操作只是你自己没察觉。理解这一点你就能理解为什么递归太深会栈溢出因为系统栈的空间是有限的。2.2 Python里的三种栈写法Python自带类型里没有专门的Stack类但list天然就能当栈用Python官方文档也推荐这么做。# 最直接的栈用法list append pop stack [] stack.append(1) # 入栈等价于 push stack.append(2) stack.append(3) top stack[-1] # 查看栈顶不弹出 pop_value stack.pop() # 出栈弹出栈顶这里有个细节值得注意stack[-1]取栈顶元素时列表为空会报错IndexError。所以实战中我建议封装一个类把判空逻辑包进去class Stack: def __init__(self): self._items [] def push(self, item): self._items.append(item) def pop(self): if self.is_empty(): raise IndexError(pop from empty stack) return self._items.pop() def peek(self): if self.is_empty(): raise IndexError(peek from empty stack) return self._items[-1] def is_empty(self): return len(self._items) 0 def size(self): return len(self._items)Java里用Stack类可以直接push、pop、peekC里是stackT都有native支持。不过如果你在一些面试场景里Stack类其实是Vector的遗产性能上有同步开销Java官方后来更推荐用ArrayDeque来实现栈。这也是一个典型的“用什么容器”比“用什么API”更重要的例子。2.3 经典场景括号匹配与函数调用括号匹配是面试高频题但现实中写编译器、做模板解析、校验配置语法都会遇到。核心思路就是用栈def is_valid_brackets(s: str) - bool: stack [] pairing {): (, ]: [, }: {} for ch in s: if ch in pairing.values(): # 左括号入栈 stack.append(ch) elif ch in pairing: # 右括号检查配对 if not stack or stack.pop() ! pairing[ch]: return False # 其他字符忽略 return not stack这段代码看起来简单但有一个坑很多人没注意到判断for ch in s时如果字符串里带有引号包裹的括号字符比如字符串字面量里的括号不应该参与匹配。我在写过一个SQL解析器的时候就因为没有跳过字符串内容导致括号配对逻辑被引号里的内容干扰。真正的实现里必须加一个状态机去识别是否处于字符串中。2.4 栈使用的三个经验优先用小数组模拟很多语言里栈底层是数组初始化时如果预估数据量可以先分配空间避免频繁扩容。入栈操作要关注“引用别变”入栈的是对象引用后续修改外部对象会影响栈里的数据这是个隐蔽Bug。该用不可变对象或拷贝时不要偷懒。遍历栈不要直接用下标栈的语义就是只能从栈顶操作。如果你发现自己需要遍历栈说明你的数据结构选错了应该改用一个list有序处理。3. 队列常用方法先进先出的任务流水线3.1 什么场景必须用队列队列的语义是“先进先出”FIFO最典型的场景就是排队。消息队列、网络请求调度、线程池任务排队底层都是这个思想。我自己的区分经验是这样需要公平、需要顺序、需要削峰时用队列需要回溯、需要回退时用栈。你写一个请求限流系统进来的请求就得按到达顺序一个个处理第一个来的不能被后来的插队这必须用队列。3.2 Python的deque才是最优解很多人用list模拟队列我劝你别这么干# 错误示范list模拟队列 queue [] queue.append(1) # 入队 item queue.pop(0) # 出队但是时间复杂度是O(n)pop(0)会把后面所有元素往前挪一位列表一长效率极其低下。正确的做法是用collections.deque双向队列两端操作都是O(1)from collections import deque queue deque() queue.append(1) # 从右边入队 queue.append(2) left_item queue.popleft() # 从左边出队也就是队首如果需要限定队列长度可以用deque(maxlen3)当队列满了之后再append最老的元素会自动被挤出去。这在做“最近N条日志”这种场景非常好用——它就是你的滑动窗口。如果说deque是双端队列那就意味着它同时支持两端的append和pop这在实现某些算法时非常灵活。比如你既需要从队首取值又需要往队尾塞数据deque一把梭。3.3 优先队列与普通队列的区别说完普通队列必须讲讲优先队列。因为在实际工作中最常见的队列其实是带优先级的。业务上的典型场景订单有VIP等级VIP用户的请求要插队到普通用户前面这种就不能用普通队列要用优先队列PriorityQueue/Heap。import heapq # 最小堆做优先队列数字越小优先级越高 tasks [] heapq.heappush(tasks, (2, 普通任务)) # 第二个元素是任务内容 heapq.heappush(tasks, (1, VIP任务)) heapq.heappush(tasks, (3, 后台任务)) while tasks: priority, task heapq.heappop(tasks) print(task, 优先级, priority)优先队列的底层是堆结构入队出队都是O(log n)它和普通队列的“绝对顺序”不同它保证的是“按优先级顺序”。如果你希望高优先级的任务先被处理但同级的还要保证先后顺序这里有个小技巧插入时加一个计数器(priority, seq, task)避免两个相同优先级的任务互相比较时报错同时保证先到的先出。我在某公司做消息网关时遇到过一个问题直接(priority, task)推送给不同的任务对象时Python会遇到类型无法比较的TypeError。加上计数器做一个递增序号就能完美解决。3.4 队列实现的取舍细节Python的queue.Queue是线程安全的但多线程环境下它内部有锁单线程环境下性能反而不如deque。如果你只是普通业务代码里做在中转建议用deque就行。如果你在做广度优先搜索BFS队列就是天然的遍历容器。网格类路径规划、树的层序遍历出队一个节点、把相邻节点入队整个搜索过程就是这样简单。deque切片是逐段操作的不要试图对整个deque做queue[1:3]这种切片操作它不支持。需要时先转成list再说。4. set常用方法去重和集合运算的效率霸主4.1 为什么查“是否存在”一定要用set一个最常见的错误用list去查一个元素是否存在。# 慢list查找是O(n) user_ids [101, 102, 103, 104] if 103 in user_ids: print(存在) # 快set查找是O(1) user_ids_set {101, 102, 103, 104} if 103 in user_ids_set: print(存在)当数据量到达万级别的时候list的in操作在性能上的劣势就开始明显了到百万级别差异甚至能到上千倍。set底层是哈希表通过哈希值直接定位和list的线性扫描完全是两个维度。我习惯用一句话记判断“存在与否”用set需要“保持顺序”才用list。4.2 set的增删查与集运算set的常用方法很多但归类起来就是增删查、集合运算、集合关系三大类。# 创建与增删 s {1, 2, 3} s.add(4) # 添加元素 s.discard(2) # 删除元素不存在也不会报错 s.remove(1) # 删除元素不存在会抛KeyError s.pop() # 随机删除并返回一个元素 # 判断 3 in s # O(1)复杂度 # 集合运算 a {1, 2, 3} b {2, 3, 4} print(a b) # 交集 {2, 3} print(a | b) # 并集 {1, 2, 3, 4} print(a - b) # 差集 {1} print(a ^ b) # 对称差集 {1, 4}这中间有一个非常隐蔽的坑remove和discard的区别。如果你不确定元素是否存在用discard更安全但如果你需要知道元素是否存在应该在remove前面加一次in判断而不是catch异常。写Python时尽量遵循“与其请求原谅不如事先检查”EAFP vs LBYL但在set操作上提前判断更能保证代码可读性。集合运算再深入一点还涉及子集和超集的判断a {1, 2} b {1, 2, 3} print(a.issubset(b)) # Truea是b的子集 print(b.issuperset(a)) # Trueb是a的超集这个在权限校验上非常好用比如判断用户拥有的权限是否包含访问某个接口所需要的一组权限一条子集判断搞定了不需要写循环遍历。4.3 set的内部机制决定了它的限制set有两个天然限制大家容易忽略第一元素必须是可哈希的。list、dict这类可变类型不能放进set。我见过有人想存一个坐标列表直接用set([[1,2],[3,4]])结果直接TypeError。解决办法是把list转tuple再放进去。coords [(1, 2), (3, 4)] s set(coords) # tuple可以哈希list不行第二set是无序的。你以为输出顺序和插入顺序一致在小数据量时Python 3.7的实现确实有点像顺序但这只是哈希决定的偶然表现不是语言保证的行为。如果你需要“去重且有序”要么用Python 3.7的dict.fromkeys技巧要么用SortedContainer这类第三方库要么手动转list sort。# 去重保持顺序的经典写法 items [3, 1, 3, 2, 1, 4] unique_ordered list(dict.fromkeys(items)) print(unique_ordered) # [3, 1, 2, 4]这个写法我经常用本质是拿dict的键天然唯一且保持插入序的特性做文章比set输出后你无法保证顺序要好得多。4.4 frozenset不可变set的另类用处set本身可变所以不能作为另一个set的元素也不能作为dict的键。但如果你需要“集合的集合”这种场景比如判断一个用户是否已经访问过某个权限组合就需要frozenset。fs1 frozenset({1, 2}) fs2 frozenset({2, 3}) outer {fs1, fs2}我自己写一个权限去重模块时就用到了frozenset因为要判断若干个权限集合是否重复出现直接放进set里自动去重完美符合需求。5. map常用方法键值映射的百搭神器5.1 什么时候想到用map说白了map就是字典存储键值对。什么时候用map你遇到“根据某样东西查另一样东西”的场景就该用map。根据用户ID查用户详情根据配置项名称查配置值根据单词统计出现次数根据路由路径查处理函数在我做的很多后端项目里map是最常用的容器没有之一。缓存、状态机、枚举映射、多条件分发全都靠在map上做文章。5.2 Python dict的常用方法盘点data {name: 张三, age: 20, city: 北京} # 读取与写入 value data[name] # 键不存在抛KeyError不推荐 value data.get(name, 默认值) # 键不存在时返回默认值推荐 data[age] 21 # 修改已有键 data[gender] 男 # 新增键 # 遍历 for key in data: print(key, data[key]) for key, value in data.items(): print(key, value) for key in data.keys(): pass for value in data.values(): pass # 删除 popped data.pop(city, None) # 删除键并返回值不存在时返回默认 del data[age] # 直接删除键不存在抛KeyError data.clear() # 清空所有内容 # 合并更新 data.update({email: xexample.com, age: 22})这里面最常用的自然是.get因为它安全。我在实际开发里几乎不会直接用data[key]因为一旦键不存在整个流程就崩掉了。.get配合默认值写起来很顺手。另一个高频操作是统计次数。很多人会用if key not in dict的写法判断但其实有更简洁的方法# 传统写法 counter {} word_list [a, b, a, c, a] for word in word_list: if word in counter: counter[word] 1 else: counter[word] 1 # 简洁写法 counter {} for word in word_list: counter[word] counter.get(word, 0) 1 # 更优雅的defaultdict from collections import defaultdict counter defaultdict(int) for word in word_list: counter[word] 1defaultdict(int)的用法很妙键不存在时自动用int()初始化成0直接加一相当于你少写了一个判断。defaultdict(list)也很常用存“一个键对应多个值”的映射时特别方便不需要手动判断键存不存在再append。5.3 有序的map需求用OrderedDict还是普通dictPython 3.7之后普通dict也保持插入顺序这一点让OrderedDict的存在感降低了很多。但如果你需要做“移动键到末尾”这种操作OrderedDict依然有用from collections import OrderedDict od OrderedDict({a: 1, b: 2, c: 3}) od.move_to_end(a) # 把a移动到末尾 print(list(od.keys())) # [b, c, a]这个操作在实现一个简单的LRU缓存时很关键。每次访问某个键就把它move_to_end淘汰的时候直接从头部popitem就能实现一个最简LRU。from collections import OrderedDict class LRUCache: def __init__(self, capacity): self.capacity capacity self.cache OrderedDict() def get(self, key): if key in self.cache: self.cache.move_to_end(key) return self.cache[key] return -1 def put(self, key, value): if key in self.cache: self.cache[key] value self.cache.move_to_end(key) else: if len(self.cache) self.capacity: oldest_key self.cache.popitem(lastFalse)[0] self.cache[key] value这个LRU实现我很喜欢因为代码量少也能清楚地展示OrderedDict的价值。5.4 dict的合并与解包技巧Python 3.9之后dict支持用|合并这个写起来比update更直观a {x: 1, y: 2} b {y: 3, z: 4} merged a | b # {x: 1, y: 3, z: 4}另外解包方式也能实现同样效果merged {**a, **b}5.5 map的键类型限制dict的键和set元素一样必须是可哈希的。经常有人想在Python里用dict当键做缓存记忆化然后报错。解决办法是用frozenset或者自定义类的__hash__方法。不过一个原则性建议键尽量用字符串、数字、tuple这类不可变基础类型这样既安全又清晰。6. list常用方法最灵活的序列容器6.1 list能做的远比你想象的多list是Python中使用频率最高的容器。它既能当数组用又能当栈用还能在部分场景下当队列用但前面说过队列性能要小心。常用方法一次性捋清arr [3, 1, 2] arr.append(4) # 末尾追加 arr.insert(0, 0) # 指定位置插入O(n)复杂度 arr.extend([5, 6]) # 合并另一个列表 arr.remove(1) # 删除第一个匹配值不存在抛ValueError item arr.pop() # 删除并返回末尾元素 item arr.pop(0) # 删除并返回指定位置元素 arr.index(3) # 查找第一个匹配值的索引 arr.count(2) # 统计某个值的出现次数 arr.reverse() # 原地反转 arr.sort() # 原地升序排序 sorted_arr sorted(arr, reverseTrue) # 返回新排序列表 # 切片 sub arr[1:3] # 左闭右开区间 reverse_view arr[::-1] # 反转的拷贝6.2 排序与自定义键的套路list.sort()可以传key函数这是一个非常强大但很多人用不熟的点people [ {name: 小明, age: 22}, {name: 小红, age: 20}, {name: 小刚, age: 25}, ] # 按年龄升序 people.sort(keylambda p: p[age]) # 按年龄降序再按名字升序 people.sort(keylambda p: (-p[age], p[name]))我在实际项目中经常要“按某个业务字段排序同时为空值沉底”。这里有一个坑None无法和数字比较大小。解决办法是用一个超大数或超小数的替代key# 把None映射到无穷大排序时沉底 people.sort(keylambda p: (p.get(age) is None, p.get(age) or 9999))这样处理过的逻辑在排行榜、分页列表里都非常实用。6.3 遍历中删除元素的大坑这是list操作中最经典的坑我敢说到今天还有人在犯。错误做法后果正序遍历并删除元素索引错位漏删元素循环中改变列表长度越界或早退出用del删除正在迭代的元素迭代器行为不可预知典型错误示例# 想删除所有偶数结果删不干净 nums [1, 2, 4, 5, 7, 6, 8] for num in nums: if num % 2 0: nums.remove(num) print(nums) # [1, 4, 5, 7, 8]4和8还留着为什么会这样因为删除元素后列表的索引自动前移循环指针却继续前进于是跳过了紧接着的下一个元素。正确的做法是倒序遍历或者创建新列表# 方案一倒序遍历删除 nums [1, 2, 4, 5, 7, 6, 8] for i in range(len(nums) - 1, -1, -1): if nums[i] % 2 0: del nums[i] print(nums) # 方案二列表推导筛选更推荐 nums [1, 2, 4, 5, 7, 6, 8] nums [num for num in nums if num % 2 ! 0]列表推导式不仅是写法简洁的问题它在底层是直接生成新列表完全避开索引错位问题性能也很高。6.4 list与array的区别如果数据量非常大且都是同类型数值list的性能和内存占用都不占优因为list里每个元素都是独立对象加上Python的list本质上是引用数组。数值密集计算场景建议用array.array或者numpy.ndarray。from array import array # 同类型数值数组内存占用远小于list nums array(i, [1, 2, 3, 4, 5])不过99%的业务场景里list已经够用不用盲目追求array。7. 栈、队列、set、map、list的横向对比与选型决策7.1 一张表搞定核心差异容器底层实现有序性唯一性查找复杂度插入删除复杂度常见用途栈数组/链表后进先出不要求一般不查中间O(1)栈顶递归回溯、撤销、括号匹配队列数组/链表/堆先进先出或优先级不要求一般不查中间O(1)两端任务调度、BFS、消息缓冲set哈希表无序是O(1)O(1)去重、成员判断、集合运算map/dict哈希表插入序Python键唯一O(1)O(1)映射查询、缓存、计数list动态数组完全有序不要求O(n)末尾O(1)头部O(n)序列存储、遍历、栈模拟从这表里能直接看出选型思路一个数据如果既需要保持顺序又需要频繁按键查找那dict就是答案如果需要顺序且可重复就是list如果只需要去重就是set如果既要先来后到又要有序处理就是队列。7.2 一个场景跑通所有容器我在某电商后台做一个订单处理流程时这套容器几乎全用上了订单数据从外部系统拉下来先用list按时间顺序缓存方便分页展示。待处理的订单进入一个队列按创建时间依次推送处理引擎。已经处理完的订单ID存入set用来判断新来的订单是否重复处理幂等。每个订单ID映射对应的处理状态用dict存储方便快速查询当前进度。如果处理流程出错了需要回滚就把之前经过的操作记录压入栈中逐层弹出撤销。一套流程下来list负责顺序展示、队列负责调度、set负责唯一性校验、dict负责状态查询、栈负责回滚。你把这个思路吃透在任何系统里都能画出类似结构。8. 高频笔试/面试题里这些容器是怎么被套用的8.1 用栈解决“下一个更大元素”经典面试题对数组中的每个元素找出它右边第一个比它大的数没有就是-1。暴力O(n^2)但用一个栈可以O(n)。思路是把数组的下标依次压入栈如果当前元素比栈顶下标对应元素大就说明栈顶找到了“下一个更大元素”弹出栈顶并记录结果然后把当前下标入栈继续循环。def next_greater(nums): result [-1] * len(nums) stack [] for i in range(len(nums)): while stack and nums[i] nums[stack[-1]]: idx stack.pop() result[idx] nums[i] stack.append(i) return result print(next_greater([2, 1, 4, 3, 5])) # [4, 4, 5, 5, -1]这个题型的核心就是栈里存的是“还没找到答案”的待匹配项每次来一个新元素把符合条件的都弹出来“结算”掉。8.2 用队列做BFS层序遍历树的层序遍历是队列最典型的应用from collections import deque def level_order_traversal(root): if not root: return [] result [] q deque([root]) while q: level_size len(q) current_level [] for _ in range(level_size): node q.popleft() current_level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) result.append(current_level) return result注意这个level_size变量的处理先记录当前队列长度再在这个范围内循环这样能保证每次循环处理的是同一层的所有节点这个细节直接区分了“层序遍历”和“普通BFS”。8.3 用set/map搞定“两数之和”经典的双数求和问题给一个目标值找出数组里两个数之和等于它。如果用dict记录每个数是否出现过def two_sum(nums, target): seen {} for i, num in enumerate(nums): complement target - num if complement in seen: return [seen[complement], i] seen[num] i return [] print(two_sum([2, 7, 11, 15], 9)) # [0, 1]这里用的就是map的O(1)查找。如果用list存“已见过的数”复杂度就退化成O(n^2)了。8.4 用list切片与排序解决问题需要保持前K个最大的元素你可能有三种思路直接全排序O(n log n)、手动维护长度为K的list每次冒泡插入O(nk)、或使用堆O(n log k)。数据规模一大全排序就不是最优解了。这种题目考查的既是数据结构知识也是算法复杂度的意识。9. 常见问题速查与避坑清单9.1 问题定位表典型问题根本原因解决方式list.pop(0)性能差底层数组整体拷贝改用deque.popleft()遍历list时删除数据漏项索引错位倒序遍历或列表推导dict[key]抛KeyError键不存在用dict.get()设置默认值set/list元素类型无法哈希使用了list/dict作为元素转为tuple/frozensetset得到的数据顺序和预期不同哈希表天然无序用dict.fromkeys保持插入顺序队列在多线程性能差不必要的锁竞争单线程场景改用deque递归层级过深导致程序退出系统栈空间不足改写成循环用显式栈模拟defaultdict误用导致多了一个键defaultdict(int)访问不存在的键会创建用普通dictget或先判断9.2 我踩过的一个list坑有一次我做一个接口的分页逻辑从数据库取了一批数据放进list然后想边过滤边保留原始顺序。代码长这样for item in items: if item[status] expired: items.remove(item)结果列表中连续两个expired状态的item只删掉了一个。我排查了半天最后才意识到是索引错位。从此之后我给自己立了一条规矩不修改正在迭代的容器需要过滤就生成新列表。这不只是美观问题而是关乎正确性。9.3 关于“内存占用”的提醒set和dict的哈希表结构比list占用更多内存。如果你只需要判断“存在性”且数据量极大可以考虑用bitmap或者布隆过滤器而不是无脑用set。像我做某推荐系统的黑白名单时一台机器内存有限set存百万级样本很吃力布隆过滤器虽然有小概率误判但换来了几十倍的省内存。10. 不同语言下的方法对应参考很多人会在Python、Java、C之间切换。不同语言的API不一致但数据结构思想是通的。这里整理一份常用对照表操作PythonJavaC入栈stack.append()stack.push() / deque.push()s.push()出栈stack.pop()stack.pop() / deque.pop()s.pop()栈顶查看stack[-1]stack.peek() / deque.peek()s.top()入队deque.append()queue.offer()q.push()出队deque.popleft()queue.poll()q.pop()队首查看deque[0]queue.peek()q.front()set添加s.add(x)set.add(x)s.insert(x)set删除s.discard(x)set.remove(x)s.erase(x)map赋值data[key] valuemap.put(key, value)mp[key] valuemap取值data.get(key, default)map.getOrDefault(key, default)mp[key]list追加list.append(x)list.add(x)v.push_back(x)记住这个对照表跨语言开发时你就不用再查文档了。10.1 各语言容器的性能差异Java的Stack类继承自动Vector所有方法都带锁所以性能比ArrayDeque差官方文档自己都建议用ArrayDeque优先替代。C的std::stack默认底层是deque如果数据量很大可以考虑换成vector作为底层容器降低开销。Python的list本质是引用数组元素扩容时有时会轻微浪费内存。set和dict底层是开放寻址法删除元素时不会立即缩容如果有大量的增删交替且内存宽度敏感注意重建容器来释放容量。这些差异我们日常开发不太容易察觉但在做高并发、高吞吐的模块时影响明显。我的原则是先用对的数据结构再做性能优化性能优化的前提是有什么地方慢不能盲目把数据结构和容器换来换去。11. 从源码层面看容器的设计逻辑11.1 为什么Python dict这么灵活CPython的dict底层是稀疏哈希表每个槽位存指向键和值的指针。因为键和值分成两块存储所以dict天然支持混合类型键值。比如{name: 张三, 1: 数字键}就是合法的。但哈希表在扩容时会重新计算所有现有键的哈希值并重新分配位置这意味着如果你持有某个dict的视图经过大规模的insert之后再遍历与添加时的顺序可能完全不一致这里针对旧版本Python3.7保证插入序扩容后相对顺序依然保持。11.2 为什么集合运算能快成那样集合的交并差运算底层是反复调用哈希表的查找、插入、删除。因为每次操作都是O(1)所以两个set的交集复杂度是O(min(len(a), len(b)))远快于嵌套循环的O(n*m)。我在处理几十万用户ID比对时set的交集操作跑一次只需要几十毫秒而用列表嵌套循环可能要几分钟。11.3 deque的内部结构CPython的deque底层由多个定长数组块组成形成一个双向链表式的结构。它不像list那样有“容量”的概念也不需要整体移动元素。正因为如此deque成为了BFS和滑动窗口的标配。12. 这些数据结构将来会怎么演进不同语言对容器的改进都在朝一个方向走让代码更安全、性能更好、写起来更简单。Python这边3.7把dict的插入顺序变成了语言规范3.9新增dict |操作3.10的match-case更是把dict模式匹配做进了语言层很多分发代码连if-elif都不需要了。Java这边Stream API让list、set、map的过滤、映射、聚合操作几乎全变成了链式调用。C20引入的std::ranges对容器的操作也有很大的简化。但底层数据结构本身变化不大——哈希表还是哈希表数组还是数组。只要你的基本功扎实语言版本再怎么升级你都能快速适应。我个人在实际开发中最大的体会是用好容器不是背API而是根据场景的“读写比例、顺序要求、唯一性要求”三个维度去选型。你每次写代码前花十秒钟想一想这三个问题踩坑的概率就会小很多。另外疫情期间我居家办公远程会议多就用自己写的这几套数据结构搭过一个小工具消息队列负责排队处理会议纪要、set去重标记已参加会议、map记录每个参会的发言时间、list做时间线展示、栈做回滚撤销。这件事让我确信数据结构是那种生活中真真切切用得上、不只是面试题的基本功。最后再分享一个小技巧源码里多留注释。给容器变量起名时带出它的用途比如pending_queue、seen_set、status_map我在排查旧代码时看到这些名字就能立刻读懂意图比任何设计文档都直观。代码不只是写给自己看的更多是写给三个月后的自己看的。希望这篇能把栈、队列、set、map、list这五个容器的常用方法和选型思路讲透。如果你现在正被某个容器的问题卡住回去看看第9章的问题速查表多半能找到答案。
RELATED

相关推荐

仓颉 stdx.crypto.keys 密钥枚举详解:Curve 椭圆曲线与 PadOption 填充模式

仓颉 stdx.crypto.keys 密钥枚举详解:Curve 椭圆曲线与 PadOption 填充模式

文档知识库教程OpenHarmony 【免费下载链接】CangjieCorpus 本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 R…

📅 2026/10/15 2:27:46
第一个挖到的漏洞,未授权 SQL 注入

第一个挖到的漏洞,未授权 SQL 注入

从一个后台登录口到未授权 SQL 注入:ThinkPHP exp 表达式注入发现复盘说明:本文为个人授权测试后的技术复盘,目标已完全脱敏(域名、IP、库名、哈希、盐值均为占位演示值)。漏洞已通过合法渠道提交并结案,仅…

📅 2026/10/15 2:27:46
M3U8 文件 BOM 头部标记,看不见字符带来解析失败

M3U8 文件 BOM 头部标记,看不见字符带来解析失败

、看不见的 BOM 头部带来解析失败 很多新手遇到一类诡异故障:M3U8 内容肉眼看完全没问题,第一行写的就是 #EXTM3U,VLC 播放器播放一切正常,但是浏览器 hls.js 直接解析报错,播放黑屏。 根源就是 UTF‑8 BOM 标记。BO…

📅 2026/10/15 2:22:46
MORE NEWS

更多资讯

📰

企业微信API如何接入现有系统?接口层与业务层怎么划分

很多企业研发团队在初次对接企业微信 API 时,为了赶进度,习惯性地采用“直连模式”:在 Webhook 的接收函数里直接写 if-else 业务判断,甚至直接连上内部 CRM 的数据库执行 SQL。 这种“烟囱式”的强耦合代码,在初期或…

📰

Rope 模型加载失败终极指南:从报错定位到修复验证的完整教程

Rope 模型加载失败终极指南:从报错定位到修复验证的完整教程 【免费下载链接】Rope GUI-focused roop 项目地址: https://gitcode.com/GitHub_Trending/ro/Rope Rope 模型加载失败可能是你初装这个工具时最常碰上的问题——Rope 是一个基于 insightface insw…

📰

企业微信外部群机器人开发:群内@消息应该如何单独处理

在企业微信外部群的日常运营中,群聊环境极其嘈杂。如果你的机器人处于“全量监听”状态,群里每发一句闲聊,网关都会去查一次库或者请求一次大模型,不仅服务器的算力会瞬间被榨干,机器人还极易因为“乱插嘴”引发客户反…

📰

企业微信二次开发实战:如何把客户会话接入自己的业务后台

在私域流量的精细化运营时代,企业微信已经成为链接客户的标配触角。然而,真正的客户画像、历史工单、消费评级等核心资产,其实都沉淀在企业自有的 CRM 或 ERP 系统中。 如果仅仅停留在使用企微客户端打字聊天,或者使用粗糙的“直…

📰

大麦抢票脚本 5 分钟跑通指南:从 clone 到自动下单

大麦抢票脚本 5 分钟跑通指南:从 clone 到自动下单 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 热门演出开票几秒就售罄,手动刷新和点击总是慢人一…

📰

最新计算机毕业设计选题推荐-基于spring boot的美食配送平台-网站-文档指导-Java-springboot

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬