首页 > 代码库 > 浅谈动态数组原理及其实现

浅谈动态数组原理及其实现

<style></style>

  stl中的vector是竞赛中常用的容器,原因在于省内存,O(1)在后端插入和删除、随机下标访问,今天就来谈谈它的实现。

最简单的一个动态数组

   动态数组并不是真正意义上的动态的内存,而是一块连续的内存,当添加新的元素时,容量已经等于当前的大小的时候(存不下了),执行下面3步

  1. 重新开辟一块大小为当前容量两倍的数组
  2. 把原数据拷贝过去
  3. 释放掉旧的数组

   完事后再把这个元素加在后面。

  那么你一定会很好奇,它为什么会是O(1)。这个是均摊下来的结果,我们只需要证明总共拷贝的元素个数是O(n)级别的就好了(因为释放内存和申请内存都比较快,详情自行百度吧)。

  总共拷贝的元素个数是技术分享,根据等比数列求和公式那么有技术分享。因为技术分享,所以技术分享,所以有技术分享。因此总共拷贝的元素个数是O(n)级别,均摊下来,每次在尾部插入的时间复杂度就是O(1)。

 1 #include<iostream> 2 #include<malloc.h> 3 using namespace std; 4  5 template<typename T> 6 class Vector { 7     protected: 8         int cap; 9         int siz;10         T* l;11     public:12         Vector():l(NULL), cap(0), siz(0) {    }13         14         inline void push_back(T x) {15             if(l == NULL) {16                 l = new T[4];17                 cap = 4, siz = 0;18             }19             if(siz == cap) {20                 l = (T*)realloc(l, sizeof(T) * cap * 2);    //重新申请内存,并拷贝数组 21                 cap = cap << 1; 22             }23             l[siz++] = x;24         }25         26         T& operator [] (int pos) {27             return l[pos];28         }29         30         inline int size() {31             return siz;32         }33         34         inline int capt() {35             return cap;36         }37 };

  这里为了省时间,不用new而是用malloc,这样稍微会快一些。

 支持在头部插入的动态数组

  stl的vector是不支持push_front,但是你可以通过insert在头部插入,但是这样是O(n)的,而不是O(1)的,因为vector中的insert是将它后面的内容往后copy,再把自己弄进vector。但是显然可以用近似的方法实现push_front,只是多需要一些内存罢了。

  开一个头部缓冲数组,它的大小是l的一半,凡是push_front,都把加入的元素塞进去。当这个数组满了或者重新申请内存时,就先将它中的内容拷贝进新的数组。其他地方就稍微改一下就好了。因为这样会消耗更多的内存,所以用了一个boolean类型的变量sign区分是否需要使它实现push_front。

 1 #include<iostream> 2 #include<algorithm>  3 #include<string.h> 4 #include<malloc.h> 5 using namespace std; 6 typedef bool boolean; 7  8 template<typename T> 9 class Vector {10     protected:11         int cap;        //容量 12         int siz;        //当前元素个数 13         boolean sign;    //是否支持push_front 14         T* l;            //数组部分15         T* fronts;        //头部缓冲数组(追加在前面的部分)16         int sizf;        //前面部分的大小 17         18         inline void extends(int newsize) {19             if(sign) {20                 T* newarr = (T*)malloc(sizeof(T) * newsize);        //申请新的数组 21                 memcpy(newarr, fronts, sizeof(T) * sizf);            //将原来添加到前面的数据拷贝进这个数组22                 reverse(newarr, newarr + sizf);                        //翻转这一段,因为往前塞时却是往后存 23                 memcpy(newarr + sizf, l, sizeof(T) * siz);        //拷贝从l开始的这一段 24                 siz += sizf, cap = newsize;                            //更新数据 25                 free(l);                                            //释放指针指向的数组 26                 free(fronts);27                 sizf = 0;                                            //重新设置大小 28                 fronts = (T*)malloc(sizeof(T) * (newsize >> 1));    //重新设置动态数组头部缓冲数组 29                 l = newarr;30             } else {31                 l = (T*)realloc(l, sizeof(T) * newsize);32                 cap = newsize;33             }34         }35     public:36         Vector():l(NULL), cap(0), siz(0), sizf(0), sign(false), fronts(NULL) {    }37         Vector(boolean sign):l(NULL), cap(0), siz(0), sizf(0), sign(sign), fronts(NULL) {    }38         39         /**40          * 向动态数组尾部追加一个元素。41          * @param x 追加的元素42          * @return 如果成功追加,返回true,否则返回false 43          */44         inline boolean push_back(T x) {45             if(l == NULL) {46                 extends(4);47                 if(l == NULL)    return false;48             }49             if(siz == cap) {50                 extends(cap * 2);51                 if(l == NULL)    return false;52             }53             l[siz++] = x;54             return true;55         }56         57         /**58          * 向动态数组头部追加一个元素。59          * @param x 追加的元素60          * @return 如果成功追加,返回true,否则返回false 61          */62         inline boolean push_front(T x) {63             if(!sign)    return false;64             if(fronts == NULL) {65                 extends(4);66                 if(fronts == NULL)    return false;67             }68             if(sizf == (cap >> 1)) {69                 extends(cap * 2);70                 if(fronts == NULL)    return false;71             }72             fronts[sizf++] = x;73             return true;74         }75         76         T& operator [] (int pos) {77             if(pos < sizf)78                 return fronts[sizf - pos - 1];79             return l[pos - sizf];80         }81         82         inline int size() {83             return siz + sizf;84         }85         86         inline int capt() {87             if(sign)    return cap + (cap >> 1);88             return cap;89         }90         91         inline int size_front() {92             return sizf;93         }94         95         inline int size_middle() {96             return siz;97         }98 };

