C++ STL第三篇(搞清楚deque原理和有多少用法)

ivanlee717發表於2024-03-16

deque

Vector容器是單向開口的連續記憶體空間,deque則是一種雙向開口的連續線性空間。所謂的雙向開口,意思是可以在頭尾兩端分別做元素的插入和刪除操作,當然,vector容器也可以在頭尾兩端插入元素,但是在其頭部操作效率奇差,無法被接受。

image-20240312101135297

Deque容器和vector容器最大的差異,一在於deque允許使用常數項時間對頭端進行元素的插入和刪除操作。二在於deque沒有容量的概念,因為它是動態的以分段連續空間組合而成,隨時可以增加一段新的空間並連結起來,換句話說,像vector那樣,”舊空間不足而重新配置一塊更大空間,然後複製元素,再釋放舊空間”這樣的事情在deque身上是不會發生的。也因此,deque沒有必須要提供所謂的空間保留(reserve)功能.

雖然deque容器也提供了Random Access Iterator,但是它的迭代器並不是普通的指標,其複雜度和vector不是一個量級,這當然影響各個運算的層面。因此,除非有必要,我們應該儘可能的使用vector,而不是deque。對deque進行的排序操作,為了最高效率,可將deque先完整的複製到一個vector中,對vector容器進行排序,再複製回deque.

實現原理

Deque容器是連續的空間,至少邏輯上看來如此,連續現行空間總是令我們聯想到array和vector,array無法成長,vector雖可成長,卻只能向尾端成長,而且其成長其實是一個假象,事實上(1) 申請更大空間 (2)原資料複製新空間 (3)釋放原空間 三步驟,如果不是vector每次配置新的空間時都留有餘裕,其成長假象所帶來的代價是非常昂貴的。

Deque是由一段一段的定量的連續空間構成。一旦有必要在deque前端或者尾端增加新的空間,便配置一段連續定量的空間,串接在deque的頭端或者尾端。Deque最大的工作就是維護這些分段連續的記憶體空間的整體性的假象,並提供隨機存取的介面,避開了重新配置空間,複製,釋放的輪迴,代價就是複雜的迭代器架構。

既然deque是分段連續記憶體空間,那麼就必須有中央控制,維持整體連續的假象,資料結構的設計及迭代器的前進後退操作頗為繁瑣。Deque程式碼的實現遠比vector或list都多得多。

Deque採取一塊所謂的map(注意,不是STL的map容器)作為主控,這裡所謂的map是一小塊連續的記憶體空間,其中每一個元素(此處成為一個結點)都是一個指標,指向另一段連續性記憶體空間,稱作緩衝區。緩衝區才是deque的儲存空間的主體。

image-20240312105517966

宣告方法

  1. 預設構造形式:使用預設建構函式建立一個空的deque容器。
deque<T> deqT;
deque<int> r1;//預設構造形式:建立一個空的deque容器
  1. 範圍建構函式:使用指定範圍內的元素建立deque容器,將[beg, end)區間中的元素複製給本身。
deque<T> deqT(beg, end);
int arr[] = { 1,2,3,4,5 };
deque<int> r2(arr, arr + 5);// 範圍建構函式:將指定範圍內的元素複製給本身

其中,beg是指向範圍起始位置的迭代器,end是指向範圍結束位置的迭代器。

  1. 值建構函式:使用指定值建立deque容器,將n個elem複製給本身。
deque<T> deqT(n, elem);
deque<int> r3(3, 100);// 建立包含3個值為10的元素的deque

其中,n是要建立的元素數量,elem是要複製的元素值。

  1. 複製建構函式:使用另一個deque容器進行複製構造,建立一個新的deque容器。
deque<T> deqT(deq);
deque<int> deq4(deq2);

其中,deq是要進行複製的deque容器。

賦值操作

  1. assign(beg, end);//將[beg, end)區間中的資料複製賦值給本身。

  2. assign(n, elem);//將n個elem複製賦值給本身。

  3. deque&operator=(const deque &deq); //過載等號運算子

  4. swap(deq);// 將deq與本身的元素互換

