[C++ STL] 各容器簡單介紹


一、什么是STL?

1、STL(Standard Template Library),即標准模板庫,是一個高效的C++程序庫,包含了諸多常用的基本數據結構和基本算法。為廣大C++程序員們提供了一個可擴展的應用框架,高度體現了軟件的可復用性。

2、從邏輯層次來看,在STL中體現了泛型化程序設計的思想(generic programming)。在這種思想里,大部分基本算法被抽象,被泛化,獨立於與之對應的數據結構,用於以相同或相近的方式處理各種不同情形。

3、從實現層次看,整個STL是以一種類型參數化(type parameterized)的方式實現的,基於模板(template)。


STL有六大組件,但主要包含容器、迭代器和算法三個部分。

  • 容器(Containers):用來管理某類對象的集合。每一種容器都有其優點和缺點,所以,為了應付程序中的不同需求,STL 准備了七種基本容器類型。
  • 迭代器(Iterators):用來在一個對象集合的元素上進行遍歷動作。這個對象集合或許是個容器,或許是容器的一部分。每一種容器都提供了自己的迭代器,而這些迭代器了解該種容器的內部結構。
  • 算法(Algorithms):用來處理對象集合中的元素,比如 Sort,Search,Copy,Erase 那些元素。通過迭代器的協助,我們只需撰寫一次算法,就可以將它應用於任意容器之上,這是因為所有容器的迭代器都提供一致的接口。

STL 的基本觀念就是將數據和操作分離。數據由容器進行管理,操作則由算法進行,而迭代器在兩者之間充當粘合劑,使任何算法都可以和任何容器交互運作。這一篇博客暫時只介紹容器,下一篇介紹迭代器。


二、容器(Containers)

容器用來管理某類對象。為了應付程序中的不同需求,STL 准備了兩類共七種基本容器類型:

  • 序列式容器(Sequence containers),此為可序群集,其中每個元素均有固定位置—取決於插入時機和地點,和元素值無關。如果你以追加方式對一個群集插入六個元素,它們的排列次序將和插入次序一致。STL提供了三個序列式容器:向量(vector)、雙端隊列(deque)、列表(list),此外你也可以把 string 和 array 當做一種序列式容器。
  • 關聯式容器(Associative containers),此為已序群集,元素位置取決於特定的排序准則以及元素值,和插入次序無關。如果你將六個元素置入這樣的群集中,它們的位置取決於元素值,和插入次序無關。STL提供了四個關聯式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。

示意圖如下圖所示:


2.1 vector

vector(向量): 是一種序列式容器,事實上和數組差不多,但它比數組更優越。一般來說數組不能動態拓展,因此在程序運行的時候不是浪費內存,就是造成越界。而 vector 正好彌補了這個缺陷,它的特征是相當於可拓展的數組(動態數組),它的隨機訪問快,在中間插入和刪除慢,但在末端插入和刪除快。


特點

  • 擁有一段連續的內存空間,並且起始地址不變,因此它能非常好的支持隨機存取,即 [] 操作符,但由於它的內存空間是連續的,所以在中間進行插入和刪除會造成內存塊的拷貝,另外,當該數組后的內存空間不夠時,需要重新申請一塊足夠大的內存並進行內存的拷貝。這些都大大影響了 vector 的效率。
  • 對頭部和中間進行插入刪除元素操作需要移動內存,如果你的元素是結構或類,那么移動的同時還會進行構造和析構操作,所以性能不高。
  • 對最后元素操作最快(在后面插入刪除元素最快),此時一般不需要移動內存,只有保留內存不夠時才需要。

優缺點和適用場景

優點:支持隨機訪問,即 [] 操作和 .at(),所以查詢效率高。

缺點:當向其頭部或中部插入或刪除元素時,為了保持原本的相對次序,插入或刪除點之后的所有元素都必須移動,所以插入的效率比較低。

適用場景:適用於對象簡單,變化較小,並且頻繁隨機訪問的場景。

例子

