第1篇 C++ 數(shù)據(jù)結構--ArrayList的實現(xiàn)

常用語言中的動態(tài)數(shù)組的示例包括Java和C#中的ArrayList讯柔,C ++中的Vector缨历,.NET中的List <>,Python的list等

我們這里實現(xiàn)的動態(tài)數(shù)組與上述各種語言中的相似,動態(tài)數(shù)組的大小可以在運行時動態(tài)修改。動態(tài)數(shù)組的元素占用連續(xù)的內存塊雄可,一旦創(chuàng)建,就無法更改其大小缠犀。 當內存空間所剩無幾数苫,就可以分配更大的內存塊,將內容從原始數(shù)組復制到該新空間辨液,然后繼續(xù)填充可用的"插槽"虐急。動態(tài)數(shù)組在創(chuàng)建時會分配預定數(shù)量的內存,然后在需要時以一定的比例增長室梅。 這些參數(shù),初始大小和增長因子在性能方面至關重要疚宇。 如果初始大小和增長因子較小亡鼠,那么您最終將不得不經(jīng)常重新分配內存,這對性能方面并不理想敷待; 另一方面间涵,如果分配的內存塊過高,則可能會有大量空閑的堆內存塊榜揖,并且調整大小操作可能需要更長的時間勾哩。 這里的權衡是非常重要的。

其實我這里的動態(tài)數(shù)組的實現(xiàn),內存分配機制是模仿举哟,C++標準庫中的vector/string思劳,他們從內存池中申請的內存塊數(shù)量是上一次申請內存塊數(shù)量的2倍。這個是增長因子充分平衡了性能和內存空間的消耗妨猩。

類接口

C語言編寫一個動態(tài)整數(shù)數(shù)組,但要兼容不同的基本數(shù)據(jù)類型,你可能要需要編寫較為復雜宏函數(shù)來模擬C++泛型相同的效果潜叛。而C++本身已經(jīng)集成了模板和類的垃圾回收機制,以模板為基礎的泛型技術可以很輕松地編寫出管理任何數(shù)據(jù)類型和自動內存回收的動態(tài)數(shù)組-v-!!,那么實現(xiàn)動態(tài)數(shù)組的首選語言就是C++了壶硅,我們定義了動態(tài)數(shù)組的類接口威兜。

#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:
    //默認的構造函數(shù)
    ArrayList();

    //自定義的構造函數(shù)
    ArrayList(const size_t &&n);

    //自定義的構造函數(shù)
    ArrayList(const T *data, size_t n);

    //copy構造函數(shù)
    ArrayList(const ArrayList &oth);

    //move構造函數(shù)
    ArrayList(ArrayList &&oth) : d_size(oth.d_size);

    ~ArrayList();

    //下標訪問
    T &operator[](long n);

    //ArrayLIst尺寸
    size_t size() const;

    //ArrayList當前分配內存數(shù)
    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);

    //尾部彈出數(shù)據(jù)
    T pop();

    //清空整個容器
    void clean();
};

構造函數(shù)的實現(xiàn)

    //默認的構造函數(shù)
    template <class T>
    ArrayList<T>::ArrayList()
    {
        d_size = 0;
        d_capacity = CAPACITY;
        d_arr = new T[d_capacity];
    }

    //自定義的構造函數(shù)
     template <class T>
    ArrayList<T>::ArrayList(const size_t &&n) : d_size(n)
    {
        set_capacity(n);
        d_arr = new T[d_capacity];
    }

    //自定義的構造函數(shù)
     template <class T>
     ArrayList<T>::ArrayList(const T *data, size_t n)
        : d_capacity(CAPACITY)
    {
        if (n > 0)
        {
            std::cout << "ArrayList 普通函數(shù)調用" << std::endl;
            d_arr = new T[n];
            for (size_t i = 0; i < n; i++)
            {
                *(d_arr + i) = *(data + i);
            }
            d_size = n;
        }
    };

    //copy構造函數(shù)
     template <class T>
     ArrayList<T>::ArrayList(const ArrayList &oth)
        : d_size(oth.d_size)
    {
        std::cout << "ArrayList copy函數(shù)調用" << 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構造函數(shù)
    template <class T>
    ArrayList<T>::ArrayList(ArrayList &&oth) : d_size(oth.d_size)
    {
        std::cout << "ArrayList move函數(shù)調用" << std::endl;
        d_arr = oth.d_arr;
        oth.d_size = 0;
        oth.d_arr = nullptr;
    }

析構函數(shù)的實現(xiàn)

顯式定義構造函數(shù)是一個非常好的習慣,對于很多的內存回收的文章講述的僅僅是delete[]操作釋放堆內存,而沒有將指向堆內存的T類型指針重置為nullptr,我認為這是一個很好的習慣庐椒,C風格中的回收內存的小技巧椒舵,這個是值得繼承的好習慣,我為什么這么說?
若將內部d_arr的T類型指針在delete[]前约谈,你可以嘗試用一個全局變量p來緩存該指針,且d_arr沒有重置為nullptr的話笔宿,你再次使用該全局變量p犁钟,會仍然訪問到原來指針所指向內存區(qū)域的數(shù)據(jù),雖然原有的內存返回給堆管理器了,但原本的數(shù)據(jù)值還在內存塊,對于程序的數(shù)據(jù)保密性不是個好主意措伐。

//內存回收
template <class T>
ArrayList<T>::~ArrayList()
{
        std::cout << "ArrayList析構函數(shù)" << std::endl;
        if (d_arr != nullptr)
        {
            delete[] d_arr;
        }
}

內存擴容的實現(xiàn)

expand()是一個私有的內部成員函數(shù),它內部完成了向內存池申請比現(xiàn)有內存更大的內存空間(是原有內存空間的2倍)特纤,然后將原有內存空間中的元素拷貝到新的內存空間,最后將舊的內存空間釋放返還給內存池侥加。具體示意圖如下:


expand成員函數(shù)的實現(xiàn)

//內部擴容操作
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()成員函數(shù)的實現(xiàn)

關于如何將超出額度的空閑的內存返還給內存池捧存,我這里定制的策略的是在每次pop/或者remove操作過程中,我們都檢測d_capacity/d_size的比值担败,如果該比值達到3昔穴,我們就調用shrink()成員函數(shù),將額外閑置的內存返還給內存池,僅保留的內存總量是以使用的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]的元素,請參見如下圖拓哺。


