一、List的本质:Python中的"动态数组"
首先要明确一个关键认知:Python中的List 并非传统意义上的"链表"(ListedNode) ,而是一种动态数组(Dynamic Array)。这一点与C++的vector、Java的ArrayList本质一致。
1.1 底层逻辑与特性
传统的静态数组(如C语言的数组)在初始化时必须指定固定大小,且后续无法灵活扩容/缩容,一旦元素数量超过数组长度,就需要手动申请新的内存空间、拷贝原有元素,操作繁琐且效率低下。而Python的List则解决了这一痛点,其底层实现逻辑如下:
-
List会预先申请一块连续的内存空间(初始容量通常较小,如4个元素),用于存储元素;
-
当元素数量达到当前容量上限时,List会自动扩容(通常扩容为原容量的1.5倍或2倍,具体取决于Python版本),申请一块更大的连续内存,将原有元素拷贝到新空间,再释放旧空间;
-
当元素数量大幅减少时,List也会自动缩容(部分版本支持),避免内存浪费;
-
所有元素按顺序存储在连续内存中,因此可以通过索引(index)直接访问元素,这也是"数组"的核心优势。
这里需要区分两个易混淆的概念:容量(capacity) 和 长度(length):
-
长度(len(list)):当前List中实际存储的元素个数,随时变化;
-
容量(capacity):底层连续内存空间能容纳的最大元素个数,由Python自动管理,用户无法直接获取或修改。
1.2 与链表(Linked List)的核心区别
很多初学者会将List与链表混淆,但二者的操作效率差异极大,直接决定了刷题时的算法选择(比如LeetCode中"删除链表的倒数第N个节点"不能用List模拟,否则效率过低)。具体区别如下表:
| 操作 | Python List(动态数组) | 链表(Linked List) |
|---|---|---|
| 按索引访问(查) | O(1)(直接定位内存地址) | O(n)(需从头遍历到目标节点) |
| 头部插入/删除(增/删) | O(n)(需移动所有后续元素) | O(1)(只需修改头节点指针) |
| 尾部插入/删除(增/删) | O(1)(无需移动元素,除非扩容) | O(n)(需遍历到尾节点,双向链表除外) |
| 中间插入/删除(增/删) | O(n)(需移动插入/删除位置后的所有元素) | O(n)(需遍历到目标位置) |
| 内存占用 | 可能有冗余(扩容后未用完的空间) | 无冗余(每个节点只存储自身数据和指针) |
总结:LeetCode中涉及"频繁按索引访问、尾部增删"的题目,优先用Python List;涉及"频繁头部增删、中间插入删除且数据量较大"的题目,需考虑用链表(Python无内置链表,可自定义或用collections.deque模拟双向链表)。
二、Python List的核心操作
2.1 初始化与赋值
刷题中最常用的3种初始化方式,根据场景选择:
python
# 1. 空列表(最常用,如存储结果、临时变量)
lst = []
# 2. 初始化时传入元素(已知初始数据,如题目给出的数组)
lst = [1, 2, 3, 4, 5]
# 3. 初始化指定长度、默认值(适用于需要固定长度的场景,如动态规划的dp数组)
lst = [0] * 5 # 结果:[0, 0, 0, 0, 0]
lst = [None] * 3 # 结果:[None, None, None]
【拓展】
列表推导式(List Comprehension) 适合基于已有列表生成新列表的场景。
1. 基础列表推导式(最常用)
核心语法:new_lst = [表达式 for 元素 in 可迭代对象]时间复杂度与普通遍历一致(O (n))。
python
# 场景1:基于已有列表生成新列表(如你提到的平方操作)
a = [1, 2, 3, 4]
b = [i*i for i in a] # 结果:[1, 4, 9, 16]
# 场景2:生成指定范围的列表(替代range+append)
# 生成1-10的奇数列表(Hot100中"两数之和""三数之和"常用来构造测试用例)
odd_nums = [x for x in range(1, 11) if x % 2 != 0] # 结果:[1, 3, 5, 7, 9]
# 场景3:过滤列表元素(如LeetCode"移除元素"预处理)
nums = [3, 2, 2, 3]
target = 3
filtered_nums = [num for num in nums if num != target] # 结果:[2, 2]
2. 带条件判断的列表推导式
核心语法:new_lst = [表达式1 if 条件 else 表达式2 for 元素 in 可迭代对象]适用于需要 "按需生成元素" 的场景,比如 LeetCode 中 "将数组中的偶数翻倍,奇数不变"。
python
# 场景:LeetCode"调整数组元素"类题目
nums = [1, 2, 3, 4, 5]
# 偶数乘2,奇数保持不变
adjusted_nums = [num*2 if num % 2 == 0 else num for num in nums] # 结果:[1, 4, 3, 8, 5]
# 场景:生成n阶单位矩阵(矩阵乘法/线性代数相关题目常用)
n = 4
# 生成4x4单位矩阵,对角线元素(i==j)为1,其余为0
matrix = [[1 if i == j else 0 for j in range(n)] for i in range(n)]
# 结果:
# [[1, 0, 0, 0],
# [0, 1, 0, 0],
# [0, 0, 1, 0],
# [0, 0, 0, 1]]
2.2 核心增删改查操作
(1)查:按索引访问、遍历
-
按索引访问:
lst[i],索引从0开始,支持负索引(lst[-1]表示最后一个元素),时间复杂度O(1); -
遍历元素:两种常用方式,时间复杂度均为O(n)
python# 方式1:直接遍历元素(最常用,无需关注索引) for num in lst: print(num) # 方式2:遍历索引+元素(需要索引时用,如双指针题目) for i, num in enumerate(lst): print(i, num)如果不了解enumerate()函数的使用,可以查看我的另一篇博客:
-
查找元素位置:
lst.index(target),返回第一个匹配元素的索引,无匹配元素则报错,时间复杂度O(n);刷题时常用target in lst判断元素是否存在(时间复杂度O(n))。
(2)改:修改指定索引元素
lst[index] = new_val,直接通过索引修改,时间复杂度O(1)「高频」,例如:
python
lst = [1, 2, 3]
lst[1] = 4 # 结果:[1, 4, 3]
(3)增:添加元素
-
尾部添加:
lst.append(val),时间复杂度O(1)(除非扩容,扩容时为O(n),但平均复杂度仍为O(1))「高频」; -
指定位置插入:
lst.insert(index, val),时间复杂度O(n)(需移动后续元素),刷题时尽量避免频繁使用(会导致效率低下); -
合并两个列表:
lst1.extend(lst2)(将lst2的元素添加到lst1尾部),时间复杂度O(k)(k为lst2的长度),优于lst1 + lst2(会创建新列表,时间复杂度O(n+k))。
(4)删:删除元素
-
按索引删除:
del lst[index],时间复杂度O(n)(需移动后续元素)「高频」; -
删除最后一个元素:
lst.pop(),时间复杂度O(1)「高频」;按索引删除:lst.pop(index),时间复杂度O(n); -
按元素删除:
lst.remove(val),删除第一个匹配的元素,无匹配元素则报错,时间复杂度O(n); -
清空列表:
lst.clear(),时间复杂度O(n)(释放所有元素)。
2.3 常用进阶操作
-
切片(Slice):
lst[start:end:step],截取列表的一部分,返回新列表,时间复杂度O(k)(k为切片长度)。
python
lst = [1, 2, 3, 4, 5]
lst[1:3] # 从索引1到2(不包含3):[2, 3]
lst[::2] # 步长为2,取所有奇数索引:[1, 3, 5]
lst[::-1] # 步长为-1,反转列表:[5, 4, 3, 2, 1]
-
⚠️ 注意:切片会创建新列表,若列表过大,频繁切片会占用额外内存,可考虑用双指针替代。
-
排序:
lst.sort(),原地排序(修改原列表),时间复杂度O(nlogn);sorted(lst),返回新的排序后列表,原列表不变「高频」。刷题时常用lst.sort(key=lambda x: x)自定义排序规则(如按元素绝对值排序、按二维数组的第二列排序)。 -
去重:
list(set(lst)),先将列表转为集合(自动去重),再转回列表,时间复杂度O(n),但会打乱原有的元素顺序;若需保留原顺序,可结合字典(Python 3.7+ 字典有序):
python
lst = [3, 1, 2, 1, 3, 4]
# 转集合去重 → 无序
s = set(lst) # 结果可能是 {1,2,3,4}(顺序不固定)
# 转回列表 → 顺序被打乱
new_lst = list(s)
print(new_lst) # 可能输出 [1,2,3,4] 或 [2,1,4,3] 等,和原顺序不一致
lst = [2, 1, 2, 3, 1] lst_unique = list(dict.fromkeys(lst))
# 保留原顺序:[2, 1, 3]
- 统计元素出现次数:
lst.count(val),时间复杂度O(n),适用于需要统计频率的题目
总结:
| 分类 | 函数 / 方法 | 核心作用 | 简单示例 |
|---|---|---|---|
| 增 | append(x) |
在列表末尾添加单个元素 x(直接修改原列表) | lst = [1,2]; lst.append(3) → [1,2,3] |
extend(iter) |
把可迭代对象(如列表、字符串)的元素逐个添加到列表末尾 | lst = [1,2]; lst.extend([3,4]) → [1,2,3,4] |
|
insert(idx, x) |
在指定索引idx位置插入元素 x(后面元素后移) |
lst = [1,3]; lst.insert(1,2) → [1,2,3] |
|
| 删 | remove(x) |
删除列表中第一个出现的元素 x(无该元素则报错) | lst = [1,2,2,3]; lst.remove(2) → [1,2,3] |
pop([idx]) |
删除指定索引idx的元素(默认删最后一个),返回被删除的元素 |
lst = [1,2,3]; lst.pop(1) → 返回2,列表变为[1,3] |
|
clear() |
清空列表所有元素(原列表变为空) | lst = [1,2]; lst.clear() → [] |
|
del lst[idx] |
(关键字)删除指定索引 / 切片的元素(直接修改原列表) | lst = [1,2,3]; del lst[1] → [1,3] |
|
| 改 | lst[idx] = x |
直接修改指定索引位置的元素值 | lst = [1,2]; lst[1] = 3 → [1,3] |
reverse() |
反转列表元素顺序(直接修改原列表) | lst = [1,2,3]; lst.reverse() → [3,2,1] |
|
sort(key=None, reverse=False) |
对列表排序(默认升序,reverse=True降序;直接修改原列表) |
lst = [3,1,2]; lst.sort() → [1,2,3];lst.sort(reverse=True) → [3,2,1] |
|
| 查 | index(x, [start, end]) |
查找元素 x 在列表中第一个出现的索引(指定 start/end 限定范围,无则报错) | lst = [1,2,3]; lst.index(2) → 1 |
count(x) |
统计元素 x 在列表中出现的次数 | lst = [1,2,2,3]; lst.count(2) → 2 |
|
len(lst) |
(内置函数)返回列表元素个数 | lst = [1,2,3]; len(lst) → 3 |
|
x in lst |
(运算符)判断元素 x 是否在列表中,返回布尔值 | 1 in [1,2,3] → True;4 in [1,2,3] → False |
|
| 复制 | copy() |
浅拷贝列表(返回新列表,原列表修改不影响新列表) | lst = [1,2]; new_lst = lst.copy() → new_lst = [1,2] |
lst[:] |
切片方式浅拷贝(和 copy () 等价) | lst = [1,2]; new_lst = lst[:] → new_lst = [1,2] |
|
| 其他 | copy.deepcopy(lst) |
(需导入 copy 模块)深拷贝(嵌套列表也完全复制) | import copy; lst = [[1],2]; new_lst = copy.deepcopy(lst) |
sorted(lst) |
(内置函数)返回排序后的新列表(原列表不变) | lst = [3,1,2]; sorted(lst) → [1,2,3](lst 仍为[3,1,2]) |
|
enumerate(lst) |
(内置函数)返回迭代器,包含 (索引,元素) 对 | for idx, val in enumerate([1,2]): print(idx, val) → 0 1 / 1 2 |
|
max(lst)/min(lst) |
(内置函数)返回列表中最大 / 最小值(元素需可比较) | max([1,2,3]) → 3;min([1,2,3]) → 1 |
|
sum(lst) |
(内置函数)返回列表所有元素的和(元素需为数字) | sum([1,2,3]) → 6 |
修改原列表 vs 返回新列表:
- 直接修改原列表的方法:
append()、extend()、insert()、remove()、pop()、reverse()、sort(); - 返回新列表 / 值的:
copy()、sorted()、index()、count()、len()、max()、min()、sum()。
三、注意事项:
在做List相关的Hot100题目时,很多人会因忽略细节导致代码超时或出错:
-
避免频繁在List头部插入/删除元素:头部操作时间复杂度为O(n),若数据量较大(如n>10^4),频繁操作会导致超时;
-
慎用
lst + lst2合并列表:每次合并都会创建新列表,若需多次合并,优先用extend();
lst + lst2 的底层逻辑不是 "在原列表上追加",而是:
- 申请一块新的内存空间 ,大小 =
len(lst) + len(lst2); - 把
lst的所有元素复制到新空间; - 再把
lst2的所有元素复制到新空间; - 返回这个新列表,原列表
lst本身不会改变。 - 如果写
lst=lst+lst2,则相当于把lst这个变量名重新指向新列表,原列表的内存会被垃圾回收。
而 lst.extend(lst2) 的逻辑是:
-
直接在
lst原有的内存空间后 "扩容"(按需申请少量新内存,而非全部); -
把
lst2的元素逐个追加 到lst末尾,不复制 lst 原有元素; -
直接修改原列表,无新列表创建。
-
注意List的可变特性:函数中修改传入的List,会直接修改原列表(因为List是引用类型),若需保留原列表,需传入切片
lst.copy()或lst[:]; -
排序后索引变化:对List排序后,元素的原始索引会被打乱,若题目需要保留原始索引(如"两数之和"),需提前存储索引与元素的对应关系;
-
边界条件处理:刷题时务必考虑List为空(
len(lst) == 0)、List只有一个元素、索引越界等边界情况,这是避免WA(Wrong Answer)的关键。