以下例子針對整型定義了一個 vector,插入 6 個元素,然后打印所有元素:

#include <iostream>
#include <vector>

using namespace std;

int main(int argc, char* argv[])
{
	vector<int> vecTemp;

	for (int i = 0; i<6; i++)
		vecTemp.push_back(i);

	for (int i = 0; i<vecTemp.size(); i++)
		cout << vecTemp[i] <<" "; // 輸出:0 1 2 3 4 5

	return 0;
}

2.2 deque

deque(double-ended queue)是由一段一段的定量連續空間構成。一旦要在 deque 的前端和尾端增加新空間,便配置一段定量連續空間,串在整個 deque 的頭端或尾端。因此不論在尾部或頭部安插元素都十分迅速。 在中間部分安插元素則比較費時,因為必須移動其它元素。deque 的最大任務就是在這些分段的連續空間上,維護其整體連續的假象,並提供隨機存取的接口。


特點

  • 按頁或塊來分配存儲器的,每頁包含固定數目的元素。
  • deque 是 list 和 vector 的折中方案。兼有 list 的優點,也有vector 隨機線性訪問效率高的優點。

優缺點和適用場景

優點:支持隨機訪問,即 [] 操作和 .at(),所以查詢效率高;可在雙端進行 pop,push。

缺點:不適合中間插入刪除操作;占用內存多。

適用場景:適用於既要頻繁隨機存取,又要關心兩端數據的插入與刪除的場景。

例子

以下例子聲明了一個浮點類型的 deque,並在容器尾部插入 6 個元素,最后打印出所有元素。

#include <iostream>
#include <deque>

using namespace std;

int main(int argc, char* argv[])
{
	deque<float> dequeTemp;

	for (int i = 0; i<6; i++)
		dequeTemp.push_back(i);

	for (int i = 0; i<dequeTemp.size(); i++)
		cout << dequeTemp[i] << " "; // 輸出:0 1 2 3 4 5

	return 0;
}

2.3 list

List 由雙向鏈表(doubly linked list)實現而成,元素也存放在堆中,每個元素都是放在一塊內存中,他的內存空間可以是不連續的,通過指針來進行數據的訪問,這個特點使得它的隨機存取變得非常沒有效率,因此它沒有提供 [] 操作符的重載。但是由於鏈表的特點,它可以很有效率的支持任意地方的插入和刪除操作。

特點

  • 沒有空間預留習慣,所以每分配一個元素都會從內存中分配,每刪除一個元素都會釋放它占用的內存。
  • 在哪里添加刪除元素性能都很高,不需要移動內存,當然也不需要對每個元素都進行構造與析構了,所以常用來做隨機插入和刪除操作容器。
  • 訪問開始和最后兩個元素最快,其他元素的訪問時間一樣。

優缺點和適用場景

優點:內存不連續,動態操作,可在任意位置插入或刪除且效率高。

缺點:不支持隨機訪問。

適用場景:適用於經常進行插入和刪除操作並且不經常隨機訪問的場景。

例子

以下例子產生一個空 list,准備放置字符,然后將 'a' 至 'z' 的所有字符插入其中,利用循環每次打印並移除集合的第一個元素,從而打印出所有元素:

#include <iostream>
#include <list>

using namespace std;

int main(int argc, char* argv[])
{
	list<char> listTemp;

	for (char c = 'a'; c <= 'z'; ++c)
		listTemp.push_back(c);

	while (!listTemp.empty())
	{
		cout <<listTemp.front() << " ";
		listTemp.pop_front();
	}

	return 0;
}

成員函數empty()的返回值告訴我們容器中是否還有元素,只要這個函數返回 false,循環就繼續進行。循環之內,成員函數front()會返回第一個元素,pop_front()函數會刪除第一個元素。

注意:list<指針> 完全是性能最低的做法,還不如直接使用 list<對象> 或使用 vector<指針> 好,因為指針沒有構造與析構,也不占用很大內存。


2.4 set

