深入认识Python内建类型——list
注:本篇是根据教程学习记录的笔记,部分内容与教程是相同的,因为转载需要填链接,但是没有,所以填的原创,如果侵权会直接删除。
“深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型。
list是日常开发中最常用的内建类型之一,掌握好它的底层知识,无论是对数据结构基础知识的理解,还是对开发效率的提升,应该都是有一定帮助的。
问题:
- list对象支持哪些操作?时间复杂度、空间复杂度分别是多少?
- 试分析append和insert这两个典型方法的时间复杂度。
- 头部添加元素时性能较差,如何解决?
1 常用方法
大家对于list应该是比较熟悉的,我们先列举一些常用的方法:
-
append:向尾部追加元素
>>> l = [1, 2, 3] >>> l.append(4) >>> l [1, 2, 3, 4]
-
pop:弹出元素(默认从尾部弹出元素,也可以通过index参数从指定位置弹出)
>>> l = [1, 2, 3, 4] >>> l.pop() 4 >>> l [1, 2, 3]
>>> l = [1, 2, 3, 4] >>> l.pop(0) # 指定index 1 >>> l [2, 3, 4]
-
insert:在指定位置插入元素
>>> l = [1, 2, 3] >>> l.insert(0, 4) >>> l [4, 1, 2, 3]
-
index:查找指定元素第一次出现位置的下标
>>> l = [1, 2, 3, 4] >>> l.index(1) 0
-
extend:用一个可迭代对象扩展列表——元素逐一追加到尾部
>>> l = [1, 2, 3] >>> l.extend({ 1: 2, 3: 4}) >>> l [1, 2, 3, 1, 3]
-
count:计算元素出现的次数
>>> l = [1, 2, 3, 1] >>> l.count(1) 2 >>> l.count(2) 1
-
reverse:将列表反转(注意这里是直接在原列表上进行操作,可以和切片区分下)
>>> l = [1, 2, 3] >>> l.reverse() >>> l [3, 2, 1]
-
clear:将列表清空
>>> l = [1, 2, 3] >>> l.clear() >>> l []
小结:list的操作总体比较简单,但是要注意的是:由于list底层是由数组实现的,对应的各类插入和删除操作就会由于数组的特型而在复杂度上有所差别,例如:通过insert()在头部插入元素时,需要挪动整个列表,此时时间复杂度为O(n),而append()直接在尾部插入元素时,时间复杂度为O(1)。在使用时要注意时空复杂度问题(后续我会结合源码详细介绍)。
题外话
:list的操作有很多,后续我会对典型操作进行源码分析,还有很多操作可能就需要大家自行去学习了解了,但是本质上都是大同小异的,掌握好数组以及Python对此在源码处理上的一些技巧就能熟练掌握了。这里我结合自己的学习、工作、面试经历,总结了一点问题和心得:
- list为什么可以使用负数索引——从源码角度看是因为判断了索引输入为负数的情况,会做一个相应的处理:索引值+列表长度。例如:索引为-1,列表长度为3,最终的索引就是2,也就是最后一个元素的索引。对比一下Java中的数组,如果使用负数索引,会报错索引越界,在网上查能否有相关方法实现负数索引:有人说用一个类来包装,也有的说用一个字典去映射负数和正确的索引,也有的直接给出了无法做到的答案。相关的做法应该是有很多的,但同时也不要忽视了安全性等相关问题。这里提出这个点也是我自己觉得比较有趣的一个地方吧,“索引值+列表长度”其实是一个很简单的做法,但总感觉又有那么一点一般人想不出来的巧妙,hh
- 以pop()和insert()为例,这两个方法有一个共同的参数:索引。对于pop(),如果索引值大于等于列表长度,则会报错;而对于insert(),如果索引值大于等于列表长度则统一将元素插入到列表最后。从源码角度看其实就是insert()对于索引参数做了一个判断处理,当它大于列表长度时,会将索引值更改为列表长度,例如:index = 5,而list = [1, 2, 3],源码处理时,会将index置为3。所以如果开发者乐意的话,应该可以对pop()也采用相同的操作,我个人认为这里应该是有其他的考虑的,例如安全性等问题,当然也有可能只是写成了这样而已,hh
- reverse(),reversed(),切片的区别。个人认为本质上这三者其实没啥关系,大家如果容易弄混可能还是因为不够熟练。重点问题可能在切片以及深拷贝、浅拷贝的问题上,后续我会详细介绍相关内容。
- append()和extend()的区别。面试题好像会问这个,也是很基础的知识了。如果换个角度问可能会更有趣些:l.append(3)和l.extend(3)的效果是不是一样的?答:不一样,后者会直接报错,hh
不知不觉就写了这么多,相比上次写str相关的内容还是要顺畅多了(捂脸)。相关的小知识应该是还有很多的,大家可以在学习的过程中多找一些问题和资料来融会贯通,当然我个人认为如果能把底层的逻辑搞清楚,其他的应该都不成问题~
2 list的内部结构:PyListObject
-
源码如下:
typedef struct { PyObject_VAR_HEAD /* Vector of pointers to list elements. list[0] is ob_item[0], etc. */ PyObject **ob_item; /* ob_item contains space for 'allocated' elements. The number * currently in use is ob_size. * Invariants: * 0 <= ob_size <= allocated * len(list) == ob_size * ob_item == NULL implies ob_size == allocated == 0 * list.sort() temporarily sets allocated to -1 to detect mutations. * * Items must normally not be NULL, except during construction when * the list is not yet visible outside the function that builds it. */ Py_ssize_t allocated;