template <typename T>
void print(const T& r1, const T& r2){
	std::cout << "r1: ";
	for (const auto& elem : r1) {
		std::cout << elem << " ";
	}
	std::cout << std::endl;

	std::cout << "r2: ";
	for (const auto& elem : r2) {
		std::cout << elem << " ";
	}
	std::cout << std::endl;
	cout << "-------------" << endl;
}
deque<int> r1, r2;
deque<int> data = { 1,2,3,4,5 };
r1.assign(data.begin(), data.end());
print(r1, r2);
r2.assign(3, 100);
print(r1, r2);
r2 = r1;
print(r1, r2);
r1.swap(r2);
print(r1, r2);

image-20240316230550602

容量大小

deque.size();//返回容器中元素的個數

deque.empty();//判斷容器是否為空

deque.resize(num);//重新指定容器的長度為num,若容器變長,則以預設值填充新位置。如果容器變短,則末尾超出容器長度的元素被刪除。

deque.resize(num, elem); //重新指定容器的長度為num,若容器變長,則以elem值填充新位置,如果容器變短,則末尾超出容器長度的元素被刪除。

	deque<int> r1, r2;
	deque<int> data = { 1,2,3,4,5 };
	r1.assign(data.begin(), data.end());
	cout << "r2.empty:" << r2.empty() << endl;
	print(r1, r2);
	r2.assign(3, 100);
	print(r1, r2);
	cout << "r1 size: " << r1.size() << endl;
	r1.resize(8);
	r2.resize(8, 100);
	print(r1, r2);

image-20240316231017869

插入和刪除

push_back(elem);//在容器尾部新增一個資料

push_front(elem);//在容器頭部插入一個資料

pop_back();//刪除容器最後一個資料

pop_front();//刪除容器第一個資料

at(idx);//返回索引idx所指的資料,如果idx越界,丟擲out_of_range。

operator[];//返回索引idx所指的資料,如果idx越界,不丟擲異常,直接出錯。

front();//返回第一個資料。

back();//返回最後一個資料

insert(pos,elem);//在pos位置插入一個elem元素的複製,返回新資料的位置。

insert(pos,n,elem);//在pos位置插入n個elem資料,無返回值。

insert(pos,beg,end);//在pos位置插入[beg,end)區間的資料,無返回值。

clear();//移除容器的所有資料

erase(beg,end);//刪除[beg,end)區間的資料,返回下一個資料的位置。

erase(pos);//刪除pos位置的資料,返回下一個資料的位置。insert 函式的引數 pos 實際上是一個迭代器(iterator)型別,而不是簡單的整數型別
deque<int> r1;
r1.push_back(27);
print(r1);
r1.push_front(12);
print(r1);
// 返回索引idx所指的資料,如果idx越界,丟擲out_of_range。
try {
	int v = r1.at(12);
	cout << "Value at index 12: " << v << std::endl;
	v = r1.at(127);
}
catch (const out_of_range& e) {
	cerr << "Out of range error: " << e.what() << std::endl;
}
// 返回索引idx所指的資料,如果idx越界,不丟擲異常,直接出錯。
int value = r1[1]; // 可能會直接出錯,因為不會丟擲異常

cout << "First value: " << r1.front() << std::endl;// 返回第一個資料
cout << "Last value: " << r1.back() << std::endl;// 返回最後一個資料

//在pos位置插入一個elem元素的複製,返回新資料的位置。
auto it = r1.insert(r1.begin() + 2, 99);
/*在程式碼中使用 auto 關鍵字的作用是讓編譯器根據等號右邊表示式的型別推匯出左邊變數的型別,從而簡化程式碼書寫。
r1.insert(r1.begin() + 2, 99) 這個表示式的返回型別是一個迭代器(iterator),它指向插入的元素位置。
由於迭代器的型別可能很複雜且並非顯式指定,因此可以使用 auto 讓編譯器自動推匯出正確的型別。*/
// 在pos位置插入n個elem資料
r1.insert(r1.begin() + 2, 3, 88);
print(r1);
// 在pos位置插入[beg,end)區間的資料
deque<int> r2 = { 1,2,3 };
r1.insert(r1.begin() + 2, r2.begin(), r2.end());
r2 = r1;
print(r1);
r1.clear();
r1 = r2;
r1.erase(r1.begin() + 2, r1.begin() + 5);
print(r1);
r1.erase(r1.begin()+2);

image-20240316232442997