set(集合)由紅黑樹實現,其內部元素依據其值自動排序,每個元素值只能出現一次,不允許重復。


特點

  • set 中的元素都是排好序的,集合中沒有重復的元素;
  • map 和 set 的插入刪除效率比用其他序列容器高,因為對於關聯容器來說,不需要做內存拷貝和內存移動。

優缺點和適用場景

優點:使用平衡二叉樹實現,便於元素查找,且保持了元素的唯一性,以及能自動排序。

缺點:每次插入值的時候,都需要調整紅黑樹,效率有一定影響。

適用場景:適用於經常查找一個元素是否在某群集中且需要排序的場景。

例子

下面的例子演示 set(集合)的兩個特點:

#include <iostream>
#include <set>

using namespace std;

int main(int argc, char* argv[])
{
	set<int> setTemp;

	setTemp.insert(3);
	setTemp.insert(1);
	setTemp.insert(2);
	setTemp.insert(1);

	set<int>::iterator it;
	for (it = setTemp.begin(); it != setTemp.end(); it++)
	{
		cout << *it << " ";
	}

	return 0;
}

輸出結果:1 2 3。一共插入了 4 個數,但是集合中只有 3 個數並且是有序的,可見之前說過的 set 集合的兩個特點,有序和不重復。


當 set 集合中的元素為結構體時,該結構體必須實現運算符 ‘<’ 的重載:

#include <iostream>
#include <set>
#include <string>

using namespace std;

struct People
{
	string name;
	int age;

	bool operator <(const People p) const
	{
		return age < p.age;
	}
};

int main(int argc, char* argv[])
{
	set<People> setTemp;

	setTemp.insert({"張三",14});
	setTemp.insert({ "李四", 16 });
	setTemp.insert({ "隔壁老王", 10 });

	set<People>::iterator it;
	for (it = setTemp.begin(); it != setTemp.end(); it++)
	{
		printf("姓名:%s 年齡:%d\n", (*it).name.c_str(), (*it).age);
	}

	return 0;
}

/*
輸出結果
姓名:王二麻子 年齡:10
姓名:張三 年齡:14
姓名:李四 年齡:16 
*/ 

可以看到結果是按照年齡由小到大的順序排列。另外 string 要使用c_str()轉換一下,否則打印出的是亂碼。


另外 Multiset 和 set 相同,只不過它允許重復元素,也就是說 multiset 可包括多個數值相同的元素。這里不再做過多介紹。


2.5 map

map 由紅黑樹實現,其元素都是 “鍵值/實值” 所形成的一個對組(key/value pairs)。每個元素有一個鍵,是排序准則的基礎。每一個鍵只能出現一次,不允許重復。

map 主要用於資料一對一映射的情況,map 內部自建一顆紅黑樹,這顆樹具有對數據自動排序的功能,所以在 map 內部所有的數據都是有序的。比如一個班級中,每個學生的學號跟他的姓名就存在着一對一映射的關系。


特點

  • 自動建立 Key - value 的對應。key 和 value 可以是任意你需要的類型。
  • 根據 key 值快速查找記錄,查找的復雜度基本是 O(logN),如果有 1000 個記錄,二分查找最多查找 10次(1024)。
  • 增加和刪除節點對迭代器的影響很小,除了那個操作節點,對其他的節點都沒有什么影響。
  • 對於迭代器來說,可以修改實值,而不能修改 key。

優缺點和適用場景

優點:使用平衡二叉樹實現,便於元素查找,且能把一個值映射成另一個值,可以創建字典。

缺點:每次插入值的時候,都需要調整紅黑樹,效率有一定影響。

適用場景:適用於需要存儲一個數據字典,並要求方便地根據key找value的場景。

例子

#include "stdafx.h"
#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(int argc, char* argv[])
{
	map<int, string> mapTemp;

	mapTemp.insert({ 5,"張三" });
	mapTemp.insert({ 3, "李四"});
	mapTemp.insert({ 4, "隔壁老王" });

	map<int, string>::iterator it;
	for (it = mapTemp.begin(); it != mapTemp.end(); it++)
	{
		printf("學號:%d 姓名:%s\n", (*it).first, (*it).second.c_str());
	}

	return 0;
}

/*
輸出結果:
學號:3 姓名:李四
學號:4 姓名:隔壁老王
學號:5 姓名:張三
*/

multimap 和 map 相同,但允許重復元素,也就是說 multimap 可包含多個鍵值(key)相同的元素。這里不再做過多介紹。


2.6 容器配接器

除了以上七個基本容器類別,為滿足特殊需求,STL還提供了一些特別的(並且預先定義好的)容器配接器,根據基本容器類別實現而成。包括:

1、stack

名字說明了一切,stack 容器對元素采取 LIFO(后進先出)的管理策略。

2、queue

queue 容器對元素采取 FIFO(先進先出)的管理策略。也就是說,它是個普通的緩沖區(buffer)。

3、priority_queue

priority_queue 容器中的元素可以擁有不同的優先權。所謂優先權,乃是基於程序員提供的排序准則(缺省使用 operators)而定義。Priority queue 的效果相當於這樣一個 buffer:“下一元素永遠是queue中優先級最高的元素”。如果同時有多個元素具備最髙優先權,則其次序無明確定義。


三、總結

各容器的特點總結

  • vector 頭部與中間插入和刪除效率較低,在尾部插入和刪除效率高,支持隨機訪問。
  • deque 是在頭部和尾部插入和刪除效率較高,支持隨機訪問,但效率沒有 vector 高。
  • list 在任意位置的插入和刪除效率都較高,但不支持隨機訪問。
  • set 由紅黑樹實現,其內部元素依據其值自動排序,每個元素值只能出現一次,不允許重復,且插入和刪除效率比用其他序列容器高。
  • map 可以自動建立 Key - value 的對應,key 和 value 可以是任意你需要的類型,根據 key 快速查找記錄。

在實際使用過程中,到底選擇這幾種容器中的哪一個,應該根據遵循以下原則:

1、如果需要高效的隨機存取,不在乎插入和刪除的效率,使用 vector。
2、如果需要大量的插入和刪除元素,不關心隨機存取的效率,使用 list。
3、如果需要隨機存取,並且關心兩端數據的插入和刪除效率,使用 deque。
4、如果打算存儲數據字典,並且要求方便地根據 key 找到 value,一對一的情況使用 map,一對多的情況使用 multimap。
5、如果打算查找一個元素是否存在於某集合中,唯一存在的情況使用 set,不唯一存在的情況使用 multiset。

各容器的時間復雜度分析

  • vector 在頭部和中間位置插入和刪除的時間復雜度為 O(N),在尾部插入和刪除的時間復雜度為 O(1),查找的時間復雜度為 O(1);
  • deque 在中間位置插入和刪除的時間復雜度為 O(N),在頭部和尾部插入和刪除的時間復雜度為 O(1),查找的時間復雜度為 O(1);
  • list 在任意位置插入和刪除的時間復雜度都為 O(1),查找的時間復雜度為 O(N);
  • set 和 map 都是通過紅黑樹實現,因此插入、刪除和查找操作的時間復雜度都是 O(log N)。

各容器的共性

各容器一般來說都有下列函數:默認構造函數、復制構造函數、析構函數、empty()、max_size()、size()、operator=、operator<、operator<=、operator>、operator>=、operator==、operator!=、swap()。

順序容器和關聯容器都共有下列函數:

  • begin() :返回容器第一個元素的迭代器指針;
  • end():返回容器最后一個元素后面一位的迭代器指針;
  • rbegin():返回一個逆向迭代器指針,指向容器最后一個元素;
  • rend():返回一個逆向迭代器指針,指向容器首個元素前面一位;
  • clear():刪除容器中的所有的元素;
  • erase(it):刪除迭代器指針it處元素。

參考:

《C++標准庫 - 侯捷》中的 5.2 節-容器



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM