ARTICLE DETAIL

资讯详情

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

Python列表操作全解析:从基础增删改查到算法思维培养

Python列表操作全解析:从基础增删改查到算法思维培养 列表这种数据结构看着简单实际上很多算法题、业务代码、数据处理脚本里它都是绕不开的底层容器。编程农场这个系列把算法学习场景化第二期专门讲列表这个切入点很实际不背API而是把列表当成农场里一块真正要耕种、要管理、要调度的地来用。这篇文章我打算按实际动手的顺序拆一遍先讲清楚列表为什么值得认真学再固定运行环境然后把每个操作对应到具体场景最后补上最容易翻车的几个坑和一个综合案例。适合正在学Python、准备刷算法题或者刚看完系列视频想自己复现一遍的人。最值得关注的点是列表不只是增删改查它还是你理解遍历、查找、排序、切片、引用传递这些算法概念的第一块试验田。1. 列表不是语法课是算法思维的“第一块田”编程农场这个系列最吸引我的地方是它不把“列表”当成一个孤立的知识点来教而是一上来就告诉你列表是你在代码里管理批量数据的最基本工具。学算法之前你总得有一块能装东西的地方而列表就是这个地方。很多人第一次学列表先背append怎么拼再背pop有没有返回值然后做几道选择题就算过了。但真到写算法题的时候问题就变了遍历列表到底该怎么写才不容易下标错乱切片生成的到底是不是新列表两个列表做赋值操作为什么改了一个另一个也变了这些问题的本质不是API背得不熟而是你还没把列表当成一个活的数据结构来理解。1.1 为什么学算法要先过列表这一关算法题里一开始考的往往不是多复杂的巧思而是线性结构的操作能力。列表就是最典型的线性结构。你可以用它模拟队列用它存中间结果用它做滑动窗口用它保存一组候选值。很多算法题的最终输出也都是一个列表。换句话说如果你列表的基础操作不牢后面学二分、双指针、排序、递归都会像在沙地上盖房子。我自己带过一些新同事他们写的代码报错十个里有六七个都是列表问题不是访问越界就是切片用错要么是修改列表时影响了原来的数据。这些问题刷题时看着小但放到真实项目里可能就是线上数据被污染的大事故。所以编程农场把“列表”放在第二期我觉得挺合理。第一期大概率是热身第二期才真正开始碰数据结构。这一关过不了后面的栈、队列、字典、树都会受影响。1.2 编程农场的比喻列表是一块能种、能收、能调度的地为什么用农场来比喻列表因为列表和一块地实在太像了。你可以提前规划这块地种什么这是一种“创建列表”。你可以按编号找到第几垄种的是土豆这是“索引”。你可以只收割中间一片区域这是“切片”。你可以把种子追加到末尾这是“append”。你也可以拔掉某一行作物这是“pop”。你还能从左到右检查每一垄这是“遍历”。这个比喻的好处是它把抽象的内存连续存储变成了一个你能感知的场景。你再理解“列表里的元素有序排列”时就不会只是死记一个结论而是真的能想象出一块地每一垄都有固定位置。不过我要提醒一点比喻是为了辅助理解不要把它当成完全严格的模型。列表在内存里是连续空间这一点涉及到扩容、插入效率时会有影响但农场比喻不一定体现得出来。所以后面讲到具体操作复杂度时我们还是要回到Python列表本身的机制上。2. 动手之前先把运行环境固定住看编程农场的演示视频很多人会觉得一切都很顺滑。但轮到自己动手第一步就卡在环境上。这里不是要讲多复杂的配置而是把一个最基础的问题解决掉你用什么方式跑Python列表代码。如果只是学习列表我不建议上来就搭虚拟环境、装一堆依赖。Python 3.8以上的版本无论哪个操作系统标准库里的list都够用了。真正要做的决定是选择“文件脚本”还是“交互式环境”。2.1 用 Python 内置解释器还是 Jupyter我的建议是分场景如果你只是验证某个列表操作比如“切片到底返回什么”直接在终端敲python进入交互式解释器一行一行试效率最高。如果你在跑一段稍微完整的逻辑比如一个农场订单管理的小案例那就应该写成一个.py文件用python 文件名.py来执行。这样方便保存、修改和重复运行。如果你习惯用 Jupyter也可以。Jupyter 能让你分单元格看结果对教学场景很友好。但它会引入一点额外的启动成本和依赖所以新手如果只是想快速跑通不一定要用 Jupyter。我一般更推荐先从文件脚本开始因为刷算法题、写工程脚本时文件脚本是更接近真实工作的方式。交互式环境适合查细节但不利于你形成“写一段完整代码”的肌肉记忆。2.2 版本和依赖不用贪新但要统一Python 版本会影响一些语法细节但列表这门基础课Python 3.6 到 3.12 差别不大。你只需要确认一件事你和教程、和同伴用的版本别差太远就行。如果你在跑一个现成的项目项目里可能有一个requirements.txt或者pyproject.toml这时候要优先按项目文件来固定依赖。但像我们这种纯粹学列表的场景不需要额外安装第三方库。还有一个容易被忽略的小点项目文件名不要叫list.py。因为这会和Python标准库里的list概念产生不必要的混淆虽然大多数情况下不会直接报错但这种取名习惯不好。我见过有人建了个list.py后来导入别的模块时各种稀奇古怪的问题都来了。# 推荐先创建一个单独目录比如 farm-list-demo mkdir farm-list-demo cd farm-list-demo # 创建测试文件 touch demo.py # 运行 python demo.py这里先把环境固定住后面所有示例都按照这种方式执行能减少很多不必要的报错。3. 列表的每一项操作都对应农场里的一个动作列表的基础操作其实就那么几个创建、索引、切片、添加、删除、遍历。很多教程喜欢按函数列表一个一个讲我反而喜欢按“农场一天的流程”来讲因为这样你记忆的是场景不是孤立函数。3.1 创建列表从空货架到已上架商品创建列表有几种常见写法每一种都对应不同需求# 空列表相当于一个还没有摆放任何东西的空货架 inventory [] # 直接创建相当于一次性把几种作物摆上货架 crops [土豆, 玉米, 番茄] # 用 range 快速生成连续数字相当于给货架编号 shelf_ids list(range(1, 11)) print(shelf_ids) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # 用列表推导式创建相当于按规则批量上架 squares [x * x for x in range(1, 6)] print(squares) # [1, 4, 9, 16, 25]这里最值得你花时间理解的是列表推导式。它看起来只是简洁写法实际上它包含了“遍历一个序列 - 对每个元素做变换 - 生成新列表”的完整算法思维。后面学map、filter、生成器表达式逻辑和列表推导式是一脉相承的。为什么要区分空列表和直接创建的列表因为在真实业务里你经常是先创建一个空列表然后循环往里面加数据。这个模式太常见了。collected [] for i in range(5): collected.append(i * 2)3.2 索引和切片从指定货架取货索引就是给编号拿货。Python 的索引从 0 开始所以第一个元素是lst[0]不是lst[1]。这个规则刚接触时会不习惯但你要把它当成一种约定位置从0标记。还有一个容易懵的是负索引。lst[-1]表示最后一个元素lst[-2]表示倒数第二个。这在实际处理数据时非常方便尤其是在不确定列表长度时你想拿最后一个元素直接用-1就行。切片则是取出列表中一段连续区域。它的格式是lst[start:end:step]包含start不包含end。这个“含头不含尾”的规则很多人一开始会记混。crops [土豆, 玉米, 番茄, 茄子, 辣椒] # 取前三个元素 print(crops[0:3]) # [土豆, 玉米, 番茄] # 从第三个取到末尾 print(crops[2:]) # [番茄, 茄子, 辣椒] # 每隔一个取一个 print(crops[::2]) # [土豆, 番茄, 辣椒] # 反转列表的一个取巧方式 print(crops[::-1]) # [辣椒, 茄子, 番茄, 玉米, 土豆]切片这一步我建议你亲自在交互式环境里敲一敲看看每一种组合的输出。因为切片规则不难但组合起来变化很多脑内推演容易错。尤其是start和end都省略时lst[:]是复制整个列表的常用方式。3.3 append、pop、insert补货、下架、插队append在末尾添加元素这是列表最常用的修改操作。它的时间复杂度是 O(1) 摊还也就是说绝大多数情况下很快。pop默认删除最后一个元素并返回被删除的值你也可以传索引进去删除指定位置的元素。insert则在指定位置插入元素但这个操作会触发后续元素整体后移所以如果列表很长insert的开销会比较大。inventory [土豆, 玉米] # 补货 inventory.append(番茄) print(inventory) # [土豆, 玉米, 番茄] # 下架最后一个 last inventory.pop() print(last) # 番茄 print(inventory) # [土豆, 玉米] # 在开头插队 inventory.insert(0, 水稻) print(inventory) # [水稻, 土豆, 玉米]为什么我要强调append和insert的性能差异因为列表在内存里是连续存储的。在末尾追加通常只需要在预留空间里直接写但在开头插入后面的所有元素都要往后挪动一位。如果你需要频繁在头部操作就应该考虑用collections.deque这是专门为两端操作优化的结构。不过对于新手先不用急着换结构。你要做的是建立这个意识列表在尾部高效的增加删除在头部插入是要代价的。这个意识在算法题里非常重要因为很多时候你以为是代码问题其实是你对数据结构的操作复杂度不敏感。3.4 遍历列表流水线巡检遍历列表有两种基本方式一种是遍历元素本身一种是遍历索引。crops [土豆, 玉米, 番茄] # 方式一直接遍历元素 for crop in crops: print(crop) # 方式二同时拿到索引和元素 for idx, crop in enumerate(crops): print(f第 {idx} 个作物是 {crop})enumerate是我很推荐你早点习惯的函数。很多人在需要索引时会写for i in range(len(crops))然后里面再写crops[i]。这样写也没错但多了一层“先取索引再取元素”的间接过程容易出错。enumerate直接把索引和元素一起给你代码更清晰也不容易越界。遍历是很多算法的底子。你在列表上做的查找、统计、累加、过滤本质上都是遍历加上一个条件判断。# 统计长度大于2的作物名称数量 crops [土豆, 玉米, 番茄, 水稻] count 0 for crop in crops: if len(crop) 2: count 1 print(count)这个例子看起来简单但它就是很多算法题的雏形遍历一个列表对每个元素做判断最后汇总结果。所以不要因为简单就不练要练到闭着眼睛都能写出来的程度。4. 从列表到“算法手感”查找、反转、排序到底在练什么学会了基础操作之后就要往前一步把列表当成算法的载体来练习。这里的核心不是背几个函数而是理解遍历、比较、交换、合并这些算法概念在列表上是怎么落地的。4.1 查找从顺序找到条件找查找是最常见的需求。Python 里可以用in来判断一个元素是否在列表中crops [土豆, 玉米, 番茄] print(玉米 in crops) # True print(水稻 in crops) # Falsein在列表中默认是顺序查找时间复杂度是 O(n)。如果列表是有序的你可以用bisect模块做二分查找效率会高很多。但先不要急着上二分你至少要理解同样是查找有序条件和无序条件的时间开销是不一样的。再看“按条件查找”。很多业务场景不是判断“存不存在”而是找到所有满足条件的元素。这就是过滤。numbers [3, 7, 2, 8, 5] # 找出所有大于4的数 result [x for x in numbers if x 4] print(result) # [7, 8, 5]这段代码的内在逻辑是遍历 - 判断 - 收集。你在编程农场里做类似“找出明天可以收割的作物”时用的也是同一套逻辑。4.2 反转和切片反转操作量级怎么算列表反转有三种常见写法lst [1, 2, 3, 4] # 方式一原地反转 lst.reverse() print(lst) # [4, 3, 2, 1] # 方式二切片产生新列表 lst2 [1, 2, 3, 4] reversed_lst lst2[::-1] print(reversed_lst) # [4, 3, 2, 1] # 方式三内置函数返回迭代器 lst3 [1, 2, 3, 4] reversed_iter reversed(lst3) print(list(reversed_iter)) # [4, 3, 2, 1]这里有一个容易被忽略的细节reverse()是原地修改不会返回新列表[::-1]会产生新列表原列表不变reversed()返回一个迭代器必须用list()转成列表才能输出内容。实际写代码时要先问自己我是想修改原数据还是想得到一个反转后的新数据这两个需求对应不同写法。搞混了就会出现“我明明调了 reverse原列表为什么变了我都没意识到”的问题。4.3 排序先用内置函数跑通再研究排序流程Python 的list.sort()和sorted()是最常用的排序方式。前者原地排序后者返回新列表。numbers [3, 1, 4, 1, 5, 9, 2] numbers.sort() print(numbers) # [1, 1, 2, 3, 4, 5, 9] words [pear, apple, banana] sorted_words sorted(words) print(sorted_words) # [apple, banana, pear] print(words) # [pear, apple, banana]原列表没变排序这一块我建议你分两阶段学第一阶段就是用内置方法把数据排好掌握key参数怎么自定义排序规则。比如按字符串长度排序words [pear, apple, blueberry] words.sort(keylen) print(words) # [pear, apple, blueberry]第二阶段你在算法题里遇到“排序算法”时再去手写冒泡排序、选择排序、快速排序。手写排序不是为了生产中自己造轮子而是为了理解比较、交换、分治这些算法思想。很多人一看到“数据结构排序算法”这个热搜词就害怕其实第一步很简单先用内置函数跑通排序再逐渐打开黑盒看内部流程。5. 最容易翻车的四个坑引用拷贝、越界、切片、原地修改列表的坑不在于概念多深而在于一些操作的结果和直觉不一致。我在这里整理四个最常踩的坑每一个都是真实项目里出过问题的。5.1 直接赋值不是复制是给同一块地挂了两块牌子这是列表第一大坑。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]很多人写这段代码时以为b a是把a复制一份给b结果b改了a也变了。原因在于列表是可变对象b a只是让b也指向同一个内存地址。修改b就是修改同一份数据。想真正复制一份列表有几种方式b a.copy() b list(a) b a[:]这三种方式都适用。但要注意这些都是浅拷贝。如果列表里元素是可变对象比如列表套列表那内层列表仍然共享。matrix [[1, 2], [3, 4]] copy_matrix matrix.copy() copy_matrix[0].append(99) print(matrix) # [[1, 2, 99], [3, 4]]如果你需要彻底复制所有嵌套对象得用copy.deepcopy。在算法题里如果你只是处理一维列表浅拷贝足够但如果处理二维列表、嵌套结构就必须注意深浅拷贝的区别。5.2 越界索引报错不算意外遇到负索引才容易懵IndexError: list index out of range是所有Python初学者都见过的报错。它的原因很简单你访问的下标超过了列表实际长度减1。crops [土豆, 玉米, 番茄] print(crops[3]) # IndexError: list index out of range但更隐蔽的问题是负索引。很多人知道lst[-1]是最后一个元素但不知道lst[-4]在长度为3的列表上也会报错。负索引的范围是-len(lst)到-1超过这个范围同样越界。排查越界问题时不要只看代码表面要看一下len(lst)到底是多少。很多时候列表是在循环里不断添加或删除的长度变化不是你预期的那样自然就会越界。5.3 切片的结果是新列表但列表里的元素可能还是原对象切片lst[:]会生成一个新列表这一点很多人知道。但切片生成的新列表里元素是不是也被复制了一份答案是没有。元素仍然是原对象的引用。如果元素是不可变类型比如整数、字符串那你几乎感觉不到差异。但如果元素是可变对象比如子列表、字典就会有坑。matrix [[1, 2], [3, 4]] sub matrix[:] sub[0][0] 99 print(matrix) # [[99, 2], [3, 4]]在这个例子里sub是新列表但sub[0]和matrix[0]还是同一个子列表。所以修改sub[0][0]matrix也跟着变了。用切片做复制时一定要先确认你的列表是一维的、元素是简单类型。如果是嵌套结构就要考虑深浅拷贝问题。5.4 原地修改和返回新列表使用习惯不一样很多教程会把reverse()、sort()、append()等操作放在一起讲但它们有一个关键区别有些是原地修改有些是返回新列表。下表可以帮你快速回忆操作是否原地修改返回值lst.append(x)是Nonelst.pop()是被移除的元素lst.sort()是Nonelst.reverse()是Nonesorted(lst)否新列表lst[::-1]否新列表lst.copy()否新列表这个表看着简单但实际写代码时经常有人写错。比如lst [3, 1, 2] new_lst lst.sort() print(new_lst) # None这里new_lst是None因为sort()原地排序返回None。如果你想得到排序后的新列表应该用new_lst sorted(lst)。为什么这个坑这么常见因为很多人在学习时把“操作函数”和“返回值”混在一起记没有仔细区分哪些操作是修改对象本身。这一点在面试、笔试里也经常被考到。6. 用一个农场订单管理的小案例把列表所有操作串起来前面都是零散的知识点现在我们用一个完整的案例把它们串起来。这个案例不求复杂但会把创建、修改、遍历、排序、切片、查找都用上。6.1 场景描述和代码假设编程农场里面每天要管理一批订单每个订单有一个编号和对应的作物名。你需要完成这几件事创建订单列表。处理一个退订把指定编号的订单移除。统计今天有多少个订单涉及“番茄”。按订单编号排序。取出前3个订单进行加急处理。# 1. 创建订单列表每个订单用二元元组表示 (订单号, 作物名) orders [ (101, 土豆), (102, 玉米), (103, 番茄), (104, 番茄), (105, 水稻), ] # 2. 处理退订假设要移除订单号为103的订单 orders [order for order in orders if order[0] ! 103] print(orders) # 3. 统计番茄订单数量 tomato_count sum(1 for order in orders if order[1] 番茄) print(番茄订单数:, tomato_count) # 4. 按订单编号排序默认就是按第一个元素升序排序 orders.sort(keylambda x: x[0]) print(排序后:, orders) # 5. 取出前3个订单加急处理 urgent_orders orders[:3] print(加急订单:, urgent_orders)这里用到了列表推导式做过滤、sum加生成器表达式做统计、sort加key做排序、切片取出前N个。每一个都是列表的常见用法。6.2 输出结果和判断标准这段代码如果你能不看答案自己写出来并且能解释每一行的作用那列表的基础操作基本就过关了。运行后的输出大致应该长这样[(101, 土豆), (102, 玉米), (104, 番茄), (105, 水稻)] 番茄订单数: 1 排序后: [(101, 土豆), (102, 玉米), (104, 番茄), (105, 水稻)] 加急订单: [(101, 土豆), (102, 玉米), (104, 番茄)]判断标准很简单退订后订单103不会出现在结果里。番茄订单数先退订了103所以只剩104一个结果是1。排序函数正常执行原列表被修改。切片取出的前3个订单顺序正确。如果你跑出来的结果和预期不一致先看哪一步出了问题再倒回去检查。这个“先跑通、再改参数、再对照预期看输出”的流程才是学编程的正确方式。7. 列表的边界什么时候该换元组、字典和集合很多教程讲完列表就结束了很少提哪些场景不该用列表。这导致新手有一种错觉所有数据都可以往列表里塞。实际上列表虽然灵活但它不是万能的。7.1 列表不是万能的有几种情况你最好换别的结构第一如果你需要“按键取值”而不是“按下标取值”应该用字典。比如你想保存每块田的农作物数量用字典更合适field_crops {田1: 土豆, 田2: 玉米} print(field_crops[田1]) # 土豆第二如果你需要保证元素不重复并且不关心顺序应该用集合。列表去重虽然能写但效率不如集合语义清晰。crops [土豆, 玉米, 土豆, 番茄] unique_crops list(set(crops)) print(unique_crops) # 顺序不确定但去重了第三如果你希望数据创建后不被修改应该用元组。比如一组坐标、固定配置项用元组比列表更安全也能提高一点性能。location (116.4, 39.9) # 经度、纬度7.2 选择数据结构的判断标准怎么判断该用哪种结构我一般会问三个问题我需不需要频繁按下标顺序访问如果需要列表合适。我需不需要按名字或键去查找如果需要字典合适。我需不需要自动去重或做集合运算如果需要集合合适。这三个问题能解决90%的选型困惑。至于更深层的性能差异得等你实际遇到大数据量任务时再回头研究也不迟。现阶段先把列表、元组、字典、集合的使用边界搞清楚比背一堆复杂度分析更有用。8. 排查链路列表相关报错和异常先看哪里文章最后补一个排查链路。列表相关的报错翻来覆去就那么几类但每次排查的思路都差不多。8.1 报错类型和现场先看报错的类型IndexError: list index out of range说明访问了不存在的下标。TypeError: NoneType object is not iterable说明你对一个None值做了遍历常见于sort()、append()这类原地操作被误以为是返回新列表。ValueError: list.remove(x): x not in list说明你想删除的元素不在列表里。AttributeError: tuple object has no attribute append说明你误把一个元组当成了列表。看到报错后不要急着改代码先确认现场是哪一行报错操作对象是什么类型这个对象是怎么来的。8.2 排查顺序我自己的排查顺序一般是这样的先看报错行是在访问、修改还是遍历。再看被操作的对象是列表还是元组或者是不是变成了None。接着打印len(lst)和lst的值确认运行时数据和你预期的一致。再检查是不是在循环里修改了列表的长度导致下一次循环索引失效。最后检查是否涉及多个变量指向同一个列表导致误改。比如下面这段代码就很容易踩坑lst [1, 2, 3] for i in range(len(lst)): if lst[i] 2: lst.pop(i)在遍历列表的过程中删除元素会导致索引错位。运行到这里lst长度变成2但range(len(lst))已经生成了[0, 1, 2]到i2时访问lst[2]就会越界。更稳妥的做法是生成一个新列表lst [x for x in lst if x ! 2]或者先收集要删除的下标再统一处理。这一点在批量任务里特别重要不要一边遍历一边修改原始数据结构除非你非常清楚你在做什么。排查这类问题时最快的方式不是盯着代码看而是加上几个print把每一步的中间状态打出来。很多问题看到中间结果之后原因马上就清楚了。这也是我在实际工作中最常用的调试方式。如果你能把列表的这些基础操作、常见坑点和排查思路都跑通那后面的字典、集合、栈、队列学起来会轻松很多。列表这一课值得多花点时间把它练扎实。
返回列表