Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python元组和列表 #172

Open
Qingquan-Li opened this issue Jul 7, 2021 · 0 comments
Open

Python元组和列表 #172

Qingquan-Li opened this issue Jul 7, 2021 · 0 comments
Labels

Comments

@Qingquan-Li
Copy link
Owner

Qingquan-Li commented Jul 7, 2021

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

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

  • list 因为可变,所以是一个 over-allocate 的 array;
    • 由于列表是动态的,所以它需要存储指针,来指向对应的元素。
    • 列表空间分配的过程:为了减小每次增加/删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1)
    • 对于元组,元组长度大小固定,元素不可变,所以存储空间固定。
  • tuple 因为不可变,所以长度大小固定。

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

应用场景:

  • 需要存储多个元素(多项数据)时,可使用数组或链表。

数组、链表的区别:

  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']


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

  • 元组是静态的,长度大小固定,无法增加删减或者改变(immutable)
  • 列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)
# 无法修改元组元素
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
  • 由于列表是动态的,所以它需要存储指针,来指向对应的元素。
  • 列表空间分配的过程:为了减小每次增加/删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1)
  • 对于元组,元组长度大小固定,元素不可变,所以存储空间固定。
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

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



十、元组和列表的性能

  • 元组要比列表更加轻量级一些,元组的性能速度要略优于列表。
    • Python 会在后台,对静态数据做一些资源缓存(resource caching)。
      通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。
      但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。
      这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。
  • 如果想要增加、删减或者改变元素,那么列表显然更优。因为对于元组,需要通过新建一个元组来完成。

初始化一个相同元素的元组和列表分别所需的时间:元组的初始化速度,要比列表快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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant