STL中常用容器的選擇
今天去面試問到了stl的常用容器演算法問題,但是提前沒準備,平時也沒太在意,還有就是忘了。總之,回答得很狼狽。
希望能在這裡整理一下,首先看了一下《STL原始碼剖析》中對STL六大元件是這樣介紹的:
容器(containers):各種資料結構,用來存放資料。從實現的角度看,容器是一種class template。
演算法(algorithms):各種常用演算法,如sort,search,copy...從實現角度看,演算法是一種function template。
迭代器(iterators):扮演容器與演算法之間的膠合劑,是所謂的泛型指標。共有五種型別,及其他的衍生變化。從實現角度看,迭代器是一種將operator*,operator->,operator++,operator--等指標相關操作予以過載的class template。所有STL容器都附帶有自己的專屬容器——是的,只有容器設計者才知道如何遍歷自己的元素。
仿函式(functors):行為類似函式,可以作為演算法的某種策略。從實現角度看,仿函式是過載了一種operator()的class或class template。
配接器(adapters):一種用來修飾容器或仿函式或迭代器介面的東西。配接器的實現技術很難一言以蔽之,不許逐一分析。
配置器(allocators):負責空間配置與管理。從實現的角度看,配置器是一個實現了動態空間配置,空間管理,空間釋放的class template。
記得我第一次看這裡的時候,對這些元件是沒弄懂的,只是囫圇吞棗。看樣子書還是要重複的看才行,隔一段時間再看,一些不懂的東西可能就豁然開朗了。
六大元件之間的關係如下:
根據資料在容器中的排列特性,可分為序列式容器和關聯式容器:
容器
序列式容器
所謂序列式容器,其中的元素都可序,但未必有序。
vector
vector其實和標準庫中的陣列array十分相似。若在用C++程式設計中,你想到了用陣列,那比較好的建議是你可以用vector。他們之間的區別是,陣列是靜態的,空間分配需要程式設計師自己來管理;而vector是動態的,由它的內部機制自己管理記憶體空間。
vector的內部機制,關鍵在於其對空間大小的控制以及重新分配時的資料移動效率。vector採用的是線性的連續空間,在新增元素時,如果超過當時的容量,則容量會擴充至現有的兩倍。不是簡單的增加一個元素空間,因為在連續空間中擴充空間是一個比較複雜的過程,需要重新配置、移動資料、釋放原空間。但是,在erase元素時,容量大小是不會變的。
從效率上看,由於vector是連續的,所以隨機讀取效率很高,但是insert操作,效率比較低。若需要頻繁的進行插入,vector不是很好的選擇。
list
list的資料結構是一個迴圈雙向連結串列,所以只需要一個指標,可以遍歷整個連結串列。list的這種結構,使插入的效率比較高。
list和vector是兩個比較常用的容器,選擇哪一個必須視情況而定。list提供的元素操作很多,這裡稍微列舉一些:
push_front \ pop_front :插入\移除頭結點
push_back \ pop_back :插入\移除尾節點
clear:移除所有節點
remove(value):將數值為value的所有元素移除
unique:移除數值相同的連續元素。只有連續相同的元素,才會被移除剩餘一個。
splice(iterator position, list<T,Allocator>& x ):將x移動到pos位置之前,x必須不同於*this,x中的元素會刪除
splice ( iterator position, list<T,Allocator>& x, iterator i ):將i所指元素,移動到pos之前,pos和i可以是同一個list,在x中i會被刪除
splice ( iterator position, list<T,Allocator>& x, iterator first, iterator last ):將[first, last]內的所有元素移動到pos之前,pos和[first, last]可以指向同一個list,但pos不能在[first, last]之內。[first, last]將從x中刪除。
merge(list& x):將x合併到*this身上,兩個list的內容必須經過遞增排序
reverse():將sort的內容逆向重置。
sort():list不能使用STL的演算法sort,必須使用自己的成員函式sort()
list有一個重要性質:insert和splice都不會造成原有的list迭代器失效。這在vector中是不成立的,因為vector的insert操作會造成空間重新配置,原有的迭代器全部失效。要理解這句話,要結合例項思考一下。
deque
deque 顧名思義是雙端佇列,可以在頭尾兩端分別做元素的插入和刪除操作。從邏輯上看,deque是一種雙向開口的連續線性空間。實際上,它是由分段的連續空間組合而成。跟vector不一樣,它沒有容量的概念。在擴充空間的時候,不用像vector那樣重新配置、移動資料、釋放原空間。deque由一段一段的定量連續空間組成,一旦有必要在deque兩端及頭部或尾部新增加空間,便配置一段定量的連續空間,串接在整個deque的頭部或尾部。
reallocate_map(nodes_to_add, false); } void reserve_map_at_front(size_type nodes_to_add = 1) { if(nodes_to_add > start.node - map) reallocate_map(nodes_to_add, false); } template<class T, class Alloc, size_t BufSize> void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add, bool add_at_front) { size_type old_num_nodes = finish.node - start.node +1; size_type new_num_nodes = old_num_nodes + nodes_to_add; if(map_size > 2 * new_num_nodes) { //在原有的map下操作 } else { //配置一塊新的map空間 } } deque的元素操作:pop_back, pop_front, clear, erase, insert pop_back/pop_front: pop元素的時候,若剛好最後一個緩衝區沒有元素或第一個緩衝區只有一個元素,則要對該緩衝區進行釋放。 clear:deque在最初狀態會保留一個緩衝區,clear之後會回到初始狀態。 erase:可以清除一個元素,也可以清除一個區間的元素。 insert:允許在某個點之前插入一個元素,並設定其值。很容易想象的出來,因為deque的假象是“整體連續”的,所以插入的效率並不會很高,若插入點之前的元素比較少,則移動前面的元素;否則,移動插入點後面的元素。
queue和stack其實是非容器,準確說應該是配接器。它們底層是用其他容器實現的,預設通過deque,用list也可以。這裡暫且把他倆放在容器這塊。
stack
stack是一種先進後出的資料結構,只允許在一端插入元素和移出或取得最頂端元素。 STL中stack是以某種既有的容器為底部結構,將其介面改變,使其符合stack“先進後出”的特性。所以往往我們並不把stack歸結為容器,而是被歸類為配接器。 template<class T, class Sequence = deque<T> > class stack{ ..... } 從stack的原始碼可以看到,只要底層容器的函式有empty,size,back,pop_back, push_back, 就可以作為stack的底層容器。list和deque都符合,stack的預設底層容器是deque。 還要注意點,stack必須符合先進後出的特性,且只允許在頂端操作,所以stack是不提供迭代器的。queue
queue是一種“先進先出”的資料結構。只允許底端加入元素,頂端取出元素,沒有其他方法可以存取queue的其他元素,即不可以實現遍歷。queue和stack一樣,被歸類為配接器,不應該屬於容器。也可以通過既有容器deque,list作為它的底部結構。queue也沒有迭代器。
關聯式容器
set
set的特性是,所有元素都會根據元素的鍵值進行自動排序。set元素的鍵值就是實值,不允許兩個元素有相同的鍵值。不能根據set的迭代器改變set的元素值,因為元素的實值就是set的鍵值,改變會破壞排序規則。
set擁有和list某些相同的性質:當進行元素新增操作或刪除操作後,操作之前的所有迭代器都依然有效,除了被刪除的那個元素的迭代器。
set採用的底層機制是紅黑樹--RB-tree。紅黑樹是一種平衡二叉搜尋樹,自動排序的效果很不錯。
注意一點:在set中一般不用stl中的find演算法,一般用set提供的find方法更有效率。
multiset的特性以及用法 和set完全一樣,唯一區別是multiset允許鍵值重複。因為它的插入操作採用的是RB-tree的insert_equal,而不是insert_unique。
map
map的特性是,所有的元素會根據元素的鍵值自動排序,map的所有元素都是pair,pair的第一元素視為鍵值,第二元素視為實值。map不允許元素擁有相同的鍵值,不可改變map的鍵值,但可以修正元素的實值。
map在進行新增操作或刪除操作之後,操作之前的迭代器也都依然有效。因為map和set一樣,底層也是採用紅黑樹——RB-tree來實現。
multimap的特性以及用法 和map完全一樣,唯一區別是multimap允許鍵值重複。因為它的插入操作採用的是RB-tree的insert_equal,而不是insert_unique。
priority_queue
priority_queue 是一個擁有權值的queue,其內的元素並非按照被推入的次序排列,而是依照元素的權值排列。在預設情況下,priority queue底層是利用max heap完成,大頂堆是通過vector實現的完全二叉樹——complete binary tree。
heap並不屬於STL容器元件,它是個幕後英雄,扮演priority_queue的助手。binary heap是一種完全二叉樹,有max heap和min heap之分,STL中採用的是max heap——大頂堆。所以priority queue允許使用者以任何次序將元素推入容器內,但取出時一定從優先順序最高的元素開始取。
可以思考,priority queue為什麼不採用list或binary search tree作為底層機制?
若使用list,元素插入達到常數級別,但取出極值,需要對整個list線性掃描;也可先對list進行排序,這時取極值很快,但插入操作只有線性表現。
若使用binary search tree, 插入和取極值的時間複雜度都可達到o(log(n)), 但一來二叉查詢樹的輸入需要足夠的隨機性(沒弄懂);二來binary search tree的實現不容易。
hashtable
hash table實現是通過hash function實現的,但hash function 會帶來“碰撞”問題。避免元素“碰撞”的方法比較多,常用的是二次線性探測和開鏈法。在STL中,hash table採用的是開鏈法。
hash table內的元素為bucket, 每個bucket維護一個link list,但list並不是用stl的list和slist,而是自己定義hash table node。buckets聚合體則是由vector來完成,以便有動態擴充套件的能力。template<class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
}
注意一點,hashtable的迭代器沒有後退操作。
hashtable的模版引數很多,要正確運用它不太容易。
template<class Value, class Key, //節點的實值型別
class HashFcn, //節點的鍵值型別
class ExtractKey, //從節點中取出鍵值的方法(函式或仿函式)
class EqualKey, //判斷鍵值相同與否的方法(函式或仿函式)
class Alloc>//空間配置器,預設std::alloc
class hashtable{
....
}
雖然開鏈法(separate chaining)並不要求表格大小為質數, 但SGI STL仍以質數來設計表格大小。並且先將28個質數計算好(大約兩倍的關係), 以備隨時訪問, 同時提供一個函式,用來查詢28個質數中,最接近某數並大於某數的質數。
在進行插入操作時,表格有可能會重建。表格是否重建的判斷,是拿元素個數和bucket vector的大小來比,若前者大於後者,則重建。表格重建操作分解:
前面說過,hashtable是通過hash functions實現的,其實hash functions只是用來計算元素位置的函式,STL中定義有現成的hash functions,全都是仿函式。通過呼叫hash functions,取得一個可以對hashtable進行模運算的值。char,int, long等整數型別,hash functions什麼都沒做,返回原值,但對字串(const char*),需要設計轉換函式。因此,stl中的hashtable很多型別是不支援的,比如:string,float,double。要處理這些,就必須自己定義hash functions。
hash_set
hash_set是以hashtable為底層機制,RB-tree有自動排序功能而hash table沒有,因此set的元素有自動排序功能,而hash_set沒有。這是它倆的唯一區別,其他操作是一樣的。
hash_multiset與multiset的操作基本一樣,唯一區別也是無自動排序功能。
hash_map
hash_map底層機制也是hash table,與map的區別也是五排序功能。hash_multimap,亦如此。