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


索引

 


這篇文章 http://www.cnblogs.com/daoluanxiaozi/archive/2012/12/02/confidential-stl.html 由於嚴重違反了『博客園首頁』的相關規則,因此筆者改將《私房STL》系列的每一篇都整合到博客園中,取消外鏈的做法。另,考慮篇幅的問題,此系列文章將分為上、中、下。此篇為《數據結構利器之私房STL(中)》。喜歡就頂下:-)

此系列的文章適合初學有意剖析STL和欲復習STL的同學們。

  1. 私房STL之map和set
  2. 私房STL之Hashtable
  3. 私房STL算法之全排列
  4. 私房STL算法之快速冪
  5. 私房STL之hash_set和hash_map

 


私房STL之map和set

一句話set:容器set底層是由RB_TREE實現的,它和(deque--->stack、queue)模式一樣;色set的元素不允許重復;set中鍵值就是實值,實值就是鍵值,而鍵值是不可以更改的(但MS STL不這樣做),所以set不允許對其中的元素進行更新;

一句話map:容器map底層也由RB_TREE實現,它和(deque--->stack、queue)模式一樣;map中一個鍵值對應一個實值,不允許鍵值上的重復,內部是按鍵值來進行排序存儲的,其中鍵值不允許被更改。

紅黑樹

網絡上有關於紅黑樹詳細的解說,特別是復雜的紅黑樹元素操作算法,推薦@JULY 的文章:http://blog.csdn.net/v_JULY_v/article/details/6105630。本文主要介紹STL set/map的用法和筆者對其在實現上技巧實現技法的摘錄,歡迎斧正。

set和map的創建與遍歷

set和map都是由非線性空間來存儲的,屬於Bidrectional Iterator;測試添加元素的時候,故意添加已存在的鍵值,發現被拒絕,RB_TREE內部有insert_equal()和insert_unique()兩個版本,set/map都調用后者。

set:

......
set<int> is;

is.insert(7);
is.insert(5);
is.insert(6);
is.insert(4);
is.insert(3);

is.insert(3);	/*here.*/
set<int>::iterator beg = is.begin(),
	end = is.end(),ite;

for(ite = beg; ite != end; ite++)
	cout << *ite << " ";
cout << endl;/*3 4 5 6 7*/
......

map:

map<int,int> im;

im.insert(pair<int,int>(7,700));
im.insert(pair<int,int>(5,500));
im.insert(pair<int,int>(6,600));
im.insert(pair<int,int>(4,400));
im.insert(pair<int,int>(3,300));

im.insert(pair<int,int>(3,300));	/*here.*/

map<int,int>::iterator beg = im.begin(),
	end = im.end(),ite;

for(ite = beg; ite != end; ite++)
	cout << "<" <<ite->first << " " << ite->second << ">" << " ";
cout << endl;	/*<3 300> <4 400> <5 500> <6 600> <7 700>*/

set和map的查找

RB_TREE本身就是一個搜索樹,加之它能時刻保持良好的平衡,所以查找效率高。set和map內部已經實現了find()查找。而STL <algorithm>find()效率低很多。

有趣的實現

在insert()函數中,會經常用到pair這個結構體,里頭有兩個元素:第一元素被作為鍵值,第二元素被作為實值。

/*摘自MS STL。*/

		// TEMPLATE STRUCT pair
template<class _Ty1,
	class _Ty2> struct pair
	{	// store a pair of values
	typedef pair<_Ty1, _Ty2> _Myt;
	typedef _Ty1 first_type;
	typedef _Ty2 second_type;

	pair()
		: first(_Ty1()), second(_Ty2())
		{	// construct from defaults
		}
	......
	_Ty1 first;	// the first stored value
	_Ty2 second;	// the second stored value
	};

有趣的地方是,它不僅僅用在insert()的參數中,還應用在insert()的返回值和map的“[]”運算符重載中。

typedef pair<iterator, bool> _Pairib;
......
_Pairib insert(const value_type& _Val);

所以在insert()過后,如果插入成功,_Pairib的iterator會指向元素插入的位置,bool被置為true;否則,iterator指向重復的元素的位置,且bool為false.所以,“[]”重載函數可以通過insert()間接實現的。但在MS STL中,它沒有采用這種方法,其內部雖也通過insert()間接實現,但其采用以map<T1,T2>::iterator為返回值的insert()版本。

mapped_type& operator[](const key_type& _Keyval)
	{	// find element matching _Keyval or insert with default mapped
	iterator _Where = this->lower_bound(_Keyval);
	if (_Where == this->end()
		|| this->comp(_Keyval, this->_Key(_Where._Mynode())))
		_Where = this->insert(_Where,
			value_type(_Keyval, mapped_type()));
	return ((*_Where).second);
	}

一個關於set的疑問

