C++ 向量(vector)
1. vector介紹
向量 vector 是一種物件實體, 能夠容納許多其他型別相同的元素, 因此又被稱為容器。 與string相同, vector 同屬於STL(Standard Template Library, 標準模板庫)中的一種自定義的資料型別, 可以廣義上認為是陣列的增強版 ,與陣列相比,vector就是一個可以不用再初始化就必須制定大小的邊長陣列,當然了,它還有許多高階功能。
在使用它時, 需要包含標頭檔案vector.
#include<vector>
1.1 向量宣告和初始化
vector 型變數的宣告以及初始化的形式也有許多, 常用的有以下幾種形式:
/* 初始化vector物件的方法 */ vector<T> v1 ; //v1是一個空的vector,它潛在的元素是T型別的,執行預設初始化 vector<T> v2(v1) ; //v2中包含v1中所有元素的副本 vector<T> V2 = V1; //與上等價 vector<T> v3(n, val) ; //宣告一個初始大小為10且初始值都為1的向量 vector<T> v4(n) ; //v4包含了n個重複地執行了值初始化的物件 vector<T> v5{a,b,c,...} //v5包含了初始值個數的元素,每個元素給賦予對應的初值 vector<T> v5 = {a,b,c,...} //與上等價 vector<T> v6(v5.begin(), v5.begin()+3) ; //將v5向量中從第0個到第2個(共3個)作為向量v6的初始值
如果vector的元素型別是int,預設初始化為0;如果vector元素型別為string,則預設初始化為空字串。除此之外, 還可以直接使用陣列來初始化向量,例如 :
vector<int> v1; vector<father> v2; vector<string> v3; vector<vector<int> >; //注意空格。這裡相當於二維陣列int a[n][n]; vector<int> v5 = { 1,2,3,4,5 };//列表初始化,注意使用的是花括號 vector<string> v6 = { "hi","my","name","is","lee" }; vector<int> v7(5, -1); //初始化為-1,-1,-1,-1,-1。第一個引數是數目,第二個引數是要初始化的值 vector<string> v8(3, "hi"); vector<int> v9(10); //預設初始化為0 vector<int> v10(4); //預設初始化為空字串 /* 直接使用陣列來初始化向量 */ int n[] = {1, 2, 3, 4, 5} ; vector<int> v11(n, n+5) ; //將陣列n的前5個元素作為向量a的初值 vector<int> v11(&n[1], &n[4]) ;//將n[1] - n[4]範圍內的元素作為向量v11的初值
1.2 訪問和操作vector中的每個元素
元素的輸入和訪問可以像操作普通的陣列那樣, 用cin>>進行輸入, cout<<a[n] 這樣進行輸出,示例:
#include<iostream>
#include<vector>
using namespace std ;
int main()
{
vector<int> a(10, 0) ; //大小為10初值為0的向量a
/* 對其中部分元素進行輸入 */
cin >>a[2] ;
cin >>a[5] ;
cin >>a[6] ;
/* 全部輸出 */
for(int i=0; i<a.size(); i++)
cout<<a[i]<<" " ;
return 0 ;
}
注意:只能對已存在的元素進行賦值或者修改操作,如果是要加入新元素,務必使用push_back。push_back的作用有兩個:告訴編譯器為新元素開闢空間、將新元素存入新空間裡。
比如下面的程式碼是錯誤的,但是編譯器不會報錯,就像是陣列越界。
vector<int> vec; //vec為空
vec[0] = 1; //錯誤!不能給空向量直接賦值,應該用push_back()
但是可以這樣寫:
vector<int> vec(3); //vec宣告時定義了長度為3,預設vec中有三個元素0
vec[0] = 1; //正確!相當於將vec中第0個元素的值0變成1
向量元素的位置也是一種資料型別, 在向量中迭代器的型別為: vector<int>::iterator。 迭代器不但表示元素位置, 還可以再容器中前後移動。我們也可以選擇使用迭代器(iterator)來訪問元素:
vector<string> v6 = { "hi","my","name","is","lee" };
for (vector<string>::iterator iter = v6.begin(); iter != v6.end(); iter++)
{
cout << *iter << endl;
//下面兩種方法都行
cout << (*iter).empty() << endl;
cout << iter->empty() << endl;
}
上面是正向迭代,如果我們想從後往前迭代該如何操作?使用反向迭代器(reverse_iterator),如下:
for (vector<string>::reverse_iterator iter = v6.rbegin(); iter != v6.rend(); iter++)
{
cout << *iter << endl; //訪問iter所指向的元素
}
1.3 向量的基本操作
---- 基本操作 ----
v.size() //返回向量v中的元素個數
v.empty() //若v中不包含任何元素,返回真;否則返回假
v.push_back(t) //向v的尾端新增一個值為t的元素
v.front() //訪問v的第一個元素
v.back() //訪問v的最後一個元素
v[n] //返回v中第n個位置上元素的應用
v1 = v2 ; //將v2中的元素拷貝替換v1中的元素
v1 = {a,b,c,...} //用列表中元素拷貝替換v1中的元素
==、!=、>、>=、<、<= //慣有含義,以字典順序進行比較 ;
---- 新增或刪除元素的操作 ----
push_back() //把傳送為引數的物件新增到vector的尾部
pop_back() //刪除vector尾最後一個元素
erase() //刪除一個或多個元素
--v.erase(v.begin()) ; //將起始位置的元素刪除
--v.erase(v.begin(), v.begin()+3) ;//將(v.begin(), v.begin()+3)之間的3個元素刪除
--v.erase(v.begin() + 3); //刪除第4個元素
clear() //清除所有元素
insert() //插入一個或多個物件
--v.insert(v.begin(), 1000); //將1000插入到向量v的起始位置前
--v.insert(v.begin() + 1,9); //在第二個位置插入新元素
--v.insert(v.begin(), 3, 1000) ; //位置0開始,連續插入3個1000
--v.insert(v.begin() + 1, 7,8); //位置1開始,連續插入7個8
--vector<int> a(5, 1) ;
vector<int> b(10) ;
b.insert(b.begin(), a.begin(), a.end());
//將a.begin(), a.end()之間的全部元素插入到b.begin()前
---- 交換 ----
v2.swap(v1) ; //v1向量與v2向量進行交換
vector的侷限:但凡使用了迭代器的迴圈體,都不要向迭代器所屬的容器新增元素!任何一種可能改變vector物件容量的操作,如push_back,都會使迭代器失效。
push_back和inset的區別:
- push_back把元素插入容器末尾,insert把元素插入任何你指定的位置。
- push_back速度一般比insert快,如果能用push_back儘量先用push_back。
1.4 二維向量
與陣列相同, 向量也可以增加維數, 例如宣告一個m*n大小的二維向量方式可以像如下形式:
vector< vector<int> > b(10, vector<int>(5)); //建立一個10*5的int型二維向量
在這裡, 實際上建立的是一個向量中元素為向量的向量。同樣可以根據一維向量的相關特性對二維向量進行操作, 如:
#include<iostream>
#include<vector>
using namespace std ;
int main()
{
vector< vector<int> > b(10, vector<int>(5, 0)) ;
//對部分資料進行輸入
cin>>b[1][1] ;
cin>>b[2][2] ;
cin>>b[3][3];
//全部輸出
int m, n ;
for(m=0; m<b.size(); m++) //b.size()獲取行向量的大小
{
for(n=0; n<b[m].size(); n++) //獲取向量中具體每個向量的大小
cout<<b[m][n]<<" " ;
cout<<"\n" ;
}
return 0;
}
2. C++中陣列和vector的比較
2.1 陣列
C++中陣列是一種內建的資料型別。陣列是存放型別相同的物件的容器,陣列的大小確定不變,不能隨意向陣列中增加元素。
1、定義和初始化內建陣列
- 陣列的大小不變,(a[d],d為陣列的維度),陣列的維度必須是一個常量表達式。定義陣列的時,必須指定陣列的型別和大小。
- 初始化時,允許不指明陣列的維度,不指明維度,則編譯器根據陣列初始值的大小推測出維度;若指定維度,則初始值的個數要小於等於維度,當小於時,不足的部分為0(其實還是等於維度)。
int a[]={1,2,3}; //陣列a的大小為3;
int a[5]={1,2,3}; //等價於{1,2,3,0,0},大小為5
int a[5]={1,2,3,4,5,6}; //錯誤,初始值過多
- 有一種特殊的情況:字元陣列。當用字串字面值去初始化陣列時,要注意字串字面值的後面還有一個空字元。也就是說,陣列的大小要等於字面值的大小加1。
- 特別注意:不允許拷貝和賦值,即不能將陣列的內容拷貝給其他陣列作為初始值,也不能用陣列為其他陣列賦值。
2、訪問陣列元素
- 陣列的索引是從0開始,如:包含10個元素的陣列a,其索引是從0到9而非1到10,若是a[10]則下標越界。
- 使用陣列下標時,其型別是size_t,在標頭檔案cstddef中。
2.2 向量
C++中vector為類模板。vector是型別相同的物件的容器,vector的大小可以變化,可以向陣列中增加元素。
1、定義和初始化vector物件
vector初始化的方式比較多,有如下幾種:
vector<T> v1; //v1為空,執行預設初始化,只用預設初始化時,不能通過下標進行新增元素
vector<T> v2(v1); //v2中包含v1所有元素的副本
vector<T> v2=v1; //等價於v2(v1)
vector<T> v3(n,val); //v3中包含n個重複元素,每個元素的值都是val
vector<T> v4(n); //v4包含n個重複執行了值初始化的物件
vector<T> v5{a,b,c...}; //包含初始化元素個數,每個元素被對應的賦予相應的值
vector<T> v5={a,b,c...}; //等價v5{a,b,c...}
注意事項:
- (1)vector<T> v1,只用預設初始化時,不能通過下標進行新增元素。也就是說,當你將v1初始化為空時,假如你想向v1中新增10個元素,不能通過v1[2]=3;等形式新增,因為,別人為空,壓根不知道v1[2]是什麼東東。
- (2)注意vector<T> v4(n)和vector<T> v4{n}的區別。前者說的是,v4中有n個相同的元素,至於值為多少要看相應物件的初始化值;而後者,則是說v4中只有一個元素,其值為n。
- (3)不能使用包含著多個值的括號去初始化vector物件。注意和花括號的區別。
例如:
vector<int> a ; //宣告一個int型向量 a
vector<int> a(10) ; //宣告一個初始大小為 10 的向量
vector<int> a(10, 1) ; //宣告一個初始大小為 10 且初始值都為 1 的向量
vector<int> b(a) ; //宣告並用向量 a 初始化向量 b
vector<int> b(a.begin(), a.begin()+3) ; //將a向量中從第0個到第2個(共3個)作為向量b的初始值
2、向vector物件中新增物件
利用vector的成員函式 push_back 向其中新增物件:
vector<int> v;
for(int i=0;i !=100;++i) {
v.push_back(i);
}
注意:若是迴圈體內包含向vector物件新增元素的語句,則不能使用範圍for迴圈。因為範圍for語句不應改變其所遍歷序列的額大小。原因如下:
vector<int> v={1,2,3,4,5,6,7,8,9};
for(auto &r: v) {
r*=2;
}
// 等價於
for(auto beg=v.begin(),end=v.end() ; beg !=end ; ++beg ) {
auto &r=*beg;
r*=2;
}
即在範圍for語句中,預存了end()的值,一旦在序列中新增(刪除)元素,end函式的值就可能變的無效了
3、vector的擴容、插入和刪除
(1)擴容
vector的底層資料結構是陣列。
當vector中的可用空間耗盡時,就要動態地擴大內部陣列的容量。直接在原有物理空間的基礎上追加空間?這不現實。陣列特定的地址方式要求,物理空間必須地址連續,而我們無法保證其尾部總是預留了足夠空間可供拓展。一種方法是,申請一個容量更大的陣列,並將原陣列中的成員都搬遷至新空間,再在其後方進行插入操作。新陣列的地址由OS分配,與原資料區沒有直接的關係。新陣列的容量總是取作原陣列的兩倍。
(2)插入和刪除
插入給定值的過程是,先找到要插入的位置,然後將這個位置(包括這個位置)的元素向後整體移動一位,然後將該位置的元素複製為給定值。刪除過程則是將該位置以後的所有元素整體前移一位。
(2)vector的 size 和 capacity
size指vector容器當前擁有的元素個數,capacity指容器在必須分配新儲存空間之前可以儲存的元素總數,capacity總是大於或等於size的。
2.3 陣列與vector的對比
- 記憶體中的位置 C++中陣列為內建的資料型別,存放在棧中,其記憶體的分配和釋放完全由系統自動完成;vector,存放在堆中,由STL庫中程式負責記憶體的分配和釋放,使用方便。
- 大小能否變化 陣列的大小在初始化後就固定不變,而vector可以通過 push_back 或 pop 等操作進行變化。
- 用size獲取長度 陣列在定義時就確定了長度,不能用size獲取長度;vector可以用size 獲取長度。
- 初始化 陣列不能將陣列的內容拷貝給其他陣列作為初始值,也不能用陣列為其他陣列賦值;而vector可以。
- 執行效率 陣列>vector向量。主要原因是vector的擴容過程要消耗大量的時間。
3. push_back賦值 vs. 下標賦值
【向量 push_back賦值 vs. 下標賦值 原部落格地址】
在程式設計過程中,經常會遇到需要建一個大的vector,並對裡面的元素逐一順序求值。此時我們有兩種選擇:
- 先reserve(n)後,再逐一的push_back
- 先建立含n元素的vector,再逐一的通過下標為其賦值
那麼,這兩個方法哪個會更快一些呢?
// test1 向量通過push_back賦值
int main() {
int i=100000;
for(int j=0;j< i;j++){
vector<int> sz;
sz.reserve(10000);
for(int k=0;k<10000;k++)
sz.push_back(1);
}
return 0;
}
//test2 向量直接通過下標賦值
int main() {
int i=100000;
for(int j=0;j<i;j++){
vector<int> sz(10000);
for(int k=0;k<10000;k++)
sz[k]=1;
}
return 0;
}
用g++編譯後,在ubuntu上測試兩個程式的執行時間,發現test1需要20.289s,而test2竟只需要5.435s。且更換資料,多次均是如此。 由此,顯然可知,先建立n個元素的vector,再一一用下標取值要快的多。 與n次push_back的時間相比,遍歷vector為每個元素的初始化所花時間完全是微不足道的。
故,在已知vector的資料的時候,直接在宣告時給出元素個數,是十分明智且效率高的選擇。在資料存取時,在同等情況下,可儘量用陣列下標存取元素,減少呼叫push_back的次數。
【原因淺究】為什麼vector會造成這麼慢的訪問速度呢?查閱stl原始碼,發現push_back呼叫了insert_aux,並在此函式中呼叫了constructor,copy_backward等函式,其函式的功能也都較簡單,但其每一步都要檢查是否有效是否發生錯誤等情況,可見其主要是頻繁的檢查與其他函式的呼叫佔用了時間,(在呼叫函式的時候,會需要保留棧,儲存變數等情況,在大量push_back時,這些也無形中佔用了許多時間,也自然堆積起來使得 test2 比 test1 慢很多)。