在开发中,很多时候我们需要一个数据结构来存放不同的对象,传统的数组无法实现这个需求(只能存储同一类型数据),但是一般语言都内置了专门的集合容器,今天我们就来聊下这些集合容器对象是如何实现的以及他们之间的异同。
Java集合对象-ArrayList
首先看java,java中的集合对象分别在java.util.Collection接口和java.util.Map接口中
Collection接口中有三个字接口,这些接口有着不同的实现类
List:有序的可重复的容器
Queue:队列
Set:无序的不重复的容器(这里的无序是指不按添加顺序排序,而是按照hash运输计算之后排序)
Map类中则定义了散列表相关的数据结构即(K,V)系统
接下来我们打开List接口查看源码
可以看到该接口中定义了一些对元素操作的api,分析一下他们的作用
Size():返回该list中的元素数量总和
isEmpty():如果该list为空返回true否则返回fasle
contains(Object):当前列表若包含该Object,返回为true, 若不包含,则返回falseiterator(): iterator: 返回一个迭代器,用于迭代该list中的所有元素
toArray():Object[]构造的一个Object数组,然后进行数据拷贝,此时进行转型就会产生ClassCastException
toArray(T[]):T[]根据参数数组的类型,构造了一个对应类型的,长度跟ArrayList的size一致的空数组,虽然方法本身还是以 Object数组的形式返回结果,不过由于构造数组使用的ComponentType跟需要转型的ComponentType一致,就不会产生转型异常
add(E):向list尾部添加一个指定元素,添加成功返回ture否则返回fasle
remove(E):删除指定元素,删除成功返回ture否则返回fasle
containsAll(Collection>):判断当前list是否包含指定Collection中的所有元素若包含,返回为true, 若不包含,则返回false
addAll(Collection extends E>):用于将指定 collection 中的所有元素添加到列表的尾部。如果 List 集合对象由于调用 addAll 方法而发生更改,则返回 true。
addAll(int index,Collection<?extends E>c):用于将 collection 中的所有元素添加到列表的指定位置。如果 List 集合对象由于调用 addAll 方法而发生更改,则返回 true。
removeAll(Collection>):删除该list中的指定collection,删除成功返回ture否则返回fasle
基本的api就讲到这,接下来我们看他的具体实现ArrayList(也叫动态数组)
可以看到ArrayList实现类的结构并不复杂。
serialVersionUID :序列化UID
DEFAULT_CAPACITY:定义了一个10的常量
private static final Object[] EMPTY_ELEMENTDATA = {}:一个空Objcet数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:一个空Object数组
transient Object[] elementData; 我猜该结构就是数据真正存储的地方
接下来我们看ArrayList的构造函数:
有参构造需要传入一个int类型的参数initialCapacity 该参数决定了ArrayList的初始化大小,当该参数大于0时。初始化一个Object的数组,该数组的容量为该参数,如果initialCapacity传入的值为0,则构建一个空Object类型的数组
无参构造则是直接初始一个空数组。
此外ArrayList还有一个代参构造器,此构造器则是接受一个Collection
此构造器用于构造一个带有已有集合对象的ArrayList
接下来我们实例化一个ArrayList,并使用它的add方法,来演示调用add方法之后整个ArrayList的机制
add方法源码.
在调用strings.add之后首先modCount会加1
紧接着调用上面的add方法,把要添加的元素e,初始化为10的数组elementData,和当前ArrayList的size传入add方法,这个size当前是null
add 方法会首先检测size是否等于elementData的元素长度.如果等于,则会使用一个扩容方法grow,如不等于,直接把元素e赋值给elementData[e],且size + 1。
我们来看看grow方法传入当前一个ArrayList的size当前为1,这里返回一个扩容后的elementData数组,并把原将列表元素从旧数组逐一copy到新数组elementData,并且调用了newCapacity传入当前size,最后回收旧数组
接下查看newCapacity方法源码
我们可以发现,每当调用add方法时,原数组容量不够,会扩容1.5倍,原来这样就是实现了一个动态数组!
Python中list的内部结构
python语言中的list使用起来就比较简单了,其底层和java一样是使用动态数组来实现的,且他们都只是存放对象的引用。
list 对象在 Python 内部,由 PyListObject 结构体表示,定义于头文件 Include/listobject.h 中:
PyListObject 底层由一个数组实现,关键字段是以下 3 个:
- ob_item ,指向 动态数组 的指针,数组保存元素对象指针;
- allocated ,动态数组总长度,即列表当前的 容量 ;
- ob_size ,当前元素个数,即列表当前的 长度
如果 list 对象内部数组已用满,再添加元素时则需要进行扩容。 append 等方法在操作时都会对内部数组进行检查,如需扩容则调用 list_resize 函数。在 list_resize 函数, Python 重新分配一个长度更大的数组并替换旧数组。为避免频繁扩容, Python 每次都会为内部数组预留一定的裕量(1.25倍)。
假设列表元素 l 保存 3 个元素,内部数组长度为 3 ,已满。当我们调用 append 方法向列表尾部追加元素时,需要对内部数组进行扩容。扩容步骤如下:
- 分配一个更大的数组,假设长度为 6 ,预留一定裕量避免频繁扩容;
- 将列表元素从旧数组逐一转移到新数组;
- 以新数组替换旧数组,并更新 allocated 字段;
- 回收旧数组。
由于内部数组扩容时,需要将列表元素从旧数组拷贝到新数组,时间复杂度为 O(n)O(n) ,开销较大,需要尽量避免。为此, Python 在为内部数组扩容时,会预留一定裕量,一般是 1/81/8 左右。假设为长度为 1000 的列表对象扩容, Python 会预留大约 125 个空闲位置,分配一个长度 1125 的新数组。
由于扩容操作的存在, append 方法最坏情况下时间复杂度为 O(n)O(n) 。由于扩容操作不会频繁发生,将扩容操作时的元素拷贝开销平摊到多个 append 操作中,平均时间复杂度还是 O(1)O(1) 。
总结一下:python和java中的list都是我们最常用的数据结构,其查询和尾部插入数据速度极快为O(1) ,但又因为其底层实现是一个数组结构,因此从头部插入和删除元素都需要移动每个元素的位置,因此效率不佳为O(n)