MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python列表新增元素的高效手段

2021-02-227.0k 阅读

使用 append 方法

在 Python 列表操作中,append 方法是最基础且常用的用于在列表末尾添加单个元素的手段。从本质上来说,append 方法是列表对象的内置方法,它将给定的元素追加到列表的尾部。这种操作的时间复杂度在平均情况下为 O(1),这是因为大多数情况下,Python 的列表实现能够直接在末尾添加元素而不需要对现有元素进行移动等操作。

来看一个简单的代码示例:

my_list = [1, 2, 3]
new_element = 4
my_list.append(new_element)
print(my_list)

在上述代码中,我们定义了一个列表 my_list,然后定义了一个新元素 new_element,通过 append 方法将新元素添加到了列表 my_list 的末尾,最后输出列表 my_list,结果为 [1, 2, 3, 4]

append 方法对于添加单个元素来说非常高效,因为它直接在列表的内存空间末尾进行操作。Python 的列表在内存中是动态分配的,当空间不足时,列表会自动重新分配更大的内存空间来容纳新的元素。不过,这种重新分配内存的操作并非每次 append 都会发生,而是在当前空间不足以容纳新元素时才会进行,这就是为什么平均时间复杂度是 O(1)。

当列表的元素数量较多时,append 方法依然保持较高的效率。例如:

big_list = []
for i in range(100000):
    big_list.append(i)

上述代码使用 append 方法向一个空列表中添加 100,000 个元素,这个过程在实际执行中速度较快,体现了 append 方法在添加单个元素场景下的高效性。

使用 extend 方法

extend 方法用于在列表末尾一次性追加另一个可迭代对象(如列表、元组、集合等)中的所有元素。从底层实现来看,extend 方法会遍历传入的可迭代对象,并将其中的每个元素逐一添加到列表的末尾。这种操作的时间复杂度为 O(k),其中 k 是被追加的可迭代对象的长度。

以下是使用 extend 方法添加列表元素的代码示例:

list1 = [1, 2, 3]
list2 = [4, 5, 6]
list1.extend(list2)
print(list1)

在这个例子中,我们有两个列表 list1list2,通过 list1.extend(list2) 操作,将 list2 中的所有元素追加到了 list1 的末尾,最终 list1 的值变为 [1, 2, 3, 4, 5, 6]

extend 方法不仅仅局限于追加列表,也可以追加其他可迭代对象。例如,追加一个元组:

my_list = [1, 2]
my_tuple = (3, 4)
my_list.extend(my_tuple)
print(my_list)

上述代码将元组 my_tuple 中的元素追加到了列表 my_list 中,输出结果为 [1, 2, 3, 4]

与多次使用 append 方法相比,extend 方法在追加多个元素时效率更高。因为 extend 方法是在一次操作中完成对可迭代对象所有元素的追加,而多次 append 方法会有更多的函数调用开销。例如,对比以下两种方式:

# 使用多次 append 方法
list1 = []
for i in range(1000):
    list1.append(i)

# 使用 extend 方法
list2 = []
temp_list = list(range(1000))
list2.extend(temp_list)

虽然这两种方式最终得到的结果相同,但在性能测试中会发现,使用 extend 方法的方式执行速度更快,尤其当要追加的元素数量较大时,这种差异会更加明显。

使用 “+” 运算符

在 Python 中,可以使用 “+” 运算符来合并两个列表,从而达到在一个列表中新增多个元素的目的。从本质上讲,“+” 运算符会创建一个新的列表,这个新列表包含了两个操作数列表中的所有元素。这种操作的时间复杂度为 O(m + n),其中 m 和 n 分别是两个参与运算的列表的长度。

以下是使用 “+” 运算符合并列表的代码示例:

list_a = [1, 2, 3]
list_b = [4, 5, 6]
result = list_a + list_b
print(result)

在上述代码中,通过 “+” 运算符将 list_alist_b 合并成一个新的列表 result,输出结果为 [1, 2, 3, 4, 5, 6]

需要注意的是,与 extend 方法不同,“+” 运算符不会修改原始的列表,而是返回一个全新的列表。如果我们想要在原列表上进行修改,可以这样做:

list_c = [1, 2]
list_d = [3, 4]
list_c = list_c + list_d
print(list_c)

在这个例子中,虽然表面上看起来是在原列表 list_c 上进行了操作,但实际上是重新创建了一个新列表并赋值给 list_c

“+” 运算符在某些场景下很方便,比如在创建新列表时可以简洁地合并多个列表。但如果是在原列表上频繁地使用 “+” 运算符来添加元素,会导致较多的内存开销,因为每次都会创建新的列表。例如:

original_list = []
for i in range(1000):
    original_list = original_list + [i]

