arraylist 的扩容机制_第1篇 C++ 数据结构--ArrayList的实现

常用语言中的动态数组的示例包括Java和C#中的ArrayList,C ++中的Vector,.NET中的List <T>,Python的list等

我们这里实现的动态数组与上述各种语言中的相似,动态数组的大小可以在运行时动态修改。动态数组的元素占用连续的内存块,一旦创建,就无法更改其大小。 当内存空间所剩无几,就可以分配更大的内存块,将内容从原始数组复制到该新空间,然后继续填充可用的"插槽"。动态数组在创建时会分配预定数量的内存,然后在需要时以一定的比例增长。 这些参数,初始大小和增长因子在性能方面至关重要。 如果初始大小和增长因子较小,那么您最终将不得不经常重新分配内存,这对性能方面并不理想; 另一方面,如果分配的内存块过高,则可能会有大量空闲的堆内存块,并且调整大小操作可能需要更长的时间。 这里的权衡是非常重要的。

其实我这里的动态数组的实现,内存分配机制是模仿,C++标准库中的vector/string,他们从内存池中申请的内存块数量是上一次申请内存块数量的2倍。这个是增长因子充分平衡了性能和内存空间的消耗。

类接口

C语言编写一个动态整数数组,但要兼容不同的基本数据类型,你可能要需要编写较为复杂宏函数来模拟C++泛型相同的效果。而C++本身已经集成了模板和类的垃圾回收机制,以模板为基础的泛型技术可以很轻松地编写出管理任何数据类型和自动内存回收的动态数组-v-!!,那么实现动态数组的首选语言就是C++了,我们定义了动态数组的类接口。

#define CAPACITY 15

构造函数的实现

//默认的构造函数

析构函数的实现

显式定义构造函数是一个非常好的习惯,对于很多的内存回收的文章讲述的仅仅是delete[]操作释放堆内存,而没有将指向堆内存的T类型指针重置为nullptr,我认为这是一个很好的习惯,C风格中的回收内存的小技巧,这个是值得继承的好习惯,我为什么这么说?若将内部d_arr的T类型指针在delete[]前,你可以尝试用一个全局变量p来缓存该指针,且d_arr没有重置为nullptr的话,你再次使用该全局变量p,会仍然访问到原来指针所指向内存区域的数据,虽然原有的内存返回给堆管理器了,但原本的数据值还在内存块,对于程序的数据保密性不是个好主意。

//内存回收

内存扩容的实现

expand()是一个私有的内部成员函数,它内部完成了向内存池申请比现有内存更大的内存空间(是原有内存空间的2倍),然后将原有内存空间中的元素拷贝到新的内存空间,最后将旧的内存空间释放返还给内存池。具体示意图如下:

9d2e8dfc3add1c8a703ed504cf68a93b.png

expand成员函数的实现

//内部扩容操作

shrink()成员函数的实现

关于如何将超出额度的空闲的内存返还给内存池,我这里定制的策略的是在每次pop/或者remove操作过程中,我们都检测d_capacity/d_size的比值,如果该比值达到3,我们就调用shrink()成员函数,将额外闲置的内存返还给内存池,仅保留的内存总量是以使用的2倍,也就是空闲的内存仅仅是以使用的1倍多一点

ab3729c9db62f5340618a2a1966512b1.png

如上图示例所示,我们假设分配了32个内存块给顺序表,在操作顺序表的过程当中,已存在的元素是10个的话,我们会将原有的内存块所见缩减为20个内存块,当缩减后的空闲内存块是已用内存块的1倍多一点。

//内存收缩操作

重载operator [] (int)

我们希望顺序表是可以类似Python的list那样提供逆序访问的能力,比方说,对于一个长度为9的列表,我们通过下标 a[-1]等价于访问a[8]的元素,请参见如下图。

17751c3a614ed2f81fce229084fddc9d.png

实现类似Python列表的访问能力非常简单,上图我们知道对应位置的顺序index(用m表示)和逆序index(用n表示)的绝对值的和等于该列表的长度(用length表示,始终是非负数),我们得到如下简单的结论。

若 n<0且-length ≤ n < 0;

那么m=length+n,且0≤m≤length-1

即n的有效范围 可以 0≤n≤length-1 或 -length n <0

实际上我们若提供一个的负整数n时,(-length n < 0),最终会转换回顺序index的非负整数,那么operator[]的索引操作符重写如下。在重载operator[]的同时,我们需要面size_t类型的d_size转换为带符号的整数的问题,具体的问题描述可以看我之前写的随笔《C/C++ 有符号和无符号数字的迷途》

template 

删除特定位值的元素

其实没什么好说的,删除中间位值的元素,实际上就是从传入索引位值算起,后续的元素依次将其元素值向各自的前一个元素拷贝并覆盖原先的元素值,拷贝过程结束后,d_size减1,最后一个元素会被作为一个闲置的内存块留作以后的备用,这个过程的时间消耗是O(n)原理图如下:

dd1e04b4e32712ebe10f32cca6516e01.png

remove成员函数实现

//删除特定的元素

removeAt()成员函数实现

//删除指定索引的元素

从特定位值插入元素

从特定的位值插入元素,际上就是从传入索引位值算起,后续的元素依次将其元素值向各自的后一个元素拷贝并覆盖原先的元素值,拷贝过程结束后,修改指定索引的元素的值,d_size增加1,备用内存块即减少1。但插入元素很可能导致分配的内存块空间所剩无几,所以每次插入操作前,都需要通过比较d_size和d_capacity两个计数器,如果d_size等于或大于d_capacity即会触发内部的expand函数申请内存扩容。expand成员函数的行为可以查看上文的描述。插入元素本身的时间消耗就是O(n),加之expand的时间消耗也是O(n),所以此时的时间成本是最高的。

316f4053ddf6363c6a538157e5eb4b48.png
//按索引位置插入值

查找元素

//查找特定元素值的索引,约定返回size_t的上限值表示找不到元素

清空所有元素

//清空整个列表

尾部插入

//尾部插入元素

尾部弹出

//尾部弹出数据

清空整个列表

template 

显示数组

//显示数组

最后顺序表的API测试

#include 

以下这是测试结果,

9744023784566acb44300926e48854f7.png

结语

这个ArrayList是后面讨论排序算法,设计模式,以及C++其他高级特性会经常用到的,并且这个顺序表用到了很多C++的特性,它的内存管理和C++标准的顺序容器是类似的。OK性能测试的话,等我将全系列的数据结构写完,再与C++内置的容器做个性能分析

读后有收获,可以扫码给笔者打赏杯咖啡。

c68028383bb6e78007da4fb8e2a72030.gif
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值