常用语言中的动态数组的示例包括Java和C#中的ArrayList,C ++中的Vector,.NET中的List <>,Python的list等
我们这里实现的动态数组与上述各种语言中的相似,动态数组的大小可以在运行时动态修改。动态数组的元素占用连续的内存块,一旦创建,就无法更改其大小。 当内存空间所剩无几,就可以分配更大的内存块,将内容从原始数组复制到该新空间,然后继续填充可用的"插槽"。动态数组在创建时会分配预定数量的内存,然后在需要时以一定的比例增长。 这些参数,初始大小和增长因子在性能方面至关重要。 如果初始大小和增长因子较小,那么您最终将不得不经常重新分配内存,这对性能方面并不理想; 另一方面,如果分配的内存块过高,则可能会有大量空闲的堆内存块,并且调整大小操作可能需要更长的时间。 这里的权衡是非常重要的。
其实我这里的动态数组的实现,内存分配机制是模仿,C++标准库中的vector/string,他们从内存池中申请的内存块数量是上一次申请内存块数量的2倍。这个是增长因子充分平衡了性能和内存空间的消耗。
类接口
C语言编写一个动态整数数组,但要兼容不同的基本数据类型,你可能要需要编写较为复杂宏函数来模拟C++泛型相同的效果。而C++本身已经集成了模板和类的垃圾回收机制,以模板为基础的泛型技术可以很轻松地编写出管理任何数据类型和自动内存回收的动态数组-v-!!,那么实现动态数组的首选语言就是C++了,我们定义了动态数组的类接口。
#define CAPACITY 15
#define SHK_FACTOR 3
#define EXP_FACTOR 2
template <typename T>
class ArrayList
{
private:
T *d_arr;
size_t d_capacity;
size_t d_size;
void expand(void);
void shrink(void);
long cvt_signed_number(size_t n);
inline void set_capacity(size_t n);
public:
//默认的构造函数
ArrayList();
//自定义的构造函数
ArrayList(const size_t &&n);
//自定义的构造函数
ArrayList(const T *data, size_t n);
//copy构造函数
ArrayList(const ArrayList &oth);
//move构造函数
ArrayList(ArrayList &&oth) : d_size(oth.d_size);
~ArrayList();
//下标访问
T &operator[](long n);
//ArrayLIst尺寸
size_t size() const;
//ArrayList当前分配内存数
size_t capacity() const;
//交换两个ArrayList对象
void swap(ArrayList &oth);
//尾部插入
void push_back(const T &v);
void show_array();
//查找特定元素值的索引,约定返回size_t的上限值表示找不到元素
inline size_t find(const T &v);
//按值删除
bool remove(const T &v);
//删除指定索引的值
void removeAt(long n);
//按索引位置插入值
void insert(size_t ix, T v);
//尾部弹出数据
T pop();
//清空整个容器
void clean();
};
构造函数的实现
//默认的构造函数
template <class T>
ArrayList<T>::ArrayList()
{
d_size = 0;
d_capacity = CAPACITY;
d_arr = new T[d_capacity];
}
//自定义的构造函数
template <class T>
ArrayList<T>::ArrayList(const size_t &&n) : d_size(n)
{
set_capacity(n);
d_arr = new T[d_capacity];
}
//自定义的构造函数
template <class T>
ArrayList<T>::ArrayList(const T *data, size_t n)
: d_capacity(CAPACITY)
{
if (n > 0)
{
std::cout << "ArrayList 普通函数调用" << std::endl;
d_arr = new T[n];
for (size_t i = 0; i < n; i++)
{
*(d_arr + i) = *(data + i);
}
d_size = n;
}
};
//copy构造函数
template <class T>
ArrayList<T>::ArrayList(const ArrayList &oth)
: d_size(oth.d_size)
{
std::cout << "ArrayList copy函数调用" << std::endl;
set_capacity(oth.d_size);
d_arr = new T[d_capacity];
for (size_t i = 0; i < oth.d_size; i++)
{
*(d_arr + i) = *(oth.d_arr + i);
}
};
//move构造函数
template <class T>
ArrayList<T>::ArrayList(ArrayList &&oth) : d_size(oth.d_size)
{
std::cout << "ArrayList move函数调用" << std::endl;
d_arr = oth.d_arr;
oth.d_size = 0;
oth.d_arr = nullptr;
}
析构函数的实现
显式定义构造函数是一个非常好的习惯,对于很多的内存回收的文章讲述的仅仅是delete[]操作释放堆内存,而没有将指向堆内存的T类型指针重置为nullptr,我认为这是一个很好的习惯,C风格中的回收内存的小技巧,这个是值得继承的好习惯,我为什么这么说?
若将内部d_arr的T类型指针在delete[]前,你可以尝试用一个全局变量p来缓存该指针,且d_arr没有重置为nullptr的话,你再次使用该全局变量p,会仍然访问到原来指针所指向内存区域的数据,虽然原有的内存返回给堆管理器了,但原本的数据值还在内存块,对于程序的数据保密性不是个好主意。
//内存回收
template <class T>
ArrayList<T>::~ArrayList()
{
std::cout << "ArrayList析构函数" << std::endl;
if (d_arr != nullptr)
{
delete[] d_arr;
}
}
内存扩容的实现
expand()是一个私有的内部成员函数,它内部完成了向内存池申请比现有内存更大的内存空间(是原有内存空间的2倍),然后将原有内存空间中的元素拷贝到新的内存空间,最后将旧的内存空间释放返还给内存池。具体示意图如下:
expand成员函数的实现
//内部扩容操作
template <class T>
void ArrayList<T>::expand()
{
d_capacity = d_size * EXP_FACTOR;
T *tmp = new T[d_capacity];
for (auto i = 0; i < d_size; i++)
{
*(tmp + i) = *(d_arr + i);
}
delete[] d_arr;
d_arr = tmp;
}
shrink()成员函数的实现
关于如何将超出额度的空闲的内存返还给内存池,我这里定制的策略的是在每次pop/或者remove操作过程中,我们都检测d_capacity/d_size的比值,如果该比值达到3,我们就调用shrink()成员函数,将额外闲置的内存返还给内存池,仅保留的内存总量是以使用的2倍,也就是空闲的内存仅仅是以使用的1倍多一点。
如上图示例所示,我们假设分配了32个内存块给顺序表,在操作顺序表的过程当中,已存在的元素是10个的话,我们会将原有的内存块所见缩减为20个内存块,当缩减后的空闲内存块是已用内存块的1倍多一点。
//内存收缩操作
template <class T>
void ArrayList<T>::shrink(){
size_t k = d_capacity / d_size;
if (k >= SHK_FACTOR)
{
d_capacity = d_size * EXP_FACTOR;
T *tmp = new T[d_capacity];
for (auto i = 0; i < d_size; i++)
{
*(tmp + i) = *(d_arr + i);
}
delete[] d_arr;
d_arr = tmp;
}
}
重载operator [] (int)
我们希望顺序表是可以类似Python的list那样提供逆序访问的能力,比方说,对于一个长度为9的列表,我们通过下标 a[-1]等价于访问a[8]的元素,请参见如下图。
实现类似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 <class T>
inline long ArrayList<T>::cvt_signed_number(size_t n)
{
size_t x = static_cast<size_t>(std::numeric_limits<long>::max());
if (n > x)
{
clean();
throw std::overflow_error("参数n转换错误");
}
return static_cast<long>(n);
}
template <class T>
inline void ArrayList<T>::set_capacity(size_t n)
{
if (n > CAPACITY)
{
d_capacity = n * EXP_FACTOR;
}
else
{
d_capacity = CAPACITY;
}
}
template <class T>
T &ArrayList<T>::operator[](long n)
{
long size = cvt_signed_number(d_size);
if (n >= size || n < -size)
{
std::cout << "ArrayList下标溢出" << std::endl;
ArrayList<T>::clean();
exit(0);
}
if (n >= 0)
{
return d_arr[n];
}
else
{
return d_arr[d_size + n];
}
}
删除特定位值的元素
其实没什么好说的,删除中间位值的元素,实际上就是从传入索引位值算起,后续的元素依次将其元素值向各自的前一个元素拷贝并覆盖原先的元素值,拷贝过程结束后,d_size减1,最后一个元素会被作为一个闲置的内存块留作以后的备用,这个过程的时间消耗是O(n)原理图如下:
remove成员函数实现
//删除特定的元素
//按值删除
bool remove(const T &v)
{
if (d_size > 0)
{
size_t k = 0;
//查找元素所在索引位置
k = find(v);
std::cout << "删除的元素index是:" << k << std::endl;
if (k == std::numeric_limits<size_t>::max())
{
std::cout << "没有找到该元素" << std::endl;
return false;
}
for (auto j = k; j < d_size; j++)
{
d_arr[j] = d_arr[j + 1];
}
--d_size;
shrink();
return true;
}
return false;
}
removeAt()成员函数实现
//删除指定索引的元素
//删除指定索引的值
void removeAt(long n)
{
long size = cvt_signed_number(d_size);
if (n >= size || n < -size)
{
clean();
std::cout << "Error: out of index!!" << std::endl;
}
long k = (n >= 0) ? n : size + n;
for (auto j = k; j < size; j++)
{
d_arr[j] = d_arr[j + 1];
}
d_size = --size;
shrink();
}
从特定位值插入元素
从特定的位值插入元素,际上就是从传入索引位值算起,后续的元素依次将其元素值向各自的后一个元素拷贝并覆盖原先的元素值,拷贝过程结束后,修改指定索引的元素的值,d_size增加1,备用内存块即减少1。但插入元素很可能导致分配的内存块空间所剩无几,所以每次插入操作前,都需要通过比较d_size和d_capacity两个计数器,如果d_size等于或大于d_capacity即会触发内部的expand函数申请内存扩容。expand成员函数的行为可以查看上文的描述。插入元素本身的时间消耗就是O(n),加之expand的时间消耗也是O(n),所以此时的时间成本是最高的。
//按索引位置插入值
template <class T>
void ArrayList<T>::insert(size_t ix, T v)
{
size_t old_size = d_size;
++d_size;
if (d_size >= d_capacity)
{
expand();
}
size_t e = old_size - 1;
if (ix < e)
{
//在插入元素的索引的后续元素往后移动
for (auto k = e; k >= ix; k--)
{
d_arr[k + 1] = d_arr[k];
}
d_arr[ix] = v;
}
}
查找元素
//查找特定元素值的索引,约定返回size_t的上限值表示找不到元素
inline size_t ArrayList<T>::find(const T &v)
{
if (d_size > 0)
{
for (auto i = 0; i < d_size; i++)
{
if (d_arr[i] == v)
{
return i;
}
}
}
return std::numeric_limits<size_t>::max();
}
清空所有元素
//清空整个列表
template <class T>
void ArrayList<T>::clearn()
{
delete[] d_arr;
d_size = d_capacity = 0;
}
尾部插入
//尾部插入元素
template <class T>
void ArrayList<T>::push(T value)
{
if (d_size >= d_capacity)
{
expand();
}
*(d_arr + d_size++) = value;
length = d_size;
}
尾部弹出
//尾部弹出数据
template <class T>
T ArrayList<T>::pop()
{
--d_size;
shrink();
return d_arr[d_size];
}
清空整个列表
template <class T>
void ArrayList<T>::clearn()
{
delete[] d_arr;
d_size = d_capacity = d_idles = length = capacity = 0;
}
显示数组
//显示数组
template <class T>
void ArrayList<T>::show_array()
{
std::cout << "[";
for (size_t i = 0; i < d_size - 1; i++)
{
std::cout << *(d_arr + i) << ",";
}
std::cout << d_arr[d_size - 1] << "]" << std::endl;
}
最后顺序表的API测试
#include <iostream>
using namespace std;
void ArrayListTest(){
ArrayList<int> arr;
for (size_t i = 0; i < 32; i++)
{
std::cout << i << ":push_back(" << i * 3 << ")" << std::endl;
sleep(1);
arr.push_back(i * 3);
}
arr.show_array();
cout << "size():" << arr.size() << endl;
cout << "capacity:" << arr.capacity() << endl;
int i = 0;
while (arr.size() >= 10)
{
std::cout << i << ":remove first elem(" << arr[0] << ")" << std::endl;
sleep(0.7);
arr.removeAt(0);
}
std::cout << "删除操作测试后..." << std::endl;
arr.show_array();
cout << "size():" << arr.size() << endl;
cout << "capacity:" << arr.capacity() << endl;
std::cout << "push操作测试..." << std::endl;
for (size_t i = 0; i < 5; i++)
{
arr.push_back(2 * (10 - i));
std::cout << i << "arr.push_back(" << arr[-1] << ")" << std::endl;
sleep(1);
}
arr.show_array();
cout << "size():" << arr.size() << endl;
cout << "capacity:" << arr.capacity() << endl;
std::cout << "索引操作符测试..." << std::endl;
cout << "arr[-1]:" << arr[-1] << endl;
cout << "arr[-3]:" << arr[-3] << endl;
cout << "arr[-10]:" << arr[-10] << endl;
arr.show_array();
cout << "size():" << arr.size() << endl;
cout << "capacity:" << arr.capacity() << endl;
cout << "在索引 6位值插入操作" << endl;
arr.insert(6, 345);
arr.show_array();
cout << "pop 操作弹出: " << arr.pop() << " 之后size()是 " << arr.size() << endl;
cout << "pop 操作弹出: " << arr.pop() << "之后size()是 " << arr.size() << endl;
cout << "pop 操作弹出: " << arr.pop() << " 之后size()是 " << arr.size() << endl;
arr.show_array();
arr.remove(345);
cout << "删除元素后:"
<< "当前size()是" << arr.size() << endl;
arr.show_array();
std::cout << "swap测试" << std::endl;
int x[5] = {1, 2, 3, 4, 5};
int y[3] = {71, 14, 32};
ArrayList<int> a(x, 5);
ArrayList<int> b(y, 3);
a.swap(b);
std::cout << "a:" << std::endl;
a.show_array();
std::cout << "b:" << std::endl;
b.show_array();
}
int main(int argc, char const *argv[])
{
ArrayList<int> v1(1000);
ArrayList<int> v2(std::move(v1));
return 0;
}
以下这是测试结果,
结语
这个ArrayList是后门讨论排序算法,设计模式,以及C++其他高级特性会经常用到的,并且这个顺序表用到了很多C++的特性,它的内存管理和C++标准的顺序容器是类似的。OK性能测试的话,等我将全系列的数据结构写完,再与C++内置的容器做个性能分析。