1. 程式人生 > >C++ 向量(vector)

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的比較

【陣列與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,並對裡面的元素逐一順序求值。此時我們有兩種選擇:

  1. 先reserve(n)後,再逐一的push_back
  2. 先建立含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 慢很多)。