支持在双端删除的动态数组

  如果push_front还用上面的方法实现,那么代码可能会很长,因为在头部删除插入都需要特判。所以考虑将头部缓冲数组和l合并到一起。这样不难想到队列,给一个头指针和一个尾指针也可以实现边界判断(是否需要申请空间了)。

  为了不被极端数据卡死,每次重新申请空间时,都将旧的数组拷贝到新的数组的中间。

  下面来说一下删除。队列的删除很简单,挪"指针"就好了, 只不过要考虑下面这个问题

要不要释放内存?

  至少我觉得应该是,不然我还不如直接开一块静态内存当队列用,还要动态数组干什么?但是不是当当前元素个数少于容量的一半就应该缩小当前数组,而是少于四分之一时才考虑缩小,不然可能有些不良心的题目数据让你删完一个,然后又插入一个,迫使你重新申请内存导致时间复杂度变成了O(n)。

  1 #include<iostream>  2 #include<algorithm>   3 #include<string.h>  4 #include<malloc.h>  5 using namespace std;  6 typedef bool boolean;  7   8 template<typename T>  9 class Vector { 10     protected: 11         int cap;        //容量  12         boolean sign;    //是否支持push_front  13         T* l;            //数组部分 14         int front;        //头指针  15         int rear;        //尾指针  16          17         inline void extends(int newsize) { 18             if(sign) { 19                 int realsize = newsize * 3;                            //实际大小  20                 T* newarr = (T*)malloc(sizeof(T) * realsize);        //开辟新的数组  21                 int s = this->size();  22                 int start_pos = (realsize - s) >> 1;                //将原数据拷贝到新数组的中间  23                 memcpy(newarr + start_pos, l + front + 1, sizeof(T) * s); 24                 free(l);                                            //释放原数组  25                 l = newarr; 26                 front = start_pos - 1, rear = start_pos + s;        //重新计算下标  27                 cap = newsize; 28             } else { 29                 l = (T*)realloc(l, sizeof(T) * newsize); 30                 cap = newsize; 31             } 32         } 33          34         inline void construct(boolean sign) { 35             if(!sign) { 36                 l = (T*)malloc(sizeof(T) * 4); 37                 front = -1, rear = 0, cap = 4; 38             } else { 39                 l = (T*)malloc(sizeof(T) * 6); 40                 front = 1, rear = 2, cap = 2; 41             } 42         } 43     public: 44         Vector():l(NULL), front(-1), rear(0), sign(false) {        } 45         Vector(boolean sign):l(NULL), front(-1), rear(0), sign(sign) {        } 46          47         /** 48          * 向动态数组尾部追加一个元素。 49          * @param x 追加的元素 50          * @return 如果成功追加,返回true,否则返回false  51          */ 52         inline boolean push_back(T x) { 53             if(l == NULL) { 54                 construct(sign); 55                 if(l == NULL)    return false; 56             } 57             if(!sign && rear == cap) { 58                 extends(cap * 2); 59                 if(l == NULL)    return false; 60             } else if(sign && rear == cap * 3) { 61                 extends(cap * 2); 62                 if(l == NULL)    return false; 63             } 64             l[rear++] = x; 65             return true; 66         } 67          68         /** 69          * 向动态数组头部追加一个元素。 70          * @param x 追加的元素 71          * @return 如果成功追加,返回true,否则返回false  72          */ 73         inline boolean push_front(T x) { 74             if(!sign)    return false; 75             if(l == NULL) { 76                 construct(sign); 77                 if(l == NULL)    return false; 78             } 79             if(front == -1) { 80                 extends(cap * 2); 81                 if(l == NULL)    return false; 82             } 83             l[front--] = x; 84             return true; 85         } 86          87         /** 88          * 在头部删除动态数组的一个元素。 89          * @return 如果成功删除,返回true,否则返回false  90          */  91         inline boolean pop_front() { 92             if(!sign)    return false; 93             if(front == rear - 1)    return false; 94             front++; 95             int s = this->size(); 96             int c = this->capt(); 97             if(s < (c >> 2) && c >= 12) {        //当当前容量过大时,缩小一下容量  98                 extends(cap >> 1); 99             }100             return true;101         }102         103         /**104          * 在尾部删除动态数组的一个元素105          * @return 如果成功删除,返回true,否则返回false 106          */107         inline boolean pop_back() {108             if(front == rear - 1)    return false;109             rear--;110             int s = this->size();111             int c = this->capt();112             if(s < (c >> 2) && c >= 12) {        //当当前容量过大时,缩小一下容量 113                 extends(cap >> 1);114             }115             return true;116         } 117         118         T& operator [] (int pos) {119             return l[front + pos + 1];120         }121         122         inline int size() {123             return rear - front - 1;124         }125         126         inline int capt() {127             if(sign)    return cap * 3;128             return cap;129         }130         131 };

