Qingquan-Li / blog

My Blog
https://Qingquan-Li.github.io/blog/
132 stars 16 forks source link

Python元组和列表 #172

Open Qingquan-Li opened 3 years ago

Qingquan-Li commented 3 years ago

一、元组和列表都属于基础数据结构中的“数组”

tuple 和 list 的内部实现都是 array(数组)的形式,而不是 linked list(链表)。


基础数据结构中的数组、链表

应用场景:

数组、链表的区别:

  1. 数组的元素都在一起,即在内存中都是相连的(紧靠在一起的)。 链表的元素是分开的,链表中的元素可存储在内存的任何地方,其中每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。

  2. 数组的元素带编号,元素的位置称为索引。 假设有一个数组array = [1, 2, 3],第1个元素(索引为0的元素)是1(array[0] = 1)。

  3. 数组的读取速度很快。 需要随机地读取元素时,数组的效率很高,数组支持随机访问,因为知道其中每个元素的地址,可迅速找到数组的任何元素。 在链表中,元素并非都靠在一起的,必须先读取第一个元素,根据其中的地址再读取第二个元素,以此类推。链表只能顺序访问,要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机访问意味着可直接跳到第十个元素。

  4. 链表的插入和删除速度很快。 需要在中间插入元素时,使用链表插入元素很简单,只需修改它前面的那个元素指向的地址;而使用数组时,则必须将后面的元素都向后移。 删除元素,链表也是更好的选择,因为只需修改前一个元素指向的地址即可;而使用数组时,删除元素后,必须将后面的元素都向前移。

常见的数组和列表操作的运行时间( O~(n)~ 为线性时间,O~(1)~ 为常量时间):

数组 链表
读取 O(1) O(n)
插入(内存不足插入会失败) O(n) O(1)
删除 O(n) O(1)



二、元组和列表都是一个可以放置任意数据类型的有序集合

在绝大多数编程语言中,集合的数据类型必须一致。不过,对于 Python 的元组和列表来说,并无此要求:

tup = ('Alice', 18)  # 元组中同时含有int和string类型的元素
tup

# Jupyter Notebook Out:
('Alice', 18)
l = [1, 2, 'hello', 'world']  # 列表中同时含有int和string类型的元素
l

# Jupyter Notebook Out:
[1, 2, 'hello', 'world']



三、元组是静态的,列表是动态的

# 无法修改元组元素
tup = (1, 2, 3, 4)
tup[3] = 40
tup

# Jupyter Notebook Out:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-0151a4c9e088> in <module>
      1 # 无法修改元组元素
      2 tup = (1, 2, 3, 4)
----> 3 tup[3] = 40
      4 tup

TypeError: 'tuple' object does not support item assignment
# 如果想对已有的元组做任何"改变",那就只能重新开辟一块内存,创建新的元组了。
tup = (1, 2, 3, 4)
new_tup = tup + (5, )  # 创建新的元组new_tup,并依次填充原元组的值
new_tup

# Jupyter Notebook Out:
(1, 2, 3, 4, 5)
# 修改列表元素
l = [1, 2, 3, 4]
l[3] = 40
l

# Jupyter Notebook Out:
[1, 2, 3, 40]
l = [1, 2, 3, 4]
l.append(5)  # 添加元素5到原列表的末尾
l

# Jupyter Notebook Out:
[1, 2, 3, 4, 5]



四、元组和列表都支持负数索引

和其他语言不同,Python 中的元组和列表都支持负数索引,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。

tup = (1, 2, 3, 4)
tup[-1]

# Jupyter Notebook Out:
4
l = [1, 2, 3, 4]
l[-2]

# Jupyter Notebook Out:
3



五、元组和列表都支持切片操作

tup = (1, 2, 3, 4)
tup[1:3]  # 返回元组中索引从1到2的子元组

# Jupyter Notebook Out:
(2, 3)
l = [1, 2, 3, 4]
l[1:10]  # 返回列表中索引从1到9的子列表

# Jupyter Notebook Out:
[2, 3, 4]



六、元组和列表都可以随意嵌套

# 元组的每一个元素也是一个元组
tup = ((1, 2, 3), (4, 5, 6))