在这个循环中,每次迭代都会创建一个新的列表,随着循环次数的增加,内存开销会越来越大,效率也会逐渐降低。相比之下,extend 方法直接在原列表上进行操作,不会频繁创建新列表,因此在这种场景下 extend 方法更为高效。

使用 insert 方法

insert 方法用于在列表的指定位置插入一个元素。其本质是将指定位置及之后的元素依次向后移动一位,然后将新元素插入到指定位置。这种操作的时间复杂度为 O(n),其中 n 是列表中指定位置之后的元素数量。

以下是使用 insert 方法的代码示例:

my_list = [1, 2, 4]
my_list.insert(2, 3)
print(my_list)

在上述代码中,我们有一个列表 my_list,通过 my_list.insert(2, 3) 操作,在索引为 2 的位置插入了元素 3,最终 my_list 的值变为 [1, 2, 3, 4]

insert 方法虽然灵活,可以在任意位置插入元素,但由于涉及到元素的移动,所以在列表较大且插入位置靠前时,性能会受到较大影响。例如,在一个有 100,000 个元素的列表开头插入元素:

big_list = list(range(100000))
big_list.insert(0, -1)

这个操作会导致列表中所有 100,000 个元素都要向后移动一位,从而消耗较多的时间和资源。因此,在使用 insert 方法时,需要根据实际情况权衡插入位置和列表大小对性能的影响。如果经常需要在列表开头插入元素,使用 collections.deque 这种数据结构会更高效,deque 提供了 appendleft 方法,时间复杂度为 O(1)。

使用列表推导式

列表推导式是一种简洁的创建列表的方式,也可以用于在已有列表基础上生成包含新增元素的新列表。列表推导式在内部实现上是通过迭代器来高效生成新列表的。其基本语法为 [expression for item in iterable if condition],其中 expression 是对 item 进行处理后生成新列表元素的表达式,iterable 是可迭代对象,if condition 是可选的过滤条件。

以下是使用列表推导式在已有列表基础上新增元素的示例:

original_list = [1, 2, 3]
new_list = [num * 2 for num in original_list]
print(new_list)

在上述代码中,通过列表推导式对 original_list 中的每个元素乘以 2,生成了一个新的列表 new_list,结果为 [2, 4, 6]

如果想要在新列表中添加一些额外的元素,可以这样做:

list1 = [1, 2]
extra_elements = [3, 4]
new_list = [num for num in list1] + extra_elements
print(new_list)

在这个例子中,先通过列表推导式复制了 list1 的元素,然后使用 “+” 运算符将 extra_elements 合并进来,生成了包含新增元素的 new_list

列表推导式的优点在于其简洁性和高效性。由于它是基于迭代器实现的,在生成新列表时,内存使用相对高效,而且代码更加紧凑。与传统的循环方式相比,列表推导式在可读性和性能上都有一定优势。例如,对比以下两种方式生成新列表:

# 传统循环方式
original = [1, 2, 3]
new1 = []
for num in original:
    new1.append(num * 2)

# 列表推导式方式
new2 = [num * 2 for num in original]

虽然这两种方式实现的功能相同,但列表推导式的代码更简洁,而且在性能测试中,列表推导式的执行速度通常更快,尤其是在处理较大的可迭代对象时。

使用 itertools.chain 方法

itertools.chain 方法来自 Python 的 itertools 模块,它可以将多个可迭代对象连接起来,返回一个迭代器。虽然它本身并不直接生成列表,但可以很方便地转换为列表,从而实现类似于合并列表的效果,达到新增元素的目的。从底层实现来看,itertools.chain 方法是通过迭代器协议来逐个返回各个可迭代对象中的元素,而不会在内存中一次性创建一个大的列表。

以下是使用 itertools.chain 方法的代码示例:

import itertools

list_a = [1, 2]
list_b = [3, 4]
result = list(itertools.chain(list_a, list_b))
print(result)

在上述代码中,首先导入了 itertools 模块,然后使用 itertools.chain 方法将 list_alist_b 连接起来,最后通过 list() 函数将返回的迭代器转换为列表,输出结果为 [1, 2, 3, 4]

itertools.chain 方法在处理多个可迭代对象时非常高效,尤其是当这些可迭代对象较大时,它不会像 “+” 运算符那样一次性创建一个全新的大列表,从而减少了内存开销。例如,连接多个较大的列表:

big_list1 = list(range(100000))
big_list2 = list(range(100000, 200000))
big_list3 = list(range(200000, 300000))

result_chain = list(itertools.chain(big_list1, big_list2, big_list3))

在这个例子中,如果使用 “+” 运算符来连接这三个大列表,会在内存中一次性创建一个包含 300,000 个元素的新列表,可能会导致内存压力较大。而使用 itertools.chain 方法,它会逐个迭代这些列表的元素,在需要时才生成新的列表,从而更有效地利用内存。

