數據結構利器之私房STL(上)


索引

 


這篇文章 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中的每一部件作簡要的總結說明,並穿插其中實現的技巧。

  1. 私房STL之vector
  2. 私房STL之list
  3. 私房STL之deque
  4. 私房STL之stack與queue
  5. 私房STL之一分鍾的heap

 


私房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.*/
......
bomb!!!

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

搗亂小子 http://www.daoluan.net/


私房STL之list

一句話list:list是我們在數據結構中接觸過的雙向循環鏈表,應用有約瑟夫環;可見其空間非連續的,但可以動態擴充,效率很高,只是不支持隨機訪問,必須通過迭代器找到指定的元素。總的來說,list用起來比較順手。

list_node

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

搗亂小子 http://www.daoluan.net/


私房STL之deque

一句話deque:deque是雙端隊列,它的空間構造並非一個vector式的長數組,而是“分段存儲,整體維護”的方式;STL允許在deque中任意位置操作元素(刪除添加)(這超出了deque的概念,最原始的deque將元素操作限定在隊列兩端),允許遍歷,允許隨機訪問(這是假象);我們將看到,deque將是STL中stack和queue的幕后功臣,對deque做適當的修正,便可以實現stack和queue。

bug,deque_in_real

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

搗亂小子 http://www.daoluan.net/


私房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

==================

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

搗亂小子 http://www.daoluan.net/


私房STL之一分鍾的heap

一句話的heap:一種數據結構,完全二叉樹(若二叉樹高h,除過最底層h層,其他層1~h-1都是滿的;並且最底層從左到右不能有空隙。),但在實現上,它沒有選擇一般的二叉樹數據結構(即一個節點包含指向兩個孩子的指針),使用的是數組;heap最為常用的操作是上溯和下溯,它們在“維持堆”和“堆排序”中經常用到。這篇文章能讓你快速回顧heap。

 

完全二叉樹(左)和非完全二叉樹(右)

===============================================

完全二叉樹的數組存儲(對應上圖左),X是實現上的技巧,刻意空出來

 

如果某節點位於數組i處,那么那么2i即為其左子結點,2i+1即為其右子結點。

最大堆和最小堆

堆有有最大堆和最小堆兩種。最大堆即根節點的鍵值比其他所有節點鍵值都大;最小堆即根節點的鍵值比其他所有節點鍵值都小。只討論最大堆,最小堆和最大堆思路如出一轍,便不一一復述了。

上溯和下塑

上溯操作主要用在“push_heap”過程中維持堆性質;下塑操作經常用在“sort_heap”過程中維持堆性質。

上溯:某節點與父節點比較,如果其鍵值比父節點大,即交換父子節點。重復上述操作,直到不需要交換或者到達根節點為止。

上溯

下塑:此節點為與堆頂,拿其與min(左子結點鍵值,右子結點鍵值)比較,如果父節點鍵值小過min,即交換父子節點。重復上述操作,直到不需要交換為止。

下溯

堆的形成

任務:給定一個數組,將其轉換為最大heap。STL中make_heap()函數可以完成,它的思路:從最底層開始維持每一個子堆。看圖:

 

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

搗亂小子 http://www.daoluan.net/


免責聲明!

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



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