更小内存消耗的动态数组

  注意到上面两种动态数组常常有些位置是空的,但是有时还是会重新申请新的内存,这对空间是一个极大的浪费(某些卡内存的题目,这个1.5倍可以决定你是AC还是MLE)。首先存数据是连续的,上面已经用了队列,不难想到用循环队列。这样极大地减少了浪费的空间,唯一的缺点就是循环队列取模很多,会很耗时间,所以还是按需使用吧。下面给出实现。(比较懒,就不写取模优化了)

  1 #include<iostream>  2 #include<algorithm>   3 #include<string.h>  4 #include<malloc.h>  5 using namespace std;  6 typedef bool boolean;  7   8 template<typename T>  9 class Vector { 10     protected: 11         int cap;            //容量  12         T* l;                //数组部分 13         int front;            //头指针  14         int rear;            //尾指针 15         boolean isempty;    //动态数组是否是空  16          17         inline void extends(int newsize) { 18             if(!isempty) { 19                 T* newarr = (T*)malloc(sizeof(T) * newsize);        //开辟新的数组 20                 int s = size(); 21                 if(rear <= front) {                                    //拷贝数据 22                     memcpy(newarr, l + front, sizeof(T) * (cap - front));  23                     memcpy(newarr + (cap - front), l, sizeof(T) * rear);  24                 } else { 25                     memcpy(newarr, l + front, sizeof(T) * s); 26                 }  27                 free(l);                                            //释放原数组  28                 l = newarr; 29                 front = 0, rear = s;                        //重新计算数据  30             } else { 31                 l = (T*)realloc(l, sizeof(T) * newsize); 32             } 33             cap = newsize; 34         } 35          36         inline void construct() { 37             l = (T*)malloc(sizeof(T) * 4); 38             front = 0, rear = 0, cap = 4, isempty = true; 39         } 40          41         inline int lastPos(int pos) {    return (pos + cap - 1) % cap;        } 42         inline int nextPos(int pos) {    return (pos + 1) % cap;                } 43     public: 44         Vector():l(NULL), front(0), rear(0), isempty(true) {        } 45          46         /** 47          * 向动态数组尾部追加一个元素。 48          * @param x 追加的元素 49          * @return 如果成功追加,返回true,否则返回false  50          */ 51         inline boolean push_back(T x) { 52             if(l == NULL) { 53                 construct(); 54                 if(l == NULL)    return false; 55             } 56             if(rear == front && !isempty) { 57                 extends(cap * 2); 58                 if(l == NULL)    return false; 59             } 60             l[rear] = x; 61             rear = nextPos(rear); 62             isempty = false; 63             return true; 64         } 65          66         /** 67          * 向动态数组头部追加一个元素。 68          * @param x 追加的元素 69          * @return 如果成功追加,返回true,否则返回false  70          */ 71         inline boolean push_front(T x) { 72             if(l == NULL) { 73                 construct(); 74                 if(l == NULL)    return false; 75             } 76             if(rear == front && !isempty) { 77                 extends(cap * 2); 78                 if(l == NULL)    return false; 79             } 80             front = lastPos(front); 81             l[front] = x; 82             isempty = false; 83             return true; 84         } 85          86         /** 87          * 在头部删除动态数组的一个元素。 88          * @return 如果成功删除,返回true,否则返回false  89          */  90         inline boolean pop_front() { 91             if(isempty)    return false; 92             front = nextPos(front); 93             if(front == rear)    isempty = true; 94             int s = this->size(); 95             int c = this->capt(); 96             if(s < (c >> 2) && c >= 8) {        //当当前容量过大时,缩小一下容量  97                 extends(cap >> 1); 98             } 99             return true;100         }101         102         /**103          * 在尾部删除动态数组的一个元素104          * @return 如果成功删除,返回true,否则返回false 105          */106         inline boolean pop_back() {107             if(isempty)    return false;108             rear = lastPos(rear);109             int s = this->size();110             int c = this->capt();111             if(s < (c >> 2) && c >= 8) {        //当当前容量过大时,缩小一下容量 112                 extends(cap >> 1);113             }114             return true;115         }116         117         inline void clear() {118             free(l);119             l = NULL;120             front = 0, rear = 0, cap = 0;121             isempty = true;122         }123         124         inline boolean empty() {125             return isempty;126         }127         128         T& operator [] (int pos) {129             return l[(front + pos) % cap];130         }131         132         inline int size() {133             if(rear == front && !isempty)    return cap;134             return (rear - front + cap) % cap;135         }136         137         inline int capt() {138             return cap;139         }140         141 };

快速实现随机下标插入,随机下标删除的动态数组

  这个嘛。。直接用splay好了,反正都是一个log。。当然也可以自行百度搜一下stl中的deque的实现。

性能测试

  因为比较懒,就用1e7的数据量测试一下push_back和pop_back

std::vector的测试结果

  技术分享

普通队列实现的Vector的测试结果

  不支持push_front的速度

  技术分享

  支持push_front的速度

  技术分享

  (反正插入都比stl快)

循环队列实现的Vector的测试结果

  技术分享

  (插入还是比stl,取模真的是慢得吓死人啊。。)

最后呢,欢迎提出问题或指出上面的错误。

浅谈动态数组原理及其实现