C++ STL之container

1. Container 容器

1.1 容器之间的实现关系与分类

  1. 容器之间的关系并非继承而是复合,例如,set是内部维护一个rb_tree来实现的
    在这里插入图片描述

1.2 list

![在这里插入图片描述](https://img-blog.csdnimg.cn/3e486f2cb848426ca8c2c50ecf173f9c.png)

  1. 如图所示,在GC2.9中list是一个环状链表,其中有一个成员变量,是指向list_node的指针,所以sizeof(list)=一个指针的大小;
  2. list_node中包含两个指针和一个值,指针指向前后节点,所以list申请内存的时候每个节点要耗用两个指针+一个值的内存
  3. list中的迭代器__list_iterator, 里面会有大量的操作符重载,要模拟指针,指向node,支持各类操作符。
    在这里插入图片描述
  4. 如下图所示,在GC4.9中改善了list的实现代码,减少了_List_iterator的模板参数,以及更改了list_node中前后指针的写法。GC2.9中是一个指针指向头节点,所以sizeof(list)的大小是一个指针的大小,而4.9中,list中有两个指针,分别是_M_next_M_pre,sizeof(list)是两个指针的大小,不再是一个指针。
    在这里插入图片描述
    在这里插入图片描述
  5. 单向链表forwward_list实现与list大同小异
    在这里插入图片描述

1.3 vector

  1. vector是两倍增长的,GC2.9中vector维护三个指针,start指向开头,finish指向存储的最后一个数据的后一个位置,end_of_storage指向最多存储的位置的下一位。所以sizeof(vector)是三个指针的大小。
    在这里插入图片描述
  2. 当vector已满时,push_back操作会使vector两倍增长,push_back调用insert_aux,insert_aux判断vector是否已经没有可用空间,如果没有则开辟一块两倍大小的内存,将vector的原来的数据拷贝到新的内存中,并将要插入的数据放到内存中,然后拷贝安插点后面到finish的内容(最后这一步是insert()函数会用到,push_back不会指向,因为finish和安插点重合)
    在这里插入图片描述
    在这里插入图片描述
  3. vector的iterator在2.9版中是指针类型,在4.9版中是类,所以在2.9版中走的是iterator_traits的指针偏特化的版本,在4.9版本中,走的是泛化版本,本质是一样的。
    在这里插入图片描述
    在这里插入图片描述

1.4 array

  1. array允许用户定义一个size为0的数组,当指定的_Nm为0使,返回的valu_type是size为1的数组类型;array没有构造器和析构器。
    在这里插入图片描述
    在这里插入图片描述

1.5 deque、queue以及stack

1.5.1 deque
  1. deque逻辑上是连续空间,但实际上是分段连续的,而非完全连续。与vector 相似,deque也采用dynamic array来管理元素,提供随机访问,并有着和 vector 几乎一模一样的接口。不同的是 deque 的 dynamic array 头尾都开放,因此能在头尾两端进行快速安插和删除。

  2. deque是由一段一段的定量连续空间构成,一旦有必要在deque的前段或尾端增加新空间,便配置一段定量连续空间,然后串接在整个deque的头端或者尾端。因此,deque最大的任务就是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的借口,避开了“重新配置,复制,释放”的轮回,代价则是复杂的迭代器架构。

  3. deque采用一块所谓的map(不是STL的map容器,反而更像是vector)作为主控,这里所谓的map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向一段(较大的)连续空间,称为缓冲区。缓冲区才是deque的储存空间主体,SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。
    在这里插入图片描述

  4. start 指向头的迭代器(M_start),finish 指向尾部的下一个位置的迭代器(_M_finish),迭代器类里有四个成员变量

    • cur 指向当前元素
    • first 指向当前buffer内的第一个元素
    • last 指向当前buffer的最后一个元素
    • node 指向当前buffer的指针
      在这里插入图片描述
  5. deque的insert函数:
    在这里插入图片描述
    在这里插入图片描述

    • 如果安插点是start位置,则直接push_front
    • 如果安插点是finish位置,则直接push_back
    • 如果以上两个条件都不满足,说明不是特殊位置,则要判断pos是考前的位置还是靠后的位置
    • pos < size() / 2,位置靠前,让前面的元素前移再插入到对应位置
    • pos > size() / 2,位置靠后,让后面的元素后移再插入到对应位置
  6. deque如何模拟连续空间?

    • 重载operator[],operator+=,operator-=,operator++,operator++(int),operator–,operator–(int)

    • reference operator[](size_type _n) {
          return *(this + _n); //这里的+是重载后的
      } 
      
    • reference front() {
          return *start; //这里的+是重载后的
      }
      
    • reference back() {
      	iterator tmp = finsh;
          //注意,finish指向的是下一个可以存放数据的点,所以要先--
      	--tmp;
          return *tmp; //这里的+是重载后的
      }
      
    • size_type size()const {
          return finsh - start;//重载后的-
      }
      
    • difference_type operator-(const self& x)const {
          return difference_type(buffer_size()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur); //算start和finish之间的buffer个数再乘长度,然后加上start和finish中占用的存储;
      }
      
    • reference operator--() {
          //判断当前是否已到当前buffer的first,如果到了就先将node-1,移动到上一个buffer;
          if(cur == first) {
              set_node(node-1);
          }
          --cur;
          return *this
      }
      
    • reference operator++() {
          ++cur;
          2.判断是否已到当前buffer的last,如果到了就将node+1,移动到下一个buffer;
          if(cur == last) {
              set_node(node + 1);
              cur = first;
          }
          return *this;
      }
      
    • reference operator+=(difference_type n) {
          //1.判断+n之后是否还在本buffer中,如果在则直接加
          difference_type offset = n + (cur - first);
          if(offset >= 0 && offset < difference_type(buffer_size())) {
              cur += n;
          }esle {
              //2.如果+n之后不在本buffer,先找到对应的缓存区,然后移动剩余的位置
              difference_type node_offset = 
                  offset > 0 ? offset / difference_type(buffer_size())
                  				 : -difference_type((-offset-1)/buffer_size());
              //切换至正确的缓冲区
              set_node(node + node_offset);
              cur = first + (offset - node_offset * difference_type(buffer_size()));
          }
          return *this;
      }
      
    • reference operator-=(difference_type n) {
          return *this += -n;
      }
      
1.5.2 queue
  1. queue可以由deque构造而来,queue先入先出,不允许遍历,没有迭代器
    在这里插入图片描述

  2. queue也可以由deque、list作为底层来实现,不可以由vector、set、map作为底层实现

1.5.3 stack
  1. stack可以由deque构造而来,stack先入后出,不允许遍历,没有迭代器
    在这里插入图片描述

  2. stack也可以由deque、list、vector作为底层来实现,但不可以由set、map作为底层实现

1.6 RB_Tree

  1. 红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。

  2. 红黑树的遍历规则是中序遍历,由于红黑数有排序规则,所以不应该使用itreator来改变元素值(编程上没有禁止这种操作,因为要为map和set提供底层支持,map允许改变data,key不能被改变,按key排序)

  3. 红黑树需要五个模板参数,其中key+data=Value。模板类里有三个元素:node_count、header、key_compare。
    在这里插入图片描述

  4. 红黑树里有一个虚header节点,其父节点指向根节点,左子节点指向最小节点,右子节点指向最大节点。

  5. int main() {
        _Rb_tree<int, int, _Identity<int>, less<int>> itree;
        itree._M_insert_unique(1);
        itree._M_insert_unique(2);
        itree._M_insert_unique(3);
        itree._M_insert_unique(4);
        itree._M_insert_unique(2);
        itree._M_insert_unique(6);
        cout << itree.size() << endl;
        cout << itree.count(2) << endl;
        itree._M_insert_equal(2);
        cout << itree.size() << endl;
        cout << itree.count(2) << endl;
    }
    

    输出:

    5
    1
    6
    2
    

1.7 set、multiset

  1. set/multiset以rb_tree作为底层结构来实现,因此有元素自动排序的特性,排序的依据是key,而set/multiset的value和key合一,即:value就是key
  2. **无法使用set/multiset的iterator来改变元素值。**从set中取 iterator时,取到的是const_itreator,从而禁止了使用者通过itreator改变元素值
  3. set调用insert_unique(),multiset调用insert_equal(),所以set的不重复,multiset可重复
  4. set的所有操作都是转调用ret_type t进行操作,也就是rb_tree进行操作。
    在这里插入图片描述

1.8 map、multimap

  1. map/multimap以rb_tree作为底层结构来实现,因此有元素自动排序的特性,排序的依据是key,
  2. 无法使用map/multimap的iterator来改变元素的key值,但可以改变data的值。
  3. map调用insert_unique(),multimap调用insert_equal(),所以map的不重复,multimap可重复
  4. 从下图中可以看出,map使用key和data生成pair时(pair作为value),将const key作为first的值,从而禁止了使用者改变key。另外,rb_tree中所需要的KeyOfValue的类型使用select1st获取pair的first类型。(与identity一样,select1st也并不是标准库提供的,所以当不提供时,需要手动实现)
    在这里插入图片描述
  5. map容器独有的operator[],检查是否存在key,不存在的话创建一个值为默认参数的key-data对并返回data,存在就返回data。

1.9 hashtable深度探索

1.9.1 原理
  1. 编号为1~N的M(M < N)个元素,放到大小为M的容器中,编号id(hashcode)%M为该元素应放的位置。当两个元素计算出来的位置相同时,称为hash碰撞。

  2. GC处理hash碰撞的方式:拉链法

    • 拉链法
      1. 当发生碰撞时,在碰撞位置放置一个链表,碰撞的元素存储到链表中
      2. 当元素个数大于散列表篮子(bucket)个数时,散列表进行二倍扩容,然后所有元素重新计算放置的篮子位置
      3. 根据经验值,篮子初始值可以为53,扩容到53倍数附近的质数
        在这里插入图片描述
  3. hashtable有六个模板参数:Value,Key,HashFcn,ExtractKey(KeyOfValue),EqualKey,Alloc;五个成员变量:hasher,equals,get_key,buckets,num_elements
    在这里插入图片描述

  4. 从上图可以看出,hashtable的迭代器有两个指针:cur和ht,其中ht指向篮子,cur指向篮子中的节点。即:hashtable的迭代器必须有能力在一个篮子的最后一个元素跳到下一个元素(类似deque)

  5. 采用hash来生成hashcode,对于数值直接返回该值作为hashcode,对于字符串采用__stl_hash_string(根据每个字符计算出一个够乱的值)生成hashcode
    在这里插入图片描述
    在这里插入图片描述

  6. hashtable的操作:hashtable 的插入 跟 RB-tree 的插入类似,有两种插入方法 insert_unique 和 insert_equal ,意思也是一样的,insert_unique 不允许有重复值,而 insert_equal 允许有重复值。

    • 现在来看一下 insert_unique 函数,需要注意的是插入时,新节点直接插入到链表的头节点,代码如下:

      pair<iterator, bool> insert_unique(const value_type& obj)
      {
      	resize(num_elements + 1);//resize()函数判断需不需要进行扩容操作
      	return insert_unique_noresize(obj);
      }
      
      pair<iterator, bool> insert_unique_noresize(const value_type& obj)
      {
      	const size_type n = bkt_num(obj);	//决定 obj 应位于 buckets 的那一个链表中
      	node* first = buckets[n];
      
      	//遍历当前链表,如果发现有相同的键值,就不插入,立刻返回
      	for( node* cur = first; cur; cur = cur->next)
      	{
      		if( equals(get_key(cur->val), get_key(obj)) )
      			return pair<iterator, bool>(iterator(cur, this), false);
      	}
      	
      	//离开以上循环(或根本未进入循环)时,first指向 bucket 所指链表的头节点
      	node* tmp = new_node(obj);		//产生新节点
      	tmp->next = first;
      	buckets[n] = tmp;				//令新节点为链表的第一个节点
      	++num_elements;					//节点个数累加1
      	return pair<iterator, bool>( iterator(tmp,this), true);
      }
      
    • 允许重复插入的 insert_equal,需要注意的是插入时,重复节点插入到相同节点的后面,新节点还是插入到链表的头节点,代码如下:

      iterator insert_equal(const value_type& obj)
      {
      	resize( num_elements + 1 ); //判断是否 需要重建表格,如需要就扩充
      	return insert_equal_noresize(obj);
      }
      
      iterator insert_equalnoresize(const value_type& obj)
      {
      	const size_type n = bkt_num(obj);	//决定 obj 应位于 buckets 的那一个链表中
      	node* first = buckets[n];
      
          //遍历当前链表,如果发现有相同的键值,就马上插入,立刻返回
          for( node* cur = first; cur; cur = cur->next)
          {
          	if( equals(get_key(cur->val), get_key(obj)) )
          	{
          		node* tmp = new_node(obj);
          		tmp->next = cur->next;		//新节点插入当前节点位置之后
          		cur->next = tmp;
          		++num_elements;
          		return iterator(tmp, this);
          	}
          }
          	
          //运行到这里,表示没有发现重复的键值
          node* tmp = new_node(obj);		//产生新节点
          tmp->next = first;
          buckets[n] = tmp;				//令新节点为链表的第一个节点
          ++num_elements;					//节点个数累加1
          return iterator(tmp, this);
      }
      
1.9.2 一个万用的Hash Function
  1. 为一个类构建一个Hash Function。有三个办法,一是设计为类的成员函数,需要重载operator();二是设计成普通函数。第二种方法中,<>中第二个模板参数是函数类型,创建时还要将真正的函数地址放进来。第三个是写一个hash的特化版本
    在这里插入图片描述

  2. 如何为一个含有多种类型的数据成员的类构建一个Hash Function?

    一个可行的方法是对每个数据成员按类型进行hash再相加,但是这个方法太天真。
    通用的方法是采用hash_val(),如图所示,这个函数有三个版本。typename… Types任意多的模板参数,可变参数模板。函数1和函数2,函数3的区别是第一个模板参数的类型不一样,虽然同名函数。从参数来看,先调用的是函数1,假设原来参数有8 个,产生一个种子(seed),即一个种子加8个参数。然后调用函数2,函数2将8个参数变成1+7,将这1个参数去变化种子,这时候就是种子+7个参数,然后递归调用,直到只剩最后一个值时,hash_val调用的是函数3,函数三再调用hash_combine,生成最后一个种子,即hash_code。hash_combine中0x9e3779b9这个数值是根据黄金比例得来的

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值