使用 collections.deque 相关方法

collections.deque 是 Python 标准库中提供的一种双端队列数据结构,它支持在队列两端进行高效的添加和删除操作。与普通列表相比,deque 在某些场景下添加元素的效率更高,尤其是在队列两端添加元素时。

dequeappend 方法和列表的 append 方法类似,用于在队列右端添加一个元素,时间复杂度为 O(1)。appendleft 方法则用于在队列左端添加一个元素,时间复杂度同样为 O(1)。这与列表在开头插入元素(使用 insert(0, element))的 O(n) 时间复杂度形成鲜明对比。

以下是使用 collections.deque 的代码示例:

from collections import deque

dq = deque([1, 2])
dq.append(3)
dq.appendleft(0)
print(list(dq))

在上述代码中,首先从 collections 模块导入 deque,然后创建一个包含元素 1 和 2 的双端队列 dq。接着使用 append 方法在右端添加元素 3,使用 appendleft 方法在左端添加元素 0,最后通过 list() 函数将 deque 对象转换为列表并输出,结果为 [0, 1, 2, 3]

deque 还提供了 extendextendleft 方法。extend 方法用于在队列右端一次性追加另一个可迭代对象的所有元素,extendleft 方法则用于在队列左端一次性追加另一个可迭代对象的所有元素。例如:

dq = deque([1, 2])
new_elements = [3, 4]
dq.extend(new_elements)
dq.extendleft([0])
print(list(dq))

在这个例子中,先使用 extend 方法在右端追加 new_elements 列表中的元素,然后使用 extendleft 方法在左端追加元素 0,最终输出结果为 [0, 1, 2, 3, 4]

在需要频繁在列表两端添加元素的场景下,collections.deque 是一个非常好的选择,它能够提供比普通列表更高的效率,同时也保持了较好的灵活性。

性能对比与实际应用场景选择

为了更直观地了解上述各种列表新增元素手段的性能差异,我们可以进行一些简单的性能测试。这里使用 timeit 模块来测量不同操作的执行时间。

测试 append 方法

import timeit

def test_append():
    my_list = []
    for i in range(10000):
        my_list.append(i)
    return my_list

print(timeit.timeit(test_append, number = 100))

上述代码通过 timeit 模块测量了使用 append 方法向列表中添加 10,000 个元素,重复 100 次的总执行时间。

测试 extend 方法

import timeit

def test_extend():
    my_list = []
    temp_list = list(range(10000))
    my_list.extend(temp_list)
    return my_list

print(timeit.timeit(test_extend, number = 100))

此代码测量了使用 extend 方法一次性添加 10,000 个元素,重复 100 次的总执行时间。

测试 “+” 运算符

import timeit

def test_plus_operator():
    my_list = []
    for i in range(10000):
        my_list = my_list + [i]
    return my_list

print(timeit.timeit(test_plus_operator, number = 100))

这里测量了使用 “+” 运算符在循环中逐个添加 10,000 个元素,重复 100 次的总执行时间。

测试 insert 方法

import timeit

def test_insert():
    my_list = []
    for i in range(10000):
        my_list.insert(0, i)
    return my_list

print(timeit.timeit(test_insert, number = 100))

此代码测量了使用 insert 方法在列表开头逐个插入 10,000 个元素,重复 100 次的总执行时间。

通过实际测试,我们可以发现:

  • append 方法在添加单个元素时性能很好,平均时间复杂度为 O(1),适合逐个添加元素的场景。
  • extend 方法在添加多个元素时效率高于多次使用 append 方法,适合一次性追加一个可迭代对象所有元素的场景。
  • “+” 运算符虽然简洁,但在频繁使用时会创建较多新列表,导致内存开销大,效率相对较低,适合偶尔合并列表的场景。
  • insert 方法在插入位置靠前且列表较大时性能较差,应尽量避免在这种情况下使用,除非确实需要在特定位置精确插入元素。

在实际应用中,如果是简单的逐个添加元素,优先选择 append 方法;如果要添加多个元素,优先考虑 extend 方法;如果需要在列表开头频繁添加元素,collections.dequeappendleft 方法是更好的选择;而列表推导式、itertools.chain 等方法则在特定场景下,如生成新列表或连接多个可迭代对象时发挥高效的作用。

例如,在处理数据流,需要实时将数据逐个添加到列表中时,append 方法是最佳选择;在数据预处理阶段,可能有多个小的列表需要合并成一个大列表,此时 extenditertools.chain 方法更合适;在实现队列或栈的数据结构时,collections.deque 提供了高效的操作方法。

总之,深入理解各种列表新增元素手段的本质和性能特点,能够帮助我们在实际编程中根据具体需求选择最适合的方法,从而提高程序的运行效率和性能。