第1篇 C++ 数据结构--ArrayList的实现

常用语言中的动态数组的示例包括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;
}

以下这是测试结果,


Peek 2019-12-25 13-23.gif

结语

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容