索引

  • 数据结构利器之私房STL(上)
  • 数据结构利器之私房STL(中)
  • 数据结构利器之私房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 valuestypedef 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 mappediterator _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_permutation和prev_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;}//   iffor(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_permutation和prev_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;}// whilereturn 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/

数据结构利器之私房STL(中)相关推荐

  1. 数据结构利器之私房STL(上)

    <数据结构利器之私房STL(上)>,作者:捣乱小子,原文链接:http://www.cnblogs.com/daoluanxiaozi/archive/2012/12/02/2798235 ...

  2. 数据结构利器之私房STL

    此系列的文章适合初学有意剖析STL和欲复习STL的同学们.都是原创! 学过c++的同学相信都有或多或少接触过STL.STL不仅仅是c++中很好的编程工具(这个词可能有点歧义,用类库更恰当),还是学习数 ...

  3. python 3.8.2 / 内置的数据结构 / list (类似于 STL 中的 vector)

    一.特点 (1)相对于 tuple 来说,list 是动态的(mutable),即:各个元素都是可变的. (2)可以通过索引进行查询. (3)list 中的元素可以是 python 中的任何对象.例如 ...

  4. 算法求解中的变量、数组与数据结构(STL 中的容器)

    本质上算法都是对数据的操作,没有数据,没有存储数据的容器和组织方式,算法就是无源之水无本之木,就是巧妇也难为无米之炊.算法是演员,变量.数组.容器等就是舞台, 然后整个算法的处理流程,都是针对这些数据 ...

  5. 关于STL中的map和hash_map

    在网上看到有关STL中hash_map的文章,以及一些其他关于STL map和hash_map的资料,总结笔记如下:     1.STL的map底层是用红黑树实现的,查找时间复杂度是log(n):   ...

  6. STL中的set容器的一点总结

    1.关于set C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构 ...

  7. STL中迭代器的作用,有指针为何还要迭代器

    请你来说一下STL中迭代器的作用,有指针为何还要迭代器 参考回答: 1.迭代器 Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴 ...

  8. c++ map是有序还是无序的_C++ STL中Map的按Key排序和按Value排序

    map是用来存放键值对的数据结构,可以很方便快速的根据key查到相应的value.假如存储学生和其成绩(假定不存在重名,当然可以对重名加以区分),我们用map来进行存储就是个不错的选择. 我们这样定义 ...

  9. C++ STL中哈希表 hash_map介绍

    0 为什么需要hash_map 用过map吧?map提供一个很常用的功能,那就是提供key-value的存储和查找功能.例如,我要记录一个人名和相应的存储,而且随时增加,要快速查找和修改: 岳不群-华 ...

  10. C++ STL : 模拟实现STL中的关联式容器unordered_map/unordered_set

    目录 unordered_map/unordered_set unordered_map/unordered_set与map/set的区别 底层哈希桶的改造 仿函数 Key值的获取方法 hash(ke ...

最新文章

  1. c#对象集合去重_C# List集合去重操作注意点
  2. python监控单台多实例数据库服务器的数据库端口
  3. 1.8 深入解析new运算符
  4. javaweb(三十八)——mysql事务和锁InnoDB(扩展)
  5. linux四种集群是什么,lvs四种集群特点及使用场景
  6. 前端学习(2887):如何短时间内实现v-for proxy代理
  7. Python高级——HTTP协议
  8. c 与mysql连接_c与mysql的连接
  9. 计算机网络技术论文致谢,路由器论文致谢
  10. c语言求正弦余弦正切,公式( 正弦 余弦 正切 余切 正割 余割 )
  11. C语言程序——计算圆的周长、圆的面积、球的体积
  12. Ford-Fulkerson方法
  13. 计算机网络常见缩略语
  14. 色彩校正(CCM)和伽马校正(Gamma)
  15. QGIS 加载XYZ Tiles
  16. php正则表达式懒惰匹配,正则表达式-贪婪与懒惰
  17. HDU3085 Nightmare Ⅱ
  18. 百战RHCE(第十五战:Linux进阶命令十二-主机名和域名解析极简管理)
  19. Linux学习4 yum仓库 编译安装 sed基本用法
  20. jade选峰之后怎么去掉_jade怎么把峰标出来

热门文章

  1. LDO与DC/DC差别
  2. h5 右下角浮动按钮_Flutter 浮动按钮-FloatingActionButton的使用
  3. c# printDialog不显示问题
  4. 数据库~Mysql里的Explain说明
  5. 斯诺登:澳大利亚的监视政策比NSA还下流
  6. iconv 中文截断问题的解决方法
  7. 网站目录提交-SEO搜索引擎优化
  8. 分享安卓SD卡的后台设置
  9. html文档utf8文档字节,HTML UTF-8 参考手册
  10. oracle修改只读数据库中,如何在oracle中创建只读数据库链接