C++面試基礎知識整理(8)
阿新 • • 發佈:2021-01-31
目錄
標準模板庫
STL基本組成
- 容器、迭代器、仿函式、演算法、分配器、配接器
- 他們之間的關係:分配器給容器分配儲存空間,演算法通過迭代器獲取容器中的內容,仿函式可以協助演算法完成各種操作,配接器用來套接適配仿函式
元件 | 描述 |
---|---|
容器(Containers) | 容器是用來管理某一類物件的集合。C++ 提供了各種不同型別的容器,比如 deque、list、vector、map 等。 |
演算法(Algorithms) | 演算法作用於容器。它們提供了執行各種操作的方式,包括對容器內容執行初始化、排序、搜尋和轉換等操作。 |
迭代器(iterators) | 迭代器用於遍歷物件集合的元素。這些集合可能是容器,也可能是容器的子集。 |
動態陣列實現原理
// 改變陣列容量的大小
void resize(int newCapacity)
{
assert(newCapacity>=size);
T *newData=new T[newCapacity];
for(int i=0;i<size;i++ )
newData[i]=data[i];
deleta[] data;
data=newData;
capacity=newCapacity;
}
// 向陣列中新增一個元素
void push_bakc(T e)
{
if(size == capacity)
resize(2*capacity);// 改變陣列的容量為原來的兩倍
data[size++]=e;
}
// 從陣列中刪除一個元素
T pop_back ()
{
assert(size>0);
T ret=data[size-1];
size--;
if(size==capacity/4)
resize(capacity/2);// 改變陣列的容量為原來的四分之一
return ret;
}
-
當陣列中元素的個數等於陣列的容量時,此時若再新增一個元素,則會重新分配記憶體,將陣列的容量改變為原來的兩倍,並將陣列中的元素賦值到新陣列中,以實現動態陣列。
-
前n次賦值的時間複雜度為n,最後一次賦值的時間複雜度也為n,所以均攤時間複雜度為2n/(n+1)=2,為O(1).
-
當從陣列中刪除元素時,若陣列中元素的個數僅等於原來的1/2,就改變陣列的容量,雖然刪除元素的均攤時間複雜度仍為O(1),但在此時若重複進行插入與刪除操作,會不斷地為陣列分配記憶體,使陣列容量變為原來的兩倍或1/2,會使得均攤複雜度變為O(n),導致複雜度的震盪。
-
為避免複雜度震盪,應當在陣列中元素的個數等於原來的1/4時,再改變陣列的容量為原來的1/2,如上面程式碼所示。
vector和list
-
底層結構
- vector的底層結構是動態順序表,在記憶體中是一段連續的空間。
- list的底層結構是帶頭節點的雙向迴圈連結串列,在記憶體中不是一段連續的空間。
-
隨機訪問[]
- vector支援隨機訪問,可以利用下標精準定位到一個元素上,訪問某個元素的時間複雜度是O(1)。
- list不支援隨機訪問,要想訪問list中的某個元素只能是從前向後或從後向前依次遍歷,時間複雜度是O(N)。
-
插入和刪除
- vector任意位置插入和刪除的效率低,因為它每插入一個元素(尾插除外),都需要搬移資料,時間複雜度是O(N),而且插入還有可能要增容,這樣一來還要開闢新空間,導致效率低下。
- list任意位置插入和刪除的效率高,他不需要搬移元素,只需要改變插入或刪除位置的前後兩個節點的指向即可,時間複雜度為O(1)。
-
適用場景
- vector適合需要高效率儲存,需要隨機訪問,並且不關心插入和刪除效率的場景。
- list適合有大量的插入和刪除操作,並且不關心隨機訪問的場景。
vector迭代器失效
- 失效的兩種情況:
- 當插入元素後,如果儲存空間重新分配,則原迭代器指向的記憶體不再是vector,導致失效。
- 當刪除元素時,後面所有的元素會向前移動一個位置,導致迭代器指向的下一個位置是未知記憶體。
int main()
{
vector<int> vec(5, 0);
cout << &vec[0] << endl; // 列印陣列元素首地址
for (int i = 0; i < vec.size(); i++)
{
vec[i] = i;
}
vector<int>::iterator iter = vec.begin();
// 插入元素時,迭代器失效
vec.push_back(0);
cout << &vec[0] << endl;// 陣列擴容後,首地址已經改變
//cout << *iter << endl;
// 刪除元素時迭代器失效
for (iter = vec.begin(); iter != vec.end();)
{
if (*iter == 3) {
//vec.erase(iter);// 如果不給iter重新複製,則迭代器會失效
iter = vec.erase(iter);
}
cout << *iter << endl;
iter++;
}
return 0;
}
deque
- deque雙端佇列,由一段一段的定量連續空間構成。一旦要在 deque 的前端和尾端增加新空間,便配置一段定量連續空間,串在整個 deque 的頭端或尾端。
- 因此不論在尾部或頭部安插元素都十分迅速。 在中間部分安插元素則比較費時,因為必須移動其它元素。
- 優點:支援隨機訪問,即 [] 操作和 .at(),所以查詢效率高;可在雙端進行 pop,push。
- 缺點:不適合中間插入刪除操作;佔用記憶體多。
- 適用場景:適用於既要頻繁隨機存取,又要關心兩端資料的插入與刪除的場景。
set
- set集合由紅黑樹實現,內部元素依據其值自動排序,每個元素值只能出現一次,不允許重複。
- map 和 set 的插入刪除效率比用其他序列容器高,因為對於關聯容器來說,不需要做記憶體拷貝和記憶體移動。
- 優點:使用平衡二叉樹實現,便於元素查詢(時間複雜度為O(logN)),且保持了元素的唯一性,以及能自動排序。
- 缺點:每次插入值的時候,都需要調整紅黑樹,效率有一定影響。
- 適用場景:適用於經常查詢一個元素是否在某群集中且需要排序的場景。
map
- map 由紅黑樹實現,其元素都是 “鍵值/實值” 所形成的一個對組。內部元素根據鍵值自動排序。每個鍵值只能出現一次,不允許重複。
- 優點:使用平衡二叉樹實現,便於元素查詢,且能把一個值對映成另一個值,可以建立字典。
- 缺點:每次插入值的時候,都需要調整紅黑樹,效率有一定影響。
- 適用場景:適用於需要儲存一個數據字典,並要求方便地根據key找value的場景。
map和set的區別
- set的迭代器是const的,不允許修改元素的值;map允許修改value,但不允許修改key。原因是map和set是根據關鍵字排序來保證其有序性的,如果允許修改key的話,那麼首先需要刪除該鍵,然後調節平衡,再插入修改後的鍵值,調節平衡,如此一來,嚴重破壞了map和set的結構,導致iterator失效,不知道應該指向改變前的位置,還是指向改變後的位置。
- map支援下標操作,set不支援下標操作。map可以用key做下標,map的下標運算子[ ]將關鍵碼作為下標去執行查詢,如果關鍵碼不存在,則插入一個具有該關鍵碼和mapped_type型別預設值的元素至map中,因此下標運算子[ ]在map應用中需要慎用。如果find能解決需要,儘可能用find。
map和unordered_map的區別
-
map,其底層是基於紅黑樹實現的
- 優點:
- 有序性,這是map結構最大的優點,其元素的有序性在很多應用中都會簡化很多的操作
- map的查詢、刪除、增加等一系列操作時間複雜度穩定,都為logn
- 缺點:
- 查詢、刪除、增加等操作平均時間複雜度較慢,與n相關
- 優點:
-
unordered_map,其底層是一個雜湊表
- 優點如下:
- 查詢、刪除、新增的速度快,時間複雜度為常數級O©
- 缺點如下:
- 因為unordered_map內部基於雜湊表,以(key,value)對的形式儲存,因此空間佔用率高
- Unordered_map的查詢、刪除、新增的時間複雜度不穩定,平均為O©,取決於雜湊函式。極端情況下可能為O(n)
- 優點如下:
allocator分配器
作用
- 一般情況下,記憶體分配主要使用new和delete,但是new將記憶體分配和物件構造組合在了一起,delete將物件析構和記憶體釋放組合在了一起。一般情況下,將記憶體分配和物件構造組合在一起可能會導致不必要的浪費。
- 當分配一大塊記憶體時,我們通常計劃在這塊記憶體上按需構造物件。在此情況下,我們希望將記憶體分配和物件構造分離。
- **allocator允許記憶體分配和物件初始化的分離。**它提供一種型別感知的記憶體分配方法,它分配的記憶體是原始的、未構造的。
使用
int test_allocator_1()
{
std::allocator<std::string> alloc; // 可以分配string的allocator物件
// 記憶體分配
int n = 5;
auto const p = alloc.allocate(n); // 分配n個未初始化的string
// 物件初始化
auto q = p; // q指向最後構造的元素之後的位置
alloc.construct(q++); // *q為空字串
alloc.construct(q++, 10, 'c'); // *q為cccccccccc
alloc.construct(q++, "hi"); // *q為hi
std::cout << *p << std::endl; // 正確:使用string的輸出運算子
//std::cout << *q << std::endl; // 災難:q指向未構造的記憶體
std::cout << p[0] << std::endl;
std::cout << p[1] << std::endl;
std::cout << p[2] << std::endl;
// 物件析構
while (q != p) {
alloc.destroy(--q); // 釋放我們真正構造的string
}
// 記憶體釋放
alloc.deallocate(p, n);
return 0;
}