Python数据结构底层实现浅析——list和tuple

Python数据结构底层实现浅析——list和tuple

提到python中list和tuple的底层实现,就要回到最基本的数据结构——线性表
把一组数据元素,通常它们还是同一类型,看成一个序列,序列里的位置和顺序都代表着有意义的信息或者关系,把这样的数据序列就是线性表。线性表(表)应用非常广泛,是复杂结构的实现基础。
线性表中的线性,来源于每个元素的上下文环境是顺序衔接的,即除首元素之外,表中每个元素仅有一个前驱元素;除末尾元素之外,每个元素都仅有一个后继元素。所以称之为线性表。

如何组织线性表里的数据,并且为之配置高效的操作方法是线性表实现的关键。
这里要考虑两个方面:1.保存元素数据信息和元素顺序信息要适应计算机内存的管理,2.考虑重要操作的实现效率,如定位访问更改和删除,元素遍历等操作。
所以提出两种表的基本模型。1.顺序表:将表元素直接顺序的放在一块划分的连续存储区内,所以元素的顺序关系由存储顺序自然表示。2.链接表:将表元素放在通过链接构造起来的系列存储块里。两种模型各有长短。

下面主要看顺序表。
顺序表的基本实现方式十分简单。通常元素类型相同,故每个元素的存储量相同,等距安排同等大小的存储单元顺序存储元素数据即可,直接映射到内存里。表中任何元素的位置计算都很简单,只要知道了第一个元素的内存位置,+逻辑地址(表内元素索引0,1,2,。。。)*单个元素的存储单元数目即可,故索引操作和元素访问是O(1)时间。
然而表元素大小差异巨大,所需的存储单元不一致的话,内存没法安排统一的线性顺序。只需将实际元素数据存储在另外的存储区,在顺序表原来的内存单元里保存每个元素数据的label(标识,即引用信息,在独立存储区的地址链接,实现对元素的间接访问),由于地址链接的大小肯定是一致的,所以依然保持了内存的顺序性映射。这样的结构又叫索引结构
这里注意外部对表每一步操作后,存储块的容量(max)和当前元素个数(num)必须要实时记录,当做附加数据信息,以支持各种操作。直接访问元素显然是O(1)时间,按下标循环并检查和处理的话,O(n)时间复杂度。
尤其注意的是变动操作中的保序问题,尾部操作和定点位置的操作的差别。
在表尾部操作显然简单,判断表没满(max>num),就可以根据num直接找到尾部,执行操作。如果在其他指定位置如i,加入新元素,i这个位置被新元素占了,原来i位置的元素直接移到到末尾num处,这种就是非保序操作,O(1)时间,变动操作后,元素的顺序与原来顺序不要求一致。若要求保住原有的元素顺序,就要将原来的i位置元素后移到i+1位置,原来i+1位置元素后移到i+2,后续元素均要顺移,受元素个数影响,保序操作最坏和平均时间都是O(n)。删除操作情况类似。

总结顺序表:优点在于O(1)时间直接按位置访问元素,元素存储紧凑,除表元素外,存储区外只需要O(1)空间存放少量辅助信息(max和num)。缺点:表一旦大,需要的连续内存空间就很大。存储块划分之后,不可更新,造成闲置浪费。加入删除操作要移动很多元素,效率低。

一个顺序表包括两部分信息,一个是元素数据的集合,一个是前面的实现操作的记录辅助信息(max和num)。这样就可以采用一体式和分离式结构。分离式结构将max,num,链接信息(元素实际存放的内存地址)放一起,内存中元素存储区与他们分开存储。一体式顾名思义。

终于要考虑表存储区的容量大小问题了。存储区满了之后,肯定要分配更大的存储区去替代。一体式结构就在这里失效了。又引入分离式技术,不改变表的标识,另外申请更大的存储区,然后把已有的元素复制到新存储区,更新存储链接,就可以继续加新元素。这样可扩充容量的表就是动态顺序表
对于动态顺序表,前端插入和定位插入,每一次操作都与长度有关,如果表规模从0增长到n,整个增长过程插入的时间就为O(n2).
后端插入(O(1)),再考虑容量更新的问题。涉及到空闲内存单元的量和更替存储区的频度问题。一种策略是线性增长,比如,每次替换存储区时加10个存储单元,那么假设从0容量到1000,每加10个元素,换一次存储执行一次元素复制,总复制次数=10+20+30+。。。990=49500,考虑增长到n容量(n次后端插入),就有1/20×n2次复制。虽然每次尾端插入为O(1)时间,但一次插入操作的平均代价变成了O(n),并不理想。
另外一种方式为加倍策略。每次存储量更新时翻倍,考虑容量从0增加到1024,复制次数为1+2+4+。。。512=1023. 对于容量n,表从0到n的整个增长过程,执行尾端插入,存储区每次更新加倍,元素复制次数也是O(n),插入操作的平均时间变成了O(1)。比前者具有优势。但实际上也是以空间换时间。

至此,回顾python的list和tuple,均采用了顺序表的结构。Tuple不支持改变内部状态的操作,看可更新的List。
List的下表索引和更新高效,为O(1),且元素有序,只能采用连续表,元素数据保存在连续的存储区里,且删除,插入是要求保序的,尾部插入O(1),定位插入O(n),n为长度;list可以不断加入新元素,且对象标识(用内置id函数可以看其内存地址)不变,可以看出使用了分离式存储技术,是动态顺序表,存储区可扩充替换。
根据python的documentation,List存储区的扩充实际采用以下原则:空表分配8个元素的存储区,插入(append,insert等)元素满了之后,换4倍大的存储区(未超出50000),若表非常大了(元素超过50000个),换存储区时容量加倍。
综上,python的list采用的是连续存储的分离式结构的动态顺序表,且插入和删除要求保序。使用时,一定要考虑尾端插入和定位插入的效率差异。

当然,python没有提供检查list对象的存储容量的函数或方法,也没有人为设置容量的操作,容量更新的操作由python解释器自动解决分配。降低了编程负担和人为错误,也限制了表的使用方式,无法使用其他策略优化。

你可能感兴趣的:(数据结构)