1. 程式人生 > 其它 >C++面試基礎知識整理(8)

C++面試基礎知識整理(8)

技術標籤:C++c++資料結構面試經驗分享stl

目錄

標準模板庫

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;
}