1. 程式人生 > >淺談動態數組原理及其實現

淺談動態數組原理及其實現

縮小 vector 但是 align 幹什麽 可能 而不是 快速實現 param

  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,取模真的是慢得嚇死人啊。。)

最後呢,歡迎提出問題或指出上面的錯誤。

淺談動態數組原理及其實現