實現(xiàn)類似Python列表的訪問能力非常簡單,上圖我們知道對應位置的順序index(用m表示)和逆序index(用n表示)的絕對值的和等于該列表的長度(用length表示,始終是非負數(shù))勇垛,我們得到如下簡單的結論。

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

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

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

實際上我們若提供一個的負整數(shù)n時,(-length n < 0)士鸥,最終會轉換回順序index的非負整數(shù)闲孤,那么operator[]的索引操作符重寫如下。在重載operator[]的同時烤礁,我們需要面size_t類型的d_size轉換為帶符號的整數(shù)的問題讼积,具體的問題描述可以看我之前寫的隨筆《C/C++ 有符號和無符號數(shù)字的迷途》

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("參數(shù)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];
    }
}

刪除特定位值的元素

其實沒什么好說的,刪除中間位值的元素脚仔,實際上就是從傳入索引位值算起勤众,后續(xù)的元素依次將其元素值向各自的前一個元素拷貝并覆蓋原先的元素值,拷貝過程結束后鲤脏,d_size減1决摧,最后一個元素會被作為一個閑置的內存塊留作以后的備用,這個過程的時間消耗是O(n)原理圖如下:


remove成員函數(shù)實現(xiàn)

//刪除特定的元素
    //按值刪除
    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()成員函數(shù)實現(xiàn)

//刪除指定索引的元素
    //刪除指定索引的值
    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();
    }

從特定位值插入元素

從特定的位值插入元素,際上就是從傳入索引位值算起,后續(xù)的元素依次將其元素值向各自的后一個元素拷貝并覆蓋原先的元素值凑兰,拷貝過程結束后掌桩,修改指定索引的元素的值,d_size增加1,備用內存塊即減少1。但插入元素很可能導致分配的內存塊空間所剩無幾,所以每次插入操作前姑食,都需要通過比較d_size和d_capacity兩個計數(shù)器波岛,如果d_size等于或大于d_capacity即會觸發(fā)內部的expand函數(shù)申請內存擴容。expand成員函數(shù)的行為可以查看上文的描述音半。插入元素本身的時間消耗就是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)
        {
            //在插入元素的索引的后續(xù)元素往后移動
            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;
}

尾部彈出

//尾部彈出數(shù)據(jù)
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;
}

顯示數(shù)組

//顯示數(shù)組
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++其他高級特性會經(jīng)常用到的,并且這個順序表用到了很多C++的特性,它的內存管理和C++標準的順序容器是類似的坛善。OK性能測試的話晾蜘,等我將全系列的數(shù)據(jù)結構寫完,再與C++內置的容器做個性能分析眠屎。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末剔交,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子改衩,更是在濱河造成了極大的恐慌岖常,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葫督,死亡現(xiàn)場離奇詭異竭鞍,居然都是意外死亡,警方通過查閱死者的電腦和手機橄镜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門偎快,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蛉鹿,你說我怎么就攤上這事滨砍⊥” “怎么了妖异?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長领追。 經(jīng)常有香客問我他膳,道長,這世上最難降的妖魔是什么绒窑? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任棕孙,我火速辦了婚禮,結果婚禮上些膨,老公的妹妹穿的比我還像新娘蟀俊。我一直安慰自己,他們只是感情好订雾,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布肢预。 她就那樣靜靜地躺著,像睡著了一般洼哎。 火紅的嫁衣襯著肌膚如雪烫映。 梳的紋絲不亂的頭發(fā)上沼本,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音锭沟,去河邊找鬼抽兆。 笑死,一個胖子當著我的面吹牛族淮,可吹牛的內容都是我干的辫红。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瞧筛,長吁一口氣:“原來是場噩夢啊……” “哼厉熟!你這毒婦竟也來了?” 一聲冷哼從身側響起较幌,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤揍瑟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后乍炉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绢片,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年岛琼,在試婚紗的時候發(fā)現(xiàn)自己被綠了底循。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡槐瑞,死狀恐怖熙涤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情困檩,我是刑警寧澤祠挫,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站悼沿,受9級特大地震影響等舔,放射性物質發(fā)生泄漏。R本人自食惡果不足惜糟趾,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一慌植、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧义郑,春花似錦蝶柿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至院尔,卻和暖如春蜻展,著一層夾襖步出監(jiān)牢的瞬間喉誊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工纵顾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沟堡,地道東北人厚满。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓唱蒸,卻偏偏與公主長得像呈野,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子汉额,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容