ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python数据结构从入门到实战:掌握list、dict、set与算法思维

Python数据结构从入门到实战:掌握list、dict、set与算法思维 1. 为什么我劝你学Python数据结构而不是直接刷算法题很多刚入门的同学会跑来问我“我想学数据结构是不是直接去刷LeetCode就行”我的回答一直是别急先老老实实把Python数据结构的基本盘打牢。原因很简单数据结构和算法题是两个层次的东西。数据结构是“武器库”算法是“战术”。你连武器库里有什么兵器、每件兵器擅长打什么场景都不知道就直接上战场刷题结果就是看一道题懵一道看了答案觉得“哇原来如此”关上答案自己写还是无从下手。这种挫败感我见得太多也体会过太多次了。那什么是数据结构用大白话说数据结构就是“计算机组织和存储数据的方式”。同样一堆数据你用不同的方式摆布它们增删改查的效率可以天差地别。举个生活的例子你手机里的通讯录如果按姓名拼音排序查找一个人就很快如果乱序存着你只能从头翻到尾。这个“排序”和“乱序”就是两种不同的数据结构它们的查询效率完全不同。Python这门语言在数据结构这块有一个天然的优势它内置了非常丰富的数据结构类型——列表list、元组tuple、字典dict、集合set等等开箱即用不用像C语言那样自己从零写链表、自己管理内存。但也正因为太方便了很多人只会“用”这些结构却从没想过它们背后的设计思想和工作原理。等到面试被问“Python的list底层是什么结构”“为什么dict查找那么快”的时候直接哑火。这篇文章就是写给这样的人——你听过数据结构这个词知道Python里有list和dict但还没系统梳理过它们你想从零开始把Python数据结构吃透并且知道每个结构在什么场景下真正派上用场。我不打算把这篇写成一本正经的教科书而是按我自己的学习路径和实践经验来拆尽量做到“学完就能用用完能说清原理”。顺便说一句如果你连Python环境都还没装好我建议你先把环境搞定再回来读否则你看代码示例也没法亲自跑一遍。安装这块网上教程很多核心就一句话去Python官网下安装包安装时务必勾选“Add Python to PATH”这一步能省掉后面大量的环境变量烦恼。装完在命令行敲python --version能输出版本号就算成功了。2. 从内置四种核心结构入手先搞懂它们到底“长什么样”Python内置的数据结构是整个语言最精华的部分之一日常写脚本、做数据分析、写爬虫几乎都在和这四种结构打交道。理解它们之间的区别和适用场景比背任何API文档都重要。2.1 列表list最灵活的“万能口袋”列表是Python里最常用的数据结构用方括号[]表示可以装任意类型的数据my_list [1, hello, 3.14, [True, False], {name: 张三}]我刚开始学的时候最惊奇的一点就是Python的列表居然可以同时放整数、字符串、甚至嵌套另一个列表和字典。这在C语言里根本不可想象C的数组只能存同一种类型。这种灵活性让Python在处理混合数据比如从网页抓回来的杂乱数据时特别顺手。列表的核心特点有几点有序元素按插入顺序排列可以通过索引访问索引从0开始可变可以增删改元素允许重复同一个值可以出现多次常用的操作我就不一一背API了但有几个容易踩坑的点值得单独说。先说切片。切片是Python列表最强大的特性之一格式是list[start:stop:step]注意是“左闭右开”区间也就是包含start位置的元素不包含stop位置的元素nums [0, 1, 2, 3, 4, 5] print(nums[1:4]) # 输出 [1, 2, 3]注意没有4 print(nums[:3]) # 输出 [0, 1, 2] print(nums[::2]) # 输出 [0, 2, 4]步长为2 print(nums[::-1]) # 输出 [5, 4, 3, 2, 1, 0]妙用反转列表切片面试高频坑nums[1:4]不会包含索引4的元素。很多新手第一次写切片以为“1到4”应该包含4结果调试半天。记住Python的range函数也是这个规则——左闭右开统一记忆就不容易混了。再说一个几乎人人都会踩的坑列表的拷贝。直接用等号赋值并不是拷贝而是给同一个列表起了个别名a [1, 2, 3] b a # b只是a的引用不是新列表 b.append(4) print(a) # 输出 [1, 2, 3, 4]a也被改了正确的拷贝方式有两种b a.copy() # 方法一 b a[:] # 方法二利用切片创建新的列表但这两种都是“浅拷贝”——如果列表里装的是可变对象比如嵌套列表改内层还是会影响原列表。真正要做到完全独立的“深拷贝”需要import copy然后用copy.deepcopy(a)。这个知识点在面试中经常被追问建议自己动手敲一遍验证。2.2 元组tuple不可变的“安全档案袋”元组用圆括号()表示和列表几乎一样唯一的本质区别是元组不可变创建之后不能增删改。point (3, 5) # point[0] 10 # 这行会报错元组不支持修改元组不可变这个特性很多人觉得“那它有什么用”其实价值非常大安全多个地方共享数据时元组不会被人不小心改掉可作为字典的键字典的键要求必须是不可变类型所以元组能做键列表不行性能更好不可变对象在内存管理上更轻量遍历速度也比列表略快举例说明如果你有一组坐标点不想被其他函数意外修改用元组就比列表稳妥locations [ (116.40, 39.90), # 北京坐标 (121.47, 31.23), # 上海坐标 ]还有一个Python特有的用法叫“解包”unpacking在元组、列表上都能用但和元组搭配起来特别优雅point (3, 5) x, y point print(x, y) # 输出 3 5甚至是交换两个变量的值不用临时变量a, b 1, 2 a, b b, a # 一行交换完成Python经典特性2.3 字典dictPython的“搜索引擎”字典用花括号{}表示存的是“键值对”key-value pair它的查找速度极快。这是字典最核心的卖点——无论字典里有10条数据还是10万条数据按key查找的时间几乎不变平均时间复杂度是O(1)。Python的字典底层是哈希表hash table实现的。什么叫哈希表打个比方你去一个超大的图书馆每本书都有一个唯一的编号哈希值你根据编号直接走到对应书架不需要一本一本地翻。字典的key通过一个哈希函数计算出一个“编号”存储和查找都靠这个编号定位所以快得离谱。字典基本操作student { name: 小明, age: 18, scores: [85, 92, 78] } print(student[name]) # 按键取值 student[age] 19 # 修改值 student[city] 北京 # 新增键值对取值时要注意直接用中括号student[xxx]如果key不存在会直接抛KeyError异常导致程序崩溃。更安全的写法是用get方法print(student.get(gender)) # 不存在时返回None不报错 print(student.get(gender, 未知)) # 不存在时返回指定的默认值这个习惯建议从一开始就养成处理真实数据时你根本不知道数据里有没有某个字段get方法能帮你省掉大量if判断。另一个常见使用场景是统计频率。比如统计一段文本里每个字符出现的次数用字典实现非常自然text hello world counter {} for ch in text: counter[ch] counter.get(ch, 0) 1 print(counter) # 输出 {h: 1, e: 1, l: 3, o: 2, : 1, w: 1, r: 1, d: 1}一行get以及默认值就把“判断key是否存在再累加”的逻辑压缩了这就是字典和内置方法的配合之美。2.4 集合set天生会“去重”的魔法师集合也用花括号{}表示但里面装的是一个个独立元素而不是键值对。集合有三个核心特性无序、元素唯一、支持集合运算。无序意味着你不能通过索引访问集合里的元素元素唯一意味着自动去重集合运算包括并集、交集、差集等。最经典的应用就是去重nums [1, 2, 2, 3, 3, 3, 4] unique_nums list(set(nums)) # 先转集合去重再转回列表 print(unique_nums) # 输出可能是 [1, 2, 3, 4]顺序不一定另一个实用场景是集合运算。比如你有两个朋友列表想知道共同好友是谁用集合的交集一行搞定friends_a {张三, 李四, 王五} friends_b {李四, 赵六} common friends_a friends_b # 交集输出 {李四} all_friends friends_a | friends_b # 并集 only_a friends_a - friends_b # 差集集合和字典一样底层也是哈希表所以查找元素是否存在的效率同样是O(1)。判断某个元素在不在集合里用not in%写法if 张三 in friends_a: print(是朋友)这里有个隐藏知识点集合要求元素必须是不可变类型可哈希。所以你不能创建set([[1,2], [3,4]])因为列表不能哈希但你可以用元组set([(1,2), (3,4)])。这点和字典的键约束完全一致底层原因也一致。3. 从内置结构走向底层原理list和dict到底是怎么实现的只停留在“会用”层面是不够的。面试也好、应对性能问题也罢你需要理解Python内置结构底层的实现思路。这一节我把list和dict的底层原理拆开讲清楚。3.1 list的底层动态数组而不是链表很多人听到“Python列表”会下意识以为它等同于C语言里的链表其实完全不是。Python的list底层是一个动态数组dynamic array。理解这个很重要。动态数组的特点是内存中是一段连续的地址空间通过索引访问某个元素时计算方式是“起始地址 索引 × 每个元素占用的字节数”所以按下标访问的时间复杂度是O(1)极快。但连续空间也带来一个代价如果要在中间插入或删除元素需要把后面的元素全部往后移或往前移时间复杂度是O(n)。数据量一大这个操作就明显变慢。那动态体现在哪里当list长度不够用的时候Python会申请一块更大的连续内存通常是当前大小的1.125倍左右具体比例是CPython的实现细节把旧数据复制过去然后把旧空间释放。所以你往list尾部append元素均摊下来依然是O(1)因为大部分时候不需要扩容。举一个实际现象说明这个特性如果你在一个list的头部反复insert(0, x)当list很大时你会明显感觉到程序卡顿因为每次插入都要搬动所有已有元素。如果频繁需要从头部增删数据更合适的选择是collections.deque它是双端队列头部尾部操作都是O(1)。这个知识点在Python性能优化里非常实用。3.2 dict的底层哈希表与扩容碰撞dict的底层是哈希表。哈希表的核心思想是通过哈希函数把key映射为一个整数索引数据就存在这个索引对应的位置上。查找时对key再次计算哈希直接定位到存储位置不需要遍历所有数据。但哈希表有个必须处理的问题——哈希冲突。不同的key可能计算出相同的哈希值或映射到同一个槽位这时候Python会采用“开放寻址法”来解决如果目标位置已经被占用就按一定规则继续探测下一个空位。所以哈希表的查找虽然平均是O(1)但在极端情况下冲突非常严重性能会退化。哈希表还有一个关键特性哈希表的存储顺序不等于插入顺序。在Python 3.7之前dict是无序的遍历顺序和插入顺序不一定一致从Python 3.7开始官方实现保证了插入顺序。但你必须清楚这个“有序”是出于特殊实现不代表你可以依赖哈希表的顺序做任何需要严格排序的逻辑——需要排序就老老实实sorted()。另一个需要特别注意的约束dict的key必须是可哈希的也就是不可变类型。字符串、数字、元组都可以做key列表、字典、集合都不行。这是因为哈希表在插入、查找时都要对key计算哈希值如果key是可变的哈希值变了数据就彻底找不到了。3.3 可变与不可变藏在一切结构背后的第一性原理聊到这里有必要把“可变性”mutability这个概念单独提出来。它是理解Python数据结构的一把钥匙。可变类型list、dict、set创建之后可以修改内部内容不可变类型tuple、str、int、float创建之后无法修改不可变对象的好处是安全和可哈希可变对象的好处是灵活。但要小心可变对象在函数传参时会带来“意外”def add_item(items): items.append(100) my_list [1, 2, 3] add_item(my_list) print(my_list) # 输出 [1, 2, 3, 100]原列表被函数改了很多新手以为给函数传参是“传值”但Python传的是引用。如果你不想让函数修改原对象在函数内部对可变参数做一次拷贝再操作或者调用前就传一份副本。我踩过最狠的一次坑是定义函数默认参数时用了可变对象def add_student(name, students[]): students.append(name) return students print(add_student(张三)) # [张三] print(add_student(李四)) # [张三, 李四]居然累计了原因默认参数只在函数定义时求值一次后续调用复用同一个列表对象。正确做法是默认参数用None再在函数体里创建空列表def add_student(name, studentsNone): if students is None: students [] students.append(name) return students这种细节书上看十遍不如自己写错一次记得牢。4. 从数据结构到算法思维排序、查找与复杂度初步数据结构从来不是孤立的它和算法紧密关联。你学了数据结构下一个问题必然是数据放进这个结构里怎么操作最高效这就是复杂度分析和基础算法的内容。4.1 时间复杂度如何判断一段代码快不快复杂度分析是数据结构的“度量衡”。不掌握它你就说不清list和dict谁快也就无法在真实场景中做选择。时间复杂度描述的是当数据规模n增长时算法运行时间的增长趋势。常用大O表示法符号含义典型操作O(1)常数时间不随数据量变化按索引访问list、dict按key查找O(log n)对数时间数据量翻倍时间只加一点二分查找O(n)线性时间数据量翻倍时间也翻倍遍历list、线性查找O(n log n)线性对数时间快速排序、归并排序O(n²)平方时间数据量大时急剧变慢冒泡排序、双重循环记这些不需要死记硬背而是建立直觉写代码时看一眼循环嵌套层数基本就知道量级了。一层循环多是O(n)两层嵌套循环就是O(n²)。而如果你能用dict或set把内层循环替换成一次查找就能把O(n²)降到O(n)这是最常见的优化套路。我举一个非常典型的例子找出两个列表中重复的元素。新手写法是双重循环def find_duplicates(list1, list2): result [] for a in list1: for b in list2: if a b and a not in result: result.append(a) return result这个写法的复杂度是O(n²)而且result里的not in操作又是O(n)实际更慢。用集合优化def find_duplicates(list1, list2): set1 set(list1) result [] for b in list2: if b in set1 and b not in result: result.append(b) return result第二版把内层遍历替换成了set的O(1)查询整体复杂度降为O(n)数据量大的时候差距是秒级和分钟级的差距。这种“用哈希结构换时间”的思路是初学者从“会写”到“会优化”的第一道分水岭。4.2 排序算法从冒泡到快排理解“分治”思想数据结构课程里排序是重头戏Python内置的sorted()函数虽然强大但理解排序算法的演进逻辑仍然是必要的因为它训练的是算法思维。冒泡排序是最直白的方法重复地遍历列表比较相邻元素顺序不对就交换。一趟下来最大的元素就像气泡一样“浮”到最后。代码实现def bubble_sort(nums): n len(nums) for i in range(n - 1): swapped False for j in range(n - 1 - i): if nums[j] nums[j 1]: nums[j], nums[j 1] nums[j 1], nums[j] swapped True if not swapped: # 如果一整趟没交换说明已经有序提前结束 break return nums冒泡的时间复杂度是O(n²)数据量一大就不行了。所以要升级到快速排序。快排的核心思想是“分治”选一个基准值把列表分成“小于基准值”和“大于等于基准值”两部分然后递归地对两部分继续相同操作。关键是每次分区后基准值就落到了最终位置不再参与后续排序。不追求极致性能的话可以写出非常简洁的快排虽然空间效率不是最优def quick_sort(nums): if len(nums) 1: return nums pivot nums[len(nums) // 2] left [x for x in nums if x pivot] middle [x for x in nums if x pivot] right [x for x in nums if x pivot] return quick_sort(left) middle quick_sort(right)这个版本最好理解平均复杂度O(n log n)。不过它每次递归都创建新列表空间消耗大正式场合还是推荐用原地in-place版本或者直接用Python内置的sorted()——那是用C语言实现的Timsort又快又稳定绝大多数场景你都不该自己造轮子。但自己实现一遍逆序、查找、排序这些基础操作对理解数据结构的运作过程非常有帮助。我的建议是工作中用内置函数学习时手动实现两者不冲突。4.3 查找算法顺序查找和二分查找查找是数据结构中另一大基础操作。针对无序数据只能顺序查找一个个比较复杂度O(n)。但如果数据是有序的就能用二分查找每次取中间元素比较如果比目标值大就去左边半段找如果小就去右边半段找。每比较一次搜索范围减半。二分查找的实现最容易出错的地方是边界条件。用递归写很清晰def binary_search(nums, target, left, right): if left right: return -1 # 没找到 mid (left right) // 2 if nums[mid] target: return mid elif nums[mid] target: return binary_search(nums, target, left, mid - 1) else: return binary_search(nums, target, mid 1, right)刚才说过复杂度分析中O(log n)是很优秀的量级二分查找就是这个量级。Python标准库的bisect模块也提供了二分查找工具面试中如果被问到底层原理你能说出“每次砍一半”这个核心再手写一次循环版本基本就过关了。5. 从内置到进阶栈、队列、链表与树Python里怎么实现内置数据结构只是入门真正让“数据结构”这三个字立体起来的是那些经典结构栈、队列、链表、树。Python里没有像C语言那样的“结构体指针”但这些结构的思想可以被灵活实现出来。5.1 用list模拟栈后进先出栈Stack是一种“后进先出”LIFO, Last In First Out的结构就像一摞盘子你只能从顶部放也只能从顶部拿。栈的应用非常广泛函数调用栈、括号匹配、浏览器的后退按钮、撤销操作全是栈。Python里用list就能完美模拟stack [] stack.append(1) # 压栈push stack.append(2) stack.append(3) top stack.pop() # 弹栈pop得到3 print(stack) # [1, 2] print(stack[-1]) # 查看栈顶元素但不弹出括号匹配是栈的经典应用。比如判断一个表达式里的括号是否成对闭合def is_valid_parentheses(s): stack [] mapping {): (, ]: [, }: {} for ch in s: if ch in ([{: stack.append(ch) elif ch in )]}: if not stack or stack[-1] ! mapping[ch]: return False stack.pop() return not stack # 栈空说明全部匹配这段代码的核心思路遇到左括号压栈遇到右括号就看栈顶是否匹配。这个模式在其他场景比如撤销操作、路径解析里会反复出现。5.2 用collections.deque实现队列先进先出队列Queue是“先进先出”FIFO, First In First Out的结构就像排队买奶茶先来的先服务。Python里用list模拟队列是最容易踩坑的做法queue [] queue.append(1) front queue.pop(0) # 弹出第一个元素但这是O(n)操作问题出在pop(0)弹出头部之后后面所有元素都要往前移一位复杂度O(n)。数据量大时效率堪忧。正确的做法是用collections.deque双端队列from collections import deque queue deque() queue.append(1) # 从右端入队 queue.append(2) front queue.popleft() # 从左端出队O(1) print(front) # 1deque在两端增删都是O(1)是Python里实现栈、队列的首选工具。处理高频操作时务必优先考虑它。很多面试题里用list当队列出现超时换成deque就通过了原因就在这。5.3 链表理解“指向”和“引用”的本质链表Linked List是数据结构课程的另一个基石。它的存储方式和list的“连续内存”完全不同每个节点node里存着数据和一个“指向下一个节点的引用”节点之间通过引用串联起来。就像寻宝游戏中每张纸条上写着下一个线索的位置。一个最简单的单链表节点类可以这样定义class ListNode: def __init__(self, val0, nextNone): self.val val self.next next创建三个节点并连起来node1 ListNode(1) node2 ListNode(2) node3 ListNode(3) node1.next node2 node2.next node3遍历链表def traverse(head): current head while current: print(current.val) current current.next链表的优势是插入和删除节点非常快——只要修改相邻节点的next指针就行不像数组需要搬动其他元素。缺点是按下标访问很慢必须从头开始一个个遍历。理解和操作链表的核心就是“引用”两个字节点之间互相引用操作时千万注意顺序。比如在节点A和B之间插入新节点X必须先让X.next指向B再让A.next指向X顺序反了会导致B丢失。这种“指针操作顺序”的考究是训练细节思维的好载体。5.4 树与二叉树用Python代码写一棵最简单的树树Tree是一种分层结构有根节点、子节点、叶子节点。现实中的文件目录、公司组织架构、网页的DOM结构全是树。二叉树是每个节点最多有两个子节点的树左孩子、右孩子。二叉树在搜索场景下衍生了二叉搜索树BST在排序场景下衍生了堆Heap在文件系统里衍生了B树、B树——可以说树的变体撑起了计算机半壁江山。在Python里用类和递归可以轻松表达一棵树class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right # 构建一棵树 root TreeNode(1) root.left TreeNode(2) root.right TreeNode(3) root.left.left TreeNode(4)这棵树的样子1 / \ 2 3 / 4树最常用的操作是遍历分三种前序根左右、中序左根右、后序左右根。比如中序遍历二叉搜索树的结果是有序序列这是树的经典性质。用递归写起来非常优雅def inorder_traversal(node): if node is None: return [] result [] result.extend(inorder_traversal(node.left)) result.append(node.val) result.extend(inorder_traversal(node.right)) return result print(inorder_traversal(root)) # 输出 [4, 2, 1, 3]如果你能独立写好这三种遍历的递归版本并理解递归函数每一次进入、返回时发生了什么树的大部分相关题目就解锁了。如果一开始理解不了递归就手动拿笔在纸上画一棵小球数然后模拟一遍函数调用过程多画几遍自然就通了。6. 实战从数据到API的三个完整案例学数据结构最怕的就是“学了一堆名词却不知道往哪用”。这一节我用三个贴近真实工作的案例把前面说过的知识点串起来。6.1 案例一用字典和集合完成日志分析假设你有一份服务器访问日志要统计两个信息每个用户访问了多少次以及“今天访问过的用户”里有多少是“昨天也访问过”的。日志格式简化成一行一个用户名today_log [alice, bob, alice, carol, bob, alice] yesterday_log [alice, dave, bob]统计访问次数用字典visit_count {} for user in today_log: visit_count[user] visit_count.get(user, 0) 1 print(visit_count) # {alice: 3, bob: 2, carol: 1}然后找出连续两天都访问的用户用集合today_set set(today_log) yesterday_set set(yesterday_log) both today_set yesterday_set print(both) # {alice, bob}整个处理过程不需要引入任何第三方库纯内置数据结构就完成了。这里字典负责计数集合适用于去重和关系运算各司其职。再写一个需要找访问次数最多的用户的逻辑可以用max和字典的items方法配合most_active max(visit_count.items(), keylambda x: x[1]) print(most_active) # (alice, 3)max函数结合key参数指定比较依据是处理“按某个维度取极值”的常用模式。6.2 案例二用栈实现路径简化很多系统里都会遇到路径字符串处理比如把/home/user/../docs//file这样的路径简化成/home/user/docs/file。这个需求很适合用栈来做。思路是按/把路径切分成若干段遇到正常文件名就压栈遇到..就弹出栈顶表示回到上级目录遇到.或空串就忽略。def simplify_path(path): stack [] for part in path.split(/): if part or part .: continue elif part ..: if stack: stack.pop() else: stack.append(part) return / /.join(stack) print(simplify_path(/home/user/../docs//file)) # 输出 /home/docs/file这个案例的巧妙之处在于正常压栈、遇到..弹栈正好对应了路径的“进入目录”和“退回上级目录”栈的后进先出特性和目录的层级语义完美匹配。你在LeetCode刷到的很多中等难度题本质就是“识别出这是栈场景然后套模板”。6.3 案例三用队列实现BFS和“好友推荐”队列最常见的应用场景是广度优先搜索BFS比如在社交关系图中从一个人出发一步步扩展到他的朋友、朋友的朋友……这种“逐层扩散”的思路非常适合队列。假设好友关系用字典表示key是人value是他的好友列表friends_map { alice: [bob, carol], bob: [alice, dave], carol: [alice, eve], dave: [bob], eve: [carol] }找出alice的“二度好友”即alice朋友的朋友排除本身和直接好友from collections import deque def get_friends_of_friends(start, friends_map, max_depth2): queue deque([(start, 0)]) # (当前人, 当前深度) visited set([start]) result set() while queue: person, depth queue.popleft() if depth max_depth: continue for friend in friends_map.get(person, []): if friend not in visited: visited.add(friend) if depth 1 max_depth: result.add(friend) else: queue.append((friend, depth 1)) return result print(get_friends_of_friends(alice, friends_map)) # 输出 {dave, eve}这个例子里队列保证了一层一层往外扩展先处理深度为0的alice然后把她朋友加入队列并标记深度1再从深度1的人扩展出深度2。visited集合防止走回头路和无限循环。这个模式就是BFS的核心框架刷题和实际项目中都用得上。7. 我踩过的坑和给你的避坑清单文章最后把这几年用Python数据结构时踩过的坑集中整理一下。每一条都是真实经历有些坑甚至踩了不止一次。坑一字典的键值对遍历时修改字典d {a: 1, b: 2, c: 3} for k in d: if d[k] 2: del d[k] # 报错RuntimeError: dictionary changed size during iterationPython不允许在遍历字典的同时修改字典的大小。正确做法是先收集要删除的键遍历结束后统一删除to_delete [] for k, v in d.items(): if v 2: to_delete.append(k) for k in to_delete: del d[k]或者更优雅地用字典推导式重建d {k: v for k, v in d.items() if v ! 2}坑二集合与字典的字面量写法混淆empty {} # 这是空字典不是空集合 empty_set set() # 创建空集合必须用set()因为花括号{}在Python里有歧义解释器优先把它理解为字典。如果你想要空集合必须显式用set()。这个坑在初学阶段特别容易犯并且运行时报错信息不一定直观找半天才能发现是类型错了。坑三sorted和list.sort的区别list.sort()是列表的方法它是就地排序直接修改原列表并返回Nonesorted()是内置函数可以接受任何可迭代对象返回一个新的排序后的列表不修改原对象新手常见的错误是my_list [3, 1, 2] new_list my_list.sort() # new_list是Nonesort没有返回值正确写法my_list.sort() # 原地排序直接用my_list new_list sorted(my_list) # 返回新列表原列表不变如果排序后还需要使用原列表用sorted不需要原列表了用list.sort会更节省内存。细节虽小只是很多线上bug由此而来。坑四复杂度差距在数据量大时才真正体现当我第一次用O(n²)的写法处理10万条数据时程序跑了将近一分钟换成分组聚集的写法利用字典O(1)查找瞬间完成。这个差距在学习阶段不容易有感觉因为数据量太小了。建议你一定要造一次大数据量的场景试试亲身体会“算法复杂度决定了程序能不能用”这句话的分量。可以在本地生成100万条数据分别用双循环和集合优化版跑一遍亲眼看看时间差异。坑五别忽略collections模块这个宝藏除了dequecollections模块里还有Counter计数器、defaultdict带默认值的字典、namedtuple带字段名的元组等工具都是日常开发里经常能用上的。比如Counter实现前面的词频统计一行代码from collections import Counter counter Counter(text) print(counter.most_common(3)) # 出现次数最多的3个字符合理使用标准库能帮你写出更简洁、更不容易错的代码。但前提是你先理解底层原理——不然你只是会用而不是真的懂。数据结构这条路说到底没有捷径。先掌握Python内置四种结构再逐步深入到栈、队列、链表、树再到复杂度分析和算法应用每一步都能在实际问题中验证、迭代。把每一段代码亲手敲一遍把每一个坑亲自踩一遍这些知识就会真正长在你身上。
返回列表