# 列表的每一个元素也是一个列表
l = [[1, 2, 3], [4, 5]]



七、元组和列表可以通过list()tuple()函数相互转换

tuple([1, 2, 3])

# Jupyter Notebook Out:
(1, 2, 3)
list((1, 2, 3))

# Jupyter Notebook Out:
[1, 2, 3]



八、元组和列表常用的内置函数

l = [3, 2, 3, 7, 8, 1]
l = [3, 2, 3, 7, 8, 1]

# count(item) 表示统计元组 / 列表中 item 出现的次数
l.count(3)  # Jupyter Notebook Out: 2

# index(item) 表示返回元组 / 列表中 item 第一次出现的索引
l.index(7)  # Jupyter Notebook Out: 3

# list.reverse() 和 list.sort() 分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)
l.reverse()
l  # Jupyter Notebook Out: [1, 8, 7, 3, 2, 3]

l.sort()
l  # Jupyter Notebook Out: [1, 2, 3, 3, 7, 8]
tup = (3, 2, 3, 7, 8, 1)

# reversed() 和 sorted() 同样表示对列表 / 元组进行倒转和排序:
# reversed() 返回一个倒转后的迭代器(这里使用 list() 函数再将其转换为列表);
# sorted() 返回排好序的新列表。

list(reversed(tup))  # Jupyter Notebook Out: [1, 8, 7, 3, 2, 3]

sorted(tup)  # Jupyter Notebook Out: [1, 2, 3, 3, 7, 8]



九、元组和列表存储方式的差异

tup = (1, 2, 3)
tup.__sizeof__()

# Jupyter Notebook Out:
48
l = [1, 2, 3]
l.__sizeof__()

# Jupyter Notebook Out:
64
l = []

# 空列表的存储空间为40字节
l.__sizeof__()  # Jupyter Notebook Out: 40

# 加入了元素1之后,列表为其分配了可以存储4个元素的空间 (72 - 40)/8 = 4
l.append(1)
l.__sizeof__()  # Jupyter Notebook Out: 72

# 由于之前分配了空间,所以加入元素2,列表空间不变
l.append(2)
l.__sizeof__()  # Jupyter Notebook Out: 72

# 由于之前分配了空间,所以加入元素3,列表空间不变
l.append(3)
l.__sizeof__()  # Jupyter Notebook Out: 72

# 由于之前分配了空间,所以加入元素4,列表空间不变
l.append(4)
l.__sizeof__()  # Jupyter Notebook Out: 72

# 加入元素5之后,列表的空间不足,所以又额外分配了可以存储4个元素的空间
l.append(5)
l.__sizeof__()  # Jupyter Notebook Out: 104

根据以上分析元组和列表存储方式的差异,元组和列表储存空间大小差异似乎不大。 但是想象一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,还能忽略这样的差异吗?



十、元组和列表的性能

初始化一个相同元素的元组和列表分别所需的时间:元组的初始化速度,要比列表快5倍(此处使用macOS):

$ python3 -m timeit 'x=(1,2,3,4,5,6)'
20000000 loops, best of 5: 19 nsec per loop
python3 -m timeit 'x=[1,2,3,4,5,6]'
5000000 loops, best of 5: 90.3 nsec per loop

但如果是索引操作的话,两者的速度差别非常小,几乎可以忽略不计:

$ python3 -m timeit -s 'x=(1,2,3,4,5,6)' 'y=x[3]'
5000000 loops, best of 5: 43 nsec per loop
$ python3 -m timeit -s 'x=[1,2,3,4,5,6]' 'y=x[3]'
5000000 loops, best of 5: 42.1 nsec per loop



十一、元组和列表的使用场景

# 如果存储的数据和数量不变,比如有一个函数,需要返回的是一个地点的经纬度,然后直接传给前端渲染,那么肯定选用元组更合适:
def get_location():
    ......
    return (longitude, latitude)
# 如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在一周之内看了哪些用户的帖子,那么则用列表更合适:
viewer_owner_id_list = []  # 里面的每个元素记录了这个viewer一周内看过的所有owner的id
records = queryDB(viewer_id)  # 索引数据库,拿到某个viewer一周内的日志
for record in records:
    viewer_owner_id_list.append(record.id)