set中鍵值就是實值,實值就是鍵值。既然這樣,set中的元素就不允許被修改,一個測試:實踐證明,set中的元素允許被改變,改變后,set內部對其視若無睹。

set<int> is;

is.insert(7);
is.insert(5);
is.insert(6);
is.insert(4);
is.insert(3);

is.insert(3);

/*3 4 5 6 7*/

set<int>::iterator beg = is.begin(),
	end = is.end(),ite;

*beg = 8;	

for(ite = beg; ite != end; ite++)
	cout << *ite << " ";
cout << endl;	/*8 4 5 6 7*//*居然可以修改,嚇shi了*/

is.insert(100);

for(ite = beg; ite != end; ite++)
	cout << *ite << " ";
cout << endl;	/*8 4 5 6 7 100*//*居然也不維護一下,嚇shi了*/

原來是set的iterator迭代器被聲明為一般的iterator,而不是const。

/*摘自MS STL。*/
typedef typename _Mybase::iterator iterator;

不僅如此,在set的模板聲明中也可以看出端倪:

/*摘自MS STL。*/
template<class _Kty,
	class _Pr = less<_Kty>,
	class _Alloc = allocator<_Kty> >
	class set
		: public _Tree<_Tset_traits<_Kty, _Pr, _Alloc, false> >
	{	// ordered red-black tree of key values, unique keys

所以如果需要禁止用戶通過迭代器修改鍵值,那么可以將迭代器聲明為const:(筆者認為這樣可行的)

typedef typename _Mybase::const_iterator iterator;

而map把關得很好,它強行將pair中的第一元素(注意,只是第一元素而已)定義為const:

/*摘自MS STL。*/
template<class _Kty,
	class _Ty,
	class _Pr = less<_Kty>,
	class _Alloc = allocator<pair<const _Kty, _Ty> > >
	class map
		: public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false> >
	{	// ordered red-black tree of {key, mapped} values, unique keys
......

本文的后部分需要對STL源碼有一定的了解。本文有待補充。

本文完 2012-10-16

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


私房STL之Hashtable

一句話之Hashtable:哈希表(散列表)能通過鍵值對數據進行訪問的數據結構;其在C++0X標准中未出現,可能是考慮到哈希表效率低下,出於其廣泛用於工程中,C++11將其納入了標准庫。C++11的新特性:http://en.wikipedia.org/wiki/C%2B%2B11,C++11中哈希表的說明:http://en.wikipedia.org/wiki/C%2B%2B11#Hash_tables;我們知道,通過哈希表來索引目標是很高效的,但這樣會出現碰撞問題(即對不同的關鍵字可能得到同一哈希地址)。常用的解決碰撞的方法有四:線性探測、二次探測、再散列和開鏈法。而STL中的哈希表所采用的是開鏈法(也叫鏈地址法)。

哈希表

Hashtable的查找,插入,刪除

在通過給定的鍵值計算出元素在Hashtable中的位置(O(1)就可以完成)時,行為就與單鏈表一樣了,查找,插入和刪除操作的平均開銷都為O(N/2)。

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

剩下的內容沒有對哈希表模擬實驗之類的內容(互聯網有很多作者給出了很詳細的分析,推薦一個:http://blog.csdn.net/morewindows/article/details/7330323),只描述解決碰撞的方法和哈希表的效率問題。

哈希表碰撞問題

這樣假設,哈希表大小為N,哈希函數為Hash(elem),計算哈希表地址時,取模N,意即:elem在哈哈希表地址是Hash(elem) % N。

線性探測的做法:計算哈哈希表地址得出Hash(elem) % N,如果此地址未被占用,那么插入;否則,探測(Hash(elem) + 1)  % N 是否占用,如果未被占用,插入。否則繼續探測下去。

二次探測的做法:同線性探測,計算哈希表地址得出Hash(elem) % N,如果此地址未被占用,那么插入;否則,探測(Hash(elem) + 1^2)  % N,如果未被占用,插入。否則繼續探(Hash(elem) + 2^2)  % N。。。

再散列法:存在K個不同的哈希函數Hi = Hashi(elem) % M,k = 0,1,2,k-1。倘若第1個哈希函數不行,采用第2個,從而減少碰撞。

開鏈法的做法:屬於(vector + single list)的模式,計算哈希表地址得出Hash(elem) % N,插入對應的單鏈表。

哈希表的效率

線性探測,1、需要表有足夠大連續的空間,否則元素太多,就需要resize,效率不可觀;2、在進行探測的空閑地址的時候,最壞的情況探測整個表,平均情況是整個表的一半,不可取。

二次探測,1、它同樣需要有足夠大的連續的空間;2、對線性探測的一種改進的地方,便是平方(二次方)探測,意即步長不再是n,而為n^2,這樣能減少碰撞。

再散列法:1、它同樣需要有足夠大的連續的空間;2、增加計算量。

前三種都未能很好解決碰撞問題。

開鏈法,動態非連續空間(single list),不存在線性探測和二次探測的第一個問題;在確定地址過后,只需要對相應的single list作插入,刪除,修改操作,這樣碰撞的問題就轉化為single list的尋訪,速度可觀。STL Hashtable就是采用開鏈法。

鏈地址法

后來我們將看到,STL中的hash_set和hash_map皆由Hashtable作為底層容器。

哈希表的應用

在數據結構的課堂便有這樣的實驗:統計文本單詞出現的頻率。我們可以創建單詞哈希表,Hasn(word)定義為word中每個字符的ASCII碼之和,通過它來確定單詞在哈希表地址,進而進行統計。

另外,初學程序設計的同學都有設計學生管理系統的經歷,現有需求“以學生姓名為關鍵字,如何建立查找表,使得根據姓名可以直接找到相應記錄呢?”,這也是哈希表的一個應用。

本文完 2012-10-21

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


私房STL算法之全排列

全排列問題:從n個不同元素中任取m(m≤n)個元素,按照一定的順序排列起來,叫做從n個不同元素中取出m個元素的一個排列。當m=n時所有的排列情況叫全排列。譬如,考慮{a,b,c}的全排列有abc,acb,bac,bca,cab,cba六(3!)種情況。

首先要聲明,STL沒有實現全排列的函數,但描述了全排列的核心算法,分別是next_permutationprev_permutation,兩者實際上一樣,只不過情況不同。全排列實現可以是遞歸和迭代兩個版本。STL算法中的next_permutation便也是全排列算法迭代版本的核心。

遞歸實現全排列

遞歸實現全排列是一個經典的算法。

/*全排列遞歸版本*/
void foo1(char *str,int k,int n)
{
	if(k == n)	//	print str when it reaches the last character.
	{
		cout << str << " ";
		return;
	}//	if

	for(int i=k; i<n; i++)
	{
		swap(str[k],str[i]);
		foo1(str,k+1,n);	//	next character.
		swap(str[k],str[i]);
	}//	for
}

abcd abdc acbd acdb adcb adbc bacd badc bcad bcda bdca bdac cbad cbda cabd cadb
cdab cdba dbca dbac dcba dcab dacb dabc 請按任意鍵繼續. . .

迭代實現全排列

因為next_permutationprev_permutation實際上換湯不換葯,因此只描述next_permutation算法。在下筆之前,next_permutation()函數的作用是取下一個排列組合。同樣,考慮{a,b,c}的全排列:abc,acb,bac,bca,cab,cba,以“bac”作為參考,那么next_permutation()所得到的下一個排列組合是bca,prev_permutation()所得到的前一個排列組合是“acb”,之於“前一個”和“后一個”,是按字典進行排序的。

next_permutation()算法描述:

  1. 從str的尾端開始逆着尋找相鄰的元素,*i和*ii,滿足*i<*ii;
  2. 接着,又從str的尾端開始逆着尋找一元素,*j,滿足*i<*j(*i從步驟一中得到);
  3. swap(*i,*j);
  4. 將*ii之后(包括*ii)的所有元素逆轉。

舉個例子,需要找到“01324”的下一個排列,找到*i=2,*ii=4,*j=4,下一個排列即“01342”。再來找到“abfedc”的下一個排列,找到*i=b,*ii=f,*j=c,swap操作過后為“acfedb”,逆轉操作過后為“acbdef”。

/*全排列迭代歸版本*/
void reverse(char *str)
{
	int len = strlen(str),i;
	for(i=0; i<len/2; i++)
		swap(*(str+i),*(str+len-1-i));
}

/*階乘*/
int factorial(int n)
{
	if(n == 1)	return 1;
	return n * factorial(n-1);
}

void foo2(char *p)
{
	int len = strlen(p),cnt = 1;
	char *i,*ii,*j;

	cout << p << " ";

	/*STL <algorithm> next_permutation()函數的核心算法*/
	while(++cnt <= factorial(len))
	{
		i = p + len - 2,ii = p + len - 1,j = ii;
		while(*i >= *ii)	i--,ii--;	/*find *i and *ii.*/
		while(*i >= *j)	j--;			/*find *j.*/

		swap(*i,*j);		/*swap.*/

		reverse(ii);		/*reverse.*/
		cout << p << " ";
	}//	while
}

abcd abdc acbd acdb adbc adcb bacd badc bcad bcda bdac bdca cabd cadb cbad cbda
cdab cdba dabc dacb dbac dbca dcab dcba 請按任意鍵繼續. . .

prev_permutation()函數做法是一樣的。

本文完 2012-10-22

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


私房STL算法之快速冪

STL中的pow用來計算某數的n冪次方。

冪運算中,如AK,需要作k次乘法,可以試着用二分法減少乘法的次數,乘法因為機器性能的不同所占的時鍾周期數有10~40不等,所以降低乘法的次數,等於是節省CPU的資源,雖然在大多數情況這些無足輕重。

計算A23,可以依次計算A1b,A10b,A100b,A101b,A1010b,A1011b,A10110b,A10111b得到結果。在計算過程中,刻意將指數轉化為二進制的形式,以更好理解二分法快速冪。這里可以呈現的是快速冪的遞歸算法,發現:

當指數n為偶數時,A^n = A^(n/2) * A^(n/2);

當指數n為奇數時,A^n = A^(n/2) * A^(n/2) * A 。

從上面的例子中,即10111b為奇數,A10111b=(A10110b)* A;10110b為偶數,A10110b=(A1011b),依次類推。。。。

typedef unsigned int UINT;
UINT power(UINT A,UINT n)
{
	if(n == 1)		
		return A;

	UINT tmp = power(A,n>>1);	/*calculate pow(A,n/2).*/
	return (n & 1) 
		? tmp * tmp * A			/*odd.*/
		:tmp * tmp;				/*even.*/
}

上面是遞歸的思路,迭代的也一樣,同樣可以舉一個翔實的例子。計算A23,23用二進制展開:

23 = 1 * 24 + 0 * 23 + 1 * 22 + 1 * 21 + 1 * 20,

迭代從低位開始,第k位為0,即不操作;第k位為1,tmp *2k-1

UINT power(UINT A,UINT n)
{
	UINT tmp = 1,base = 2;
	while(n)
	{
		if(n&1)			/*低位為1*/
			tmp *= base;
		n >>= 1;		/*右移*/
		base <<= 1;
	}//	while
	return tmp;
}

把原來O(N)降低為O(lnN),很划得來。歡迎斧正。

本文完 2012-10-22

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


私房STL之hash_set和hash_map

一句話hash_set和hash_map:它們皆由Hashtable(Standard C++ Library未公開,只作為底層部件)作為底層容器, 所有的操作也都由Hashtable提供;咋看起來,好似與set和map有很大的關聯,其實不大,只不過hash_set和hash_map有着“set鍵值就是實值,實值就是鍵值,map鍵值就是鍵值,實值就是實值”特征,姑且讓set和map掛掛名:-);

由此,hash_set內部元素也是未經排序的(從Hashtable的實現可知),而hash_map可以經由鍵值索引其對應實值(其重載了“[]”操作符);由Hashtable的底層實現可知:hash_set和hash_map的查找效率和插入操作的平均時間開銷都為O(N/2)。

hash_set和hash_map的創建與遍歷

hash_set只需指定鍵值的類型,hash_map需指定鍵值和實值的類型。它們都可以像大多數的容器一樣,通過迭代器,尋訪元素。

......
hash_set<int> ihs; 

ihs.insert(1);
ihs.insert(5);
ihs.insert(6);
ihs.insert(4);
ihs.insert(3);
ihs.insert(3);
ihs.insert(100);

ihs.insert(200);		/*故意的*/

hash_set<int>::iterator beg = ihs.begin(),
	end = ihs.end(),ite;

for(ite = beg; ite != end; ite++)
	cout << *ite << " ";
cout << endl;
......

200 1 3 4 100 5 6

可證見hash_set拒絕插入重復元素(與set性質相同),未排序(違反set性質)。

......
hash_map<int,int> ihm;

ihm.insert(pair<int,int>(1,100));
ihm.insert(pair<int,int>(2,200));
ihm.insert(pair<int,int>(3,300));
ihm.insert(pair<int,int>(4,400));
ihm.insert(pair<int,int>(5,500));

hash_map<int,int>::iterator beg = ihm.begin(),
	end = ihm.end(),ite;

for(ite = beg; ite != end; ite++)
	cout << "<" << ite->first << "," << ite->second << ">" << " ";
cout << endl;

cout << "ihm[1] = " << ihm[1] << endl;		/*可以通過鍵值索引*/
......

<1,100> <2,200> <3,300> <4,400> <5,500>
ihm[1] = 100

hash_set和hash_map的查找

有Hashtable的實現可知,hash_set和hash_map的平均查找效率一樣很高,各自內部有實現find()查找函數,無需使用從頭至尾遍歷的STL <algorithm>find()函數。Standard C++ Library中的實例:http://msdn.microsoft.com/en-US/library/ea54hzhb(v=vs.80).aspx

建議

hash_set和hash_map還實現很多函數,給出參考鏈接:http://msdn.microsoft.com/en-US/library/y49kh4ha(v=vs.80).aspx

外鏈 @MoreWindows 同學的文章:http://blog.csdn.net/morewindows/article/details/7330323,里頭的亮點便是C++里頭語法的細節問題。

本文完 2012-10-23

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


免責聲明!

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



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