04.容器+迭代器+空間配置器
容器:順序容器/關聯容器
順序容器:
向量容器vector、雙端佇列 deque、雙向連結串列 list。
元素在容器中的位置同元素的值無關,即容器不是排序的。
vector 是可變長的動態陣列。#include <vector>。 隨機訪問、記憶體是連續的、方便排序、二分搜尋。可以巢狀形成二維動態陣列vector<vector<int> > v(3); //v有3個元素,每個元素都是vector<int> 容器。當然也可也vector<list<int>> vec3;
list是雙向連結串列容器,不迴圈。#include <list>。查詢速度慢,但插入刪除快。不支援隨機訪問,所以STL中演算法sort無法對list排序。但list容器引入了sort成員函式來完成排序。
deque雙端佇列容器。#include <deque>。vector頭插較慢,尾插較快。deque頭尾都挺快。首先動態開闢二維陣列,並且還有一個小塊的一維陣列(最開始開闢能存放兩個指標的空間)用指標來管理這個二維陣列。當擴容的時候首先擴容這個一維的,再2倍擴容存放資料的空間,並且是再原有的基礎上前面放一塊,後面放一塊。從而保證始終都可以頭查和尾插。但因為是動態開闢的空間,所以並不連續。
關聯容器:
關聯容器分為兩種:一種基於紅黑樹的有序容器,另一種基於雜湊表的無序容器。
基於紅黑樹的有序容器 查詢 O(log2n) 預設情況下,從小到大排序 | 基於雜湊表的無序容器 查詢 O(1) |
set: 集合 | unordered_set: 集合 |
multiset: 多重集合 | unordered_multiset: 多重集合 |
map: 對映表 不允許key重複 | unordered_map: 對映表 不允許key重複 |
multimap: 多重對映表 允許key重複 | unordered_multimap: 多重對映表 允許key重複 |
map和set的插入刪除效率比用其他序列容器高:因為對於關聯容器來說,不需要做記憶體拷貝和記憶體移動。set容器內所有元素都是以節點的方式來儲存,其節點結構和連結串列差不多,指向父節點和子節點。因此插入的時候只需要稍做變換,把節點的指標指向新的節點就可以了。刪除的時候類似,稍做變換後把指向刪除節點的指標指向其他節點也OK了。
map:
在map中存放的是鍵值對。
在定義的時候可以這樣定義:
map<int, string> map1; 即將int與string繫結。
插入:
map1.insert(make_pair(1060, "張三")); 使用make_pair函式,map裡放的型別是pair型別
map1[1022] = "張琪"; 這樣也可以插入
注意:operator[] 副作用
string name1 = map1[1022]; 如果這樣使用查詢操作,如果沒有的話會自己插入一個,並且第二個元素為空。
map可以解決找出海量資料中出現次數最多的元素的問題。
海量資料處理方法:分治法(大檔案分成記憶體能夠載入的多個小檔案 雜湊對映) + 雜湊統計 + 小跟堆結構
容器介面卡:
棧 stack、佇列 queue、優先順序佇列 priority_queue。容器介面卡沒有迭代器,沒有底層資料結構。容器介面卡,就是對容器的一個封裝。
eg:棧stack和佇列queue底下使用的就是deque實現。
為啥用deque不用vector :初期開闢的空間多,效率高(vector最開始只開闢1位元組,後進行2倍擴容)。 並且再擴容的時候,deque只需要複製地址,而vector需要拷貝資料。
優先佇列priority_queue選的是vector,vector可以計算下標,deque不連續,無法計算。
容器都是類模板。它們例項化後就成為容器類。用容器類定義的物件稱為容器物件。
stack(deque) : 棧 操作有:push pop empty size top
stack<int> s1;
stack<int, vector<int>> s2;???????
queue(deque) : 佇列 push pop empty size front back/rear
priority_queue(vector) : 優先順序佇列 push pop empty size top
補充:
對於vector來說,最開始只開闢了1位元組,後進行2倍擴容。效率不高。所以提供了兩個函式reserve() 和resize()。
vector<int> vec1, vec2;
vec1.resize(100); // 不僅會開闢記憶體,還給記憶體上添加了元素
vec2.reserve(100); // 只開闢空間
迭代器:正向/反向迭代器
要訪問容器中的元素,需要通過“迭代器(iterator)”進行。迭代器相當於容器和操縱容器的演算法之間的中介。迭代器可以指向容器中某個元素,通過*可以訪問容器元素,所以與指標類似。
迭代器的設計:每個資料型別都有自己的迭代器,並不是共用一個迭代器。
迭代器的目的:是使用者脫離容器。不需要關心底層實現。
迭代器的定義:迭代器型別設計成了容器型別的巢狀型別。
正向迭代器:容器類名::iterator 迭代器名;
反向迭代器:容器類名::reverse_iterator 迭代器名;
正反迭代器的區別:
- 正向迭代器進行
++
操作時,迭代器會指向容器中的後一個元素;呼叫begin函式返回第一個元素的迭代器,從前向後遍歷。 - 反向迭代器進行
++
操作時,迭代器會指向容器中的前一個元素;呼叫rbegin函式返回最後一個元素的迭代器,從後向前遍歷。
按迭代器的功能分類:正向迭代器,反向迭代器(可以使用--運算子),隨機訪問迭代器(可以使用+=,<等。還支援隨機訪問p[i])。
vector | 隨機訪問 |
deque | 隨機訪問 |
list | 雙向 |
set / multiset | 雙向 |
map / multimap | 雙向 |
迭代器失效問題:
1、使用erase函式刪除迭代器中的一個元素,導致迭代器失效。
正常思路:(刪除容器中的偶數)
for (it1 = vec.begin(); it1 != vec.end(); ++it1)
{
if (*it1 % 2 == 0)
{
vec.erase(it1);
}
}
正確的使用方式:erase函式是有返回值的。返回被刪除元素的下一個元素的迭代器。
for (it1 = vec.begin(); it1 != vec.end(); )
{
if (*it1 % 2 == 0)
{
it1 = vec.erase(it1); 利用返回值,保證迭代器不會失效
}
else
{
++it1;
}
}
2、使用insert函式插入一個元素,導致迭代器失效。
錯誤程式碼就不演示了!正確使用如下:
在所有奇數前插入0
for (it1 = vec.begin(); it1 != vec.end(); ++it1) 3、再加1,指向第二個奇數
{
if (*it1 % 2 != 0)
{
it1 = vec.insert(it1, 0); 1、跟erase一樣有返回值,返回的是插入的元素的迭代器。也就是指向了0.
++it1; 2、加1指向了插入的0元素之前的那個元素
}
}
C++ Primier的總結
關於容器的迭代器失效的問題,C++ Primier用了一小節作了總結:
(1)增加元素到容器後
對於vector和string,如果容器記憶體被重新分配,iterators,pointers,references失效;如果沒有重新分配,那麼插入點之前的iterator有效,插入點之後的iterator失效;
對於deque,如果插入點位於除front和back的其它位置,iterators,pointers,references失效;當我們插入元素到front和back時,deque的迭代器失效,但reference和pointers有效;
對於list和forward_list,所有的iterator,pointer和refercnce有效。
(2)從容器中移除元素後
對於vector和string,插入點之前的iterators,pointers,references有效;off-the-end迭代器總是失效的;
對於deque,如果插入點位於除front和back的其它位置,iterators,pointers,references失效;當我們插入元素到front和back時,off-the-end失效,其他的iterators,pointers,references有效;
對於list和forward_list,所有的iterator,pointer和refercnce有效。
(3)在迴圈中refresh迭代器
當處理vector,string,deque時,當在一個迴圈中可能增加或移除元素時,要考慮到迭代器可能會失效的問題。我們一定要refresh迭代器。
迭代器的輔助函式
#include <algorithm> //使用STL演算法需要包含此標頭檔案
STL 中有用於操作迭代器的三個函式模板,它們是:
- advance(p, n):使迭代器 p 向前或向後移動 n 個元素。
- distance(p, q):計算兩個迭代器之間的距離,即迭代器 p 經過多少次 + + 操作後和迭代器 q 相等。如果呼叫時 p 已經指向 q 的後面,則這個函式會陷入死迴圈。
- iter_swap(p, q):用於交換兩個迭代器 p、q 指向的值。
空間配置器 :Allocator
由於new關鍵字在申請物件記憶體的時候不僅會申請記憶體還會構造很多物件,但很多時候,這些物件我們其實並不需要使用。我們只需要在使用這個空間的時候在構造物件。
所以我們需要將開闢記憶體(allocate)和構造物件(construst)分開使用。就有了Allocator空間配置器。(delete同理)
實現簡單的向量容器+迭代器+空間配置器
template<typename T>
struct Allocator
{
// allocate開闢記憶體
T* allocate(size_t size)
{
return (T*)malloc(size); malloc只申請空間,並不構造物件
}
// deallocate釋放記憶體
void deallocate(void *ptr)
{
free(ptr); 只釋放空間
}
// construct構造物件
void construct(void *ptr, const T &val)
{
// new 定位new
new (ptr) T(val); 表示在ptr指向的記憶體,構造一個值為val的物件
}
//destroy析構物件
void destroy(T *ptr)
{
ptr->~T(); 只析構物件,並不釋放記憶體。建構函式不能自己單獨呼叫,解構函式可以
}
};
template<typename T>
class Vector
{
public:
Vector(int size=10):mSize(size),mCur(0)
{
//mpVec = new T [size];
mpVec = allocator.allocate(size * sizeof(T));
}
~Vector()
{
//delete[] mpVec;
for(int i = 0 ; i < mCur ; ++i)
{
allocator.destroy(mpVec + i);
}
allocator.deallocate(mpVec);
mpVec = NULL;
}
Vector(const Vector &src)
{
mSize = src.mSize;
mCur = src.mCur;
//mpVec = new T [src.mSize];
mpVec = allocator.allocate(mCur * sizeof(T));
for(int i = 0;i < src.mCur;++i) 由於使用類模板,所以不能使用memcpy,防止淺拷貝的發生。
{
//mpVec[i] = src.mpVec[i];
allocator.constructa(mpVec + i, src.mpVec[i]);
}
}
Vector<T>& operator=(const Vector<T> &src) 只要返回的東西還活著就返回引用。
返回引用就可以當作左值(不是立即數,立即數是通過暫存器返回的)。
{
if(mpVec == src.mpVec)
{
return *this;
}
//delete[] mpVec;
for(int i = 0 ; i < mCur ; ++i)
{
allocator.destroy(mpVec + i);
}
//mpVec = new T [src.mSize];
mpVec = allocator.allocate(src.mSize * sizeof(T));
for(int i = 0;i < src.mCur;++i)
{
//mpVec[i] = src.mpVec[i];
allocator.constructa(mpVec + i, src.mpVec[i]);
}
mSize = src.mSize;
mCur = src.mCur;
return *this;
}
int operator [] (int i)
{
return mpVec[i];
}
void push_back(const T &val) // 從末尾給向量容器新增元素
{
if(mCur == mSize)
{
reSize();
}
//mpVec[mCur++] = val;
allocator.construct(mpVec + mCur,val);
mCur++;
}
void pop_back() // 從末尾刪除向量容器的元素
{
if(mCur == 0)
{
return ;
}
mCur--;
allocator.destroy(mpVec + mCur);
}
給向量容器Vector實現迭代器
class iterator
{
public:
iterator(T *pos):mp(pos)
{}
bool operator != (const iterator &src)
{
return mp != src.mp;
}
void operator ++ ()
{
mp++;
}
T operator * ()
{
return *mp;
}
private:
T *mp;
};
iterator begin()// 返回首元素迭代器
{
return iterator(mpVec);
}
iterator end() // 返回末尾後繼位置的迭代器
{
return iterator(mpVec + mCur);
}
private:
T *mpVec;
int mSize; // 擴容的總大小
int mCur; // 當前元素的個數
Allocator<T> allocator;//空間配置器
friend ostream& operator<<(ostream &out, const Vector<T> &src);
void reSize() // 向量容器擴容函式,預設2倍擴容
{
T *tmp = new T [mSize * 2];
mSize *= 2;
for(int i = 0;i < mCur;++i)
{
tmp[i] = mpVec[i];
}
delete[] mpVec;
mpVec = tmp;
}
};
template<ypename T>
ostream& operator<<(ostream &out, const Vector<T> &src)
{
out << src.mpVec;
return out;
}