索引
這篇文章 http://www.cnblogs.com/daoluanxiaozi/archive/2012/12/02/confidential-stl.html 由於嚴重違反了『博客園首頁』的相關規則,因此筆者改將《私房STL》系列的每一篇都整合到博客園中,取消外鏈的做法。另,考慮篇幅的問題,此系列文章將分為上、中、下。此篇為《數據結構利器之私房STL(上)》,中篇和下篇將陸續發布。喜歡就頂下吧:-)
此系列的文章適合初學有意剖析STL和欲復習STL的同學們。
學過c++的同學相信都有或多或少接觸過STL。STL不僅僅是c++中很好的編程工具(這個詞可能有點歧義,用類庫更恰當),還是學習數據結構的好教材。它實現了包括可邊長數組,鏈表,棧,隊列,散列,映射等等,這些都是計算機專業同學在數據結構這門核心課程當中需要學習的。
在深入一個工具之前,首先要熟練使用它。STL也一樣。在剖析STL之前,可以先動手使用STL,比如其中的vector,list,stack等,熱熱身,而使用比剖析簡單的多,何樂而不為呢。網上很多仁人志士都推薦《C++標准程序庫》,這本書好!但如果是新手,又急於了解如何使用STL,那么我更傾向於選擇一般的c++書籍(里面有簡單的STL使用范例)。另外,還推薦c++ reference站點:http://www.cplusplus.com/,google更不在話下。注意,如果你已經通讀《C++標准程序庫》,那么至多是熟練使用STL而已,但不能說精通STL。欲精通STL,必剖之。
工欲善其事,必先利其器,剖析STL你需要做什么?剖析STL可能需要熟悉c++的基本的語法,了解泛型編程等。最后是《STL源碼剖析》。
此系列的文章無意巨細分析STL內部具體實現,因為互聯網上有很多大牛(@July @MoreWindows 待補充,他們的文章鏈接會在對應的文章中給出)的作品,STL內的一些算法和實現都已經解釋的很詳細了,不再班門弄斧。相反,此系列意在為STL中的每一部件作簡要的總結說明,並穿插其中實現的技巧。
私房STL之vector
一句話vector:vector的空間可擴充,支持隨機存取訪問,是一段連續線性的空間。所以它是動態空間,與之對比的array是靜態的空間,不可擴充。簡單來說,vector是數組的增強版。
vector創建與遍歷
vector提供了幾個版本的構造函數。詳見:http://www.cplusplus.com/reference/stl/vector
比如:
vector<int> iv(3,3); /*3,3,3*/
又或:
...... vector<int>::iterator beg = iv.begin(), end = iv.end(); cout << *beg << endl; ......
vector刪除
在經常需要刪除操作earse()(插入操作也一樣insert())的地方,不建議使用vector容器,因為刪除元素會導致內存的復制,無疑增加系統開銷。最為極端的情況,刪除vector首部的元素:
a b c d e f g h
b c d e f g h h
b c d e f g h
當然,有更好的做法,為了避免內存復制,在刪除的時候,將需要刪除的目標與vector尾端的元素交換,然后才執行刪除操作,但這無疑也增加了一個指向vector尾端元素的空間開銷。
a b c d e f g h
h b c d e f g a
h b c d e f g
vector陷阱
需要注意的是,vector備用空間是有限的,當發現備用空間不夠用的時候,vector是另外新分配一個比原有更大的空間(原有空間*2),然后把原有的內容倒騰到新的空間上去,接着釋放原有的空間。所以迭代器的使用就要特別小心了,在插入元素之后,很可能之前聲明定義的迭代器都失效了。
...... vector<int> iv(3,3); iv.push_back(10); /*3,3,3,10*/ vector<int>::iterator beg = iv.begin(), end = --iv.end(); cout << iv.size() << " " << *beg << " " << *end << endl; /*4 3 10*/ iv.push_back(20); cout << iv.size() << " " << *beg << " " << *end << endl; /*bomb.invalid iterator.*/ ......
vector元素排序
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> iv(3,3); unsigned int i; /*add new elem.*/ iv.push_back(10); iv.push_back(9); iv.push_back(0); vector<int>::iterator beg = iv.begin(), end = iv.end(); /*print.*/ for(i=0; i<iv.size(); i++) cout << iv[i] << " "; cout << endl; /*sort.*/ sort(beg,end); /*print.*/ for(i=0; i<iv.size(); i++) cout << iv[i] << " "; cout << endl; return 0; }
3 3 3 10 9 0
0 3 3 3 9 10
請按任意鍵繼續. . .
vector查找
按上述遍歷元素的方法查找,復雜度為O(n)。STL算法實現了find(),可以在指定的迭代器始末尋找指定的元素。
...... vector<int> iv(3,3); unsigned int i; /*add new elem.*/ iv.push_back(10); iv.push_back(9); iv.push_back(0); vector<int>::iterator beg = iv.begin(), end = iv.end(), ret; ret = find(beg,end,10); cout << *ret << endl; ......
建議
之於array,vector雖略勝一籌,但有它的硬傷,那就是它動態增大的時候,空間操作耗費大,特別是當vector內的元素很多的時候。
vector還提供insert,earse,clear等元素的操作,不一一復述。最后是很不錯的vector文檔:http://www.cplusplus.com/reference/stl/vector/
本文完 2012-10-16
私房STL之list
一句話list:list是我們在數據結構中接觸過的雙向循環鏈表,應用有約瑟夫環;可見其空間非連續的,但可以動態擴充,效率很高,只是不支持隨機訪問,必須通過迭代器找到指定的元素。總的來說,list用起來比較順手。
list的查找
按上述遍歷元素的方法查找,復雜度為O(n)。STL算法實現了find(),可以在指定的迭代器始末尋找指定的元素。
...... list<int> il; il.push_back(5); il.push_back(98); il.push_back(7); il.push_back(20); il.push_back(22); il.push_back(17); list<int>::iterator ite; ite = find(il.begin(),il.end(),20); cout << *ite << endl;/*20*/ ......
list創建與遍歷
STL中也為list實現了幾個版本的構造函數:http://www.cplusplus.com/reference/stl/list/list/,有最簡單缺省的版本。
list的遍歷使用迭代器,如下:
#include <iostream> #include <list> #include <algorithm> using namespace std; int main() { unsigned int i; list<int> il; il.push_back(5); il.push_back(98); il.push_back(7); il.push_back(20); il.push_back(22); il.push_back(17); list<int>::iterator ite; for(ite = il.begin(); ite != il.end(); ite++) cout << *ite << " "; cout << endl; return 0; }
list在空間拓展的時候,沒有經歷vector式的空間倒騰,所以只要不earse元素,指向它ite是不會失效的。
list元素操作
list有提供pop_back,erase,clear,insert等實用的元素操作,不一一復述,給出有用的文檔:http://www.cplusplus.com/reference/stl/list/
list排序
STL算法(<algorithm>)實現的sort只適用於支持隨機訪問的數據,所以它不適用於list,list不支持隨機訪問。所以list內部實現了自己的sort,內部排序使用使用迭代版本的快排。
unsigned int i; list<int> il; il.push_back(5); il.push_back(98); il.push_back(7); il.push_back(20); il.push_back(22); il.push_back(17); list<int>::iterator ite; for(ite = il.begin(); ite != il.end(); ite++) cout << *ite << " "; cout << endl; il.sort(); for(ite = il.begin(); ite != il.end(); ite++) cout << *ite << " "; cout << endl; return 0;
5 98 7 20 22 17
5 7 17 20 22 98
請按任意鍵繼續. . .
建議
list使用輕松自如,硬傷是由於空間的個性(不連續),不能隨機訪問。
本文完 2012-10-16
私房STL之deque
一句話deque:deque是雙端隊列,它的空間構造並非一個vector式的長數組,而是“分段存儲,整體維護”的方式;STL允許在deque中任意位置操作元素(刪除添加)(這超出了deque的概念,最原始的deque將元素操作限定在隊列兩端),允許遍歷,允許隨機訪問(這是假象);我們將看到,deque將是STL中stack和queue的幕后功臣,對deque做適當的修正,便可以實現stack和queue。
deque的迭代器
deque的迭代器與一般的迭代器不同,並不是vector或者list的普通指針式迭代器,有必要寫下。
...... typedef T** map_pointer; T* cur;//指向當前元素 T* first;//指向緩沖區頭 T* last;//指向緩沖區尾巴 map_pointer node;//二級指針,指向緩沖區地址表中的位置 ......
實現的復雜度可見一斑。正是因為deque復雜的空間結構,其迭代器也想跟着復雜晦澀。於是很容易令人產生異或!
為什么要用這么復雜的空間結構
同學A會疑問:“為什么不直接使用似vector抑或array一個長的數組?這樣實現起來簡單,而且迭代器也不會像”這個問題很容易被解決,想想:array就不用解釋了,因為它是靜態的空間,不支持拓展;另外,回想一下,vector在做空間拓展的時候,是如何勞神傷肺?!vector是依從“重新配置,復制,釋放”規則,這樣的代價是很划不來的。所以寧願實現復雜的迭代器,來換取寶貴的計算機資源。
那么deque在做空間拓展的時候是如何做的呢?
如果緩沖區中還有備用的空間,那么直接使用備用的空間;否則,另外配置一個緩沖區,將其信息記錄到緩沖區地址表里;更有甚者,如果緩沖區地址表都不夠的時候,緩沖區地址表也要嚴格依從“重新配置,復制,釋放”規則,但相比對“重新配置,復制,釋放”規則宗教式追狂熱的vector而言,效率高很多。
deque的創建與遍歷
STL中deque有提供多種版本的構造函數,一把使用缺省構造函數。
...... deque<int> id; ......
同樣,雖迭代器龐雜,但使用游刃有余,和其他的容器保持一致;並且,迭代器有重載“[]”運算符,所以支持“隨機訪問”(其實這是假象,詳見上述內容)。
...... deque<int> id; id.push_back(1); id.push_back(2); id.push_back(3); id.push_back(4); id.push_back(5); id.push_back(6); cout << id[2] << endl; /*3*/ ......
deque的查找
有迭代器在,查找可以用STL<algorithm>內部實現的find()。當然,有重載“[]”運算符,按普通的順序查找也可行。這里只給出迭代器版本:
...... deque<int> id; id.push_back(1); id.push_back(2); id.push_back(6); deque<int>::iterator ite; ite = find(id.begin(),id.end(),6); cout << *ite << endl; /*6*/ ......
deque的排序
我們已經知道,deque實際不是連續的存儲空間,它使用了“分段存儲,整體維護”的空間模式,當然代價是龐雜的迭代器。所以STL<algorithm>的sort()函數在這里並不適用。侯傑老師推薦,將deque所有的元素倒騰到一個vector中,再用STL<algorithm>的sort()函數,再從vector中倒騰進deque中。這種折騰是必須的,直接在的deque內部進行排序,效率更低。
建議
deque在實際的應用當中使用的比較少,但正如文章開頭指出的,它是容器stack和queue的幕后功臣,所以了解它的內部實現機制多多益善。
本文完 2012-10-17
私房STL之stack與queue
一句話stack和queue:相對於deque,stack和queue沒有那么底層,他們大部分底層的操作都由deque一手操辦,特別的stack和queue是deque的子集(換句話說,stack、queue管deque叫老爹);通過關閉或者限制deque的一些接口就可以輕易實現stack和queue(STL源碼剖析中管這種機制叫“adapter”。);由stack和queue的定義來看,它們的遍歷動作是不被允許的,沒有迭代器概念;有趣的是,通過修改list的接口,同樣可以讓list假冒stack和queue。
==================
stack的創建與遍歷
除了默認的構造函數,stack和其他很多容器一樣,支持依據vector中元素創建stack。只給出默認版本:更多的資料:http://www.cplusplus.com/reference/stl/stack/stack/
..... stack<int> is; is.push(4); is.push(3); is.push(2); is.push(1); is.push(0); while(!is.empty()) { cout << is.top() << " "; is.pop(); }// while /*0 1 2 3 4*/ .....
stack不允許遍歷!
queue的創建與遍歷
...... queue<int> iq; iq.push(4); iq.push(3); iq.push(2); iq.push(1); iq.push(0); cout << iq.back() << endl; /*0*/ while(!iq.empty()) { cout << iq.front() << " "; iq.pop(); }// while /*4 3 2 1 0*/ ......
queue不允許遍歷!
stack/queue的查找和排序
stack/queue不允許遍歷!
關於stack的top()和pop()
在數據結構的課程中,習慣將上面兩個功能都整合到pop中去,但STL分開了,一個函數只做一件事情,在queue中也是這樣做的。
...... Sequence c; // 底層容器 ...... reference top() { return c.back(); } void pop() { c.pop_back(); } ......
從Sequence c的定義當中可以看出一些端倪,stack允許用戶選定底層容器,所以list此時可以作為底層容器來實現stack/queue。
...... stack<int,list<int>> is; is.push(4); is.push(3); is.push(2); is.push(1); is.push(0); while(!is.empty()) { cout << is.top() << " "; is.pop(); }// while /*0 1 2 3 4*/ ......
建議
stack/queue在實際應用用的比較多,兩者有很大的共性,因此queue被提取出來。嘿嘿,突然對STL肅然起敬。
關於更多的stack和queue請參看:http://www.cplusplus.com/reference/stl/stack/和http://www.cplusplus.com/reference/stl/queue/
本文完 2012-10-19
私房STL之一分鍾的heap
一句話的heap:一種數據結構,完全二叉樹(若二叉樹高h,除過最底層h層,其他層1~h-1都是滿的;並且最底層從左到右不能有空隙。),但在實現上,它沒有選擇一般的二叉樹數據結構(即一個節點包含指向兩個孩子的指針),使用的是數組;heap最為常用的操作是上溯和下溯,它們在“維持堆”和“堆排序”中經常用到。這篇文章能讓你快速回顧heap。
===============================================
如果某節點位於數組i處,那么那么2i即為其左子結點,2i+1即為其右子結點。
最大堆和最小堆
堆有有最大堆和最小堆兩種。最大堆即根節點的鍵值比其他所有節點鍵值都大;最小堆即根節點的鍵值比其他所有節點鍵值都小。只討論最大堆,最小堆和最大堆思路如出一轍,便不一一復述了。
上溯和下塑
上溯操作主要用在“push_heap”過程中維持堆性質;下塑操作經常用在“sort_heap”過程中維持堆性質。
上溯:某節點與父節點比較,如果其鍵值比父節點大,即交換父子節點。重復上述操作,直到不需要交換或者到達根節點為止。
下塑:此節點為與堆頂,拿其與min(左子結點鍵值,右子結點鍵值)比較,如果父節點鍵值小過min,即交換父子節點。重復上述操作,直到不需要交換為止。
堆的形成
任務:給定一個數組,將其轉換為最大heap。STL中make_heap()函數可以完成,它的思路:從最底層開始維持每一個子堆。看圖:
還有一種可行的思路,即:先假設堆中的元素個數為0,然后向(尾端+1)(意即尾端后的一個位置)push一個新的元素,然后在這個位置執行上溯操作。重復上述操作,直至數組內所有的元素都push完為止。我們發現這個方法也是可行的。
堆排序
任務:給定一個最大heap,實現數組排序。思路不拐彎抹角,很直接:因為堆頂對應最大的元素swap(堆頂節點,最大heap最右一個節點);不處理最后一個節點,從堆頂下溯。注意,下溯操作過后,除過最后一個節點,現有數據仍為一個最大堆。
堆排序的算法復雜度可以達到O(NlnN),在“排序算法家族”當中效率還是很靠前的。關於heap的算法都在STL<algorithm>中實現,STL只實現了最大堆。
...... vector<int> iv(a,a+7); unsigned int i; vector<int>::iterator beg = iv.begin(), end = iv.end(),ite; for(ite = beg; ite!=end; ite++) cout << *ite << " "; cout << endl; /*1 3 9 11 21 100 4*/ make_heap(beg,end); for(ite = beg; ite!=end; ite++) cout << *ite << " "; cout << endl; /*100 21 9 11 3 1 4*/ sort_heap(beg,end); for(ite = beg; ite!=end; ite++) cout << *ite << " "; cout << endl; /*1 3 4 9 11 21 100*/ ......
max-heap實現priority_queue
priority_queue帶權值的queue,順序入隊之后,按照權值的大小出隊。max-heap正好可以滿足這個需求,max-heap的堆頂元素總是最大的。priority_queue在實現上已vector為底層容器,這與queue相差很大。
template<class _Ty, class _Container = vector<_Ty>, class _Pr = less<typename _Container::value_type> > class priority_queue {......}
本文完 2012-10-19