笔记 STL源码剖析(二)

五  关联式容器

      标准STL关联式容器分为 set(集合)和map(映射表)两大类,以及这两大类的衍生体 multiset,multimap。这些容器的底层机制都是以RB-tree(红黑树)完成。RB-tree 也是一个独立容器,但并不开放给外界使用。

      STL还提供了不在标准规格之列的关联式容器 unordered_map ,unordered_set。

所谓关联式容器,每个元素都有一个键值(key)和一个值(value)。一般而言,关联式容器的内部结构是一个平衡二叉树,以获得良好的搜素效率。平衡二叉树有许多种类型,包括 AVL-tree、RB-tree, AA-tree, 其中最被广泛用于STL的是RB-tree。

二叉搜索树时间复杂度是O(log n) 节点放置规则是:任何节点的键值一定大于左子树的每一个节点的键值,小于右子树中每一个节点的键值

 AVL tree :加上“平衡条件”的二叉搜索树。要求任何节点的左右子树高度相差最多为1。

往平衡二叉树中添加节点可能会导致二叉树失去平衡,所以我们需要在每次插入节点后进行平衡的维护操作。

插入节点破坏平衡性有如下四种情况:

插入点位于X的左子节点的左子树--左左 (二叉树进行 右旋 )  位于X的左子节点的右子树--左右   (双旋转,先左旋再右旋

位于X的右子节点的左子树--右左 (双旋转,先右旋再左旋)  位于X的右子节点的右子树 --右右(二叉树 左旋 ,单旋转

左左情况:右旋

RB-tree(平衡二叉搜索树)

二叉搜索树,并且满足以下规则:

  • 每个节点不是红色就是黑色。
  • 根节点为黑色
  • 如果节点为红,其子节点必须为黑
  • 任一节点至NULL(树尾端)的任何路径,所含的黑节点数必须相同。

根据规则4,新增加节点必须为红色,根据规则3,新增节点的父节点,必须为黑

当新节点未符合上述规则时,必须调整颜色并旋转树形。

例如:

RB-tree 节点的设计

typedef bool __rb_tree_color_type;
const __rb_tree_color_type __rb_tree_red = false;   //红色为0
const __rb_tree_color_type __rb_tree_black = true;  //黑色为1

//RB-tree节点的基类
struct __rb_tree_node_base
{
  typedef __rb_tree_color_type color_type;
  typedef __rb_tree_node_base* base_ptr;

  color_type color; //颜色
  base_ptr parent;  //指向父节点的指针
  base_ptr left;    //指向左子节点的指针
  base_ptr right;   //指向右子节点的指针

  //静态函数,获取以x为根节点的RB-tree最小节点的指针
  static base_ptr minimum(base_ptr x)
  {
    while (x->left != 0) x = x->left;
    return x;
  }

  //静态函数,获取以x为根节点的RB-tree最大节点的指针
  static base_ptr maximum(base_ptr x)
  {
    while (x->right != 0) x = x->right;
    return x;
  }
};

//RB-tree节点类
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base
{
  typedef __rb_tree_node<Value>* link_type;
  Value value_field;    //RB-tree节点的value
};

 RB-tree的迭代器

SGI将RB-tree迭代器实现为两层:

RB-tree 迭代器属于双向迭代器,但不具备随机定位能力。前进操作operator++()调用了基类迭代器的increment(),后退操作operator--()调用了基类迭代器的decrement()。前进或后退的举止行为完全依据二叉搜索树的节点排列法则.

struct __rb_tree_base_iterator
{
  typedef __rb_tree_node_base::base_ptr base_ptr;
  typedef bidirectional_iterator_tag iterator_category;
  typedef ptrdiff_t difference_type;
  base_ptr node;

  void increment()
  {
    if (node->right != 0) {  //如果有右子节点
      node = node->right;    //就往右走
      while (node->left != 0)   //然后一直往左子树走到底
        node = node->left;
    }
    else {    //没有右子节点
      base_ptr y = node->parent;  //找出父节点
      while (node == y->right) {  //如果现行节点本身是个右子节点
        node = y;             //就一直上溯,直到“不为右子节点”止
        y = y->parent;
      }
      if (node->right != y)
        node = y;
    }
  }

  void decrement()
  {
    if (node->color == __rb_tree_red &&
        node->parent->parent == node)
      node = node->right;
    else if (node->left != 0) {
      base_ptr y = node->left;
      while (y->right != 0)
        y = y->right;
      node = y;
    }
    else {
      base_ptr y = node->parent;
      while (node == y->left) {
        node = y;
        y = y->parent;
      }
      node = y;
    }
  }
};

RB-tree 的数据结构

template<class Key,class Value,class KeyOfValue,class Compare, class Alloc=alloc>

class rb_tree{
protected:
    typedef _rb_tree_node<Value> rb_tree_node;
     .....
public:
    typedef rb_tree_node* link_type;
     .....
protected:
     //RB-tree 只以三笔资料表现自己
     size_type node_count;   //rb_tree 的大小(节点数)
     link_type header;
     compare key_compare; //key的大小比较准则
};

void init(){
    header=get_node();  //产生一个节点空间,令header指向它
     color(header)=_rb_tree_red;  //令header为红色,用来区分header 和 root 
    root()=0;
    leftmost()=header;  //令header的左子节点为自己
    rightmost()=header;  //令header的右子节点为自己
}

header 的父节点指向根节点,左子节点指向最小节点,右子节点指向最大节点。

RB-tree 的元素插入操作有两个 insert_equal() //允许节点键值重复  insert_unique() //不重复

STL为什么选择红黑树而不是AVL-tree?

红黑树的平衡性是比AVL-tree弱的,但是搜索效率几乎相等。两者的插入和删除操作都是O(logn),但是就旋转操作而言,AVL-tree是O(n),而红黑树是O(1)


set

set 的所有元素都会根据元素的键值自动被排序。元素的键值就是实值,实值就是键值、set不允许两个元素具有相同的键值。

set 元素不能改变,在set源码中,set<T>::iterator被定义为底层TB-tree的const_iterator,杜绝写入操作,也就是说,set iterator是一种constant iterators(相对于mutable iterators)。几乎set 操作都只是转调用rb_tree的操作行为而已。

template <  class Key, 
            class Compare = less<Key>,  //缺省情况下,使用递增排序
            class Alloc = alloc>
class set {
public:
    ...
    //键值和实值类型相同,比较函数也是同一个
    typedef Key key_type;
    typedef Key value_type;
    typedef Compare key_compare;
    typedef Compare value_compare;
private:
    ...
    typedef rb_tree<key_type, value_type, 
                  identity<value_type>, key_compare, Alloc> rep_type;
    rep_type t;  // 内含一棵RB-tree,使用RB-tree来表现set
public:
    ...
    //iterator定义为RB-tree的const_iterator,表示set的迭代器无法执行写操作
    typedef typename rep_type::const_iterator iterator;
    ...

测试用例(让set从大到小存放元素):

#include <iostream>
#include <set>
#include <functional>
using namespace std;

/// set默认是从小到大排列,以下是让set从大到小排列
template <typename T>
struct greator
{
    bool operator()(const T &x, const T &y)
    {
        return x > y;
    }
};

int main(void)
{
    set<int, greator<int>> iset;
    iset.insert(12);
    iset.insert(1);
    for (set<int>::const_iterator iter = iset.begin(); iter != iset.end(); iter++)
        cout << *iter << " ";
    cout << endl;
    system("pause");
    return 0;
}

map

map 的所有元素都会根据元素的键值自动排序。map的所有元素都是pair,同时拥有实值(value)和键值(key)。pair的第一元素为键值,第二元素为实值。map不允许有两个相同的键值。

<stl_pair.h>中的pair 定义:

template<class T1,class T2>
struct pair{
   typedef T1 first_type;
   typedef T2 second_type;
   T1 first;  
   T2 second;
   
   pair():first(T1()),second(T2()) {}
   pair(const T1& a, const T2& b ):first(a),second(b) {}
};

map 架构

template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
  typedef Key key_type;     //键值类型
  typedef T data_type;      //实值类型
  typedef T mapped_type;    
  typedef pair<const Key, T> value_type;    //键值对,RB-tree节点中的value类型
  typedef Compare key_compare;  //键值比较函数

  ...

private:
  typedef rb_tree<key_type, value_type, 
                  select1st<value_type>, key_compare, Alloc> rep_type;
  rep_type t;  // 内含一棵RB-tree,使用RB-tree来表现map
public:
  ...
  //迭代器和set不同,允许修改实值
  typedef typename rep_type::iterator iterator;
  ...

  //下标操作
  T& operator[](const key_type& k) {
    return (*((insert(value_type(k, T()))).first)).second;
  }

  //插入操作
  pair<iterator,bool> insert(const value_type& x) { return t.insert_unique(x); }

  ...
};

multiset

multiset的特性及用法和set完全相同,唯一的差别在于它允许键值重复,插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique()

multimap

multimap的特性及用法和map完全相同,唯一的差别在于它允许键值重复,插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique()

hash table

二叉搜索树具有对数平均时间表现,但这样的表现构造在一个假设上:输入数据有足够的随机性。hashtable这种结构在插入、删除、查找具有“常数平均时间”,而且这种表现是以统计为基础,不需依赖元素的随机性。

散列函数:使用某种映射函数,负责将某一元素映射为一个索引。但不同的元素可能会被映射到相同的位置(相同的索引),出现所谓的碰撞问题,包括:线性探测、二次探测、开链等。

线性探测: 负载系数,意指元素个数除以表格大小,负载系数永远在0~1之间。若出现相同索引,则循序往下寻找一个可用空间。

二次探测:其命名由来是因为解决碰撞问题的方程式 F(i)=i^2 ;如果hash function 计算出新元素的位置为H,而该位置实际上已被使用,那么就依序尝试H+1,H+^2,H+3^2......,而不是像线性探测那样依序尝试H+1,H+2,H+3.....

开链法:在每一个表格元素中维护一个list,hash function 为我们分配某一个list,然后在list上执行元素的增删改查。SGI STL的hash table 便是采用这种做法

hash table 的 buckets 与node

SGI STL中以开链法实现hash table,hash table表格中的元素为桶,每个桶中包含了哈希到这个桶中的节点,节点定义如下:

template <class Value>
struct __hashtable_node
{
    __hashtable_node *next;
    Value val;
};

注意:bucket所维护的list,并不采用STL的list或slist,而是自行维护上述的hash table node,至于buckets聚合体,则以vector完成,以便有动态扩充能力

hashtable 的迭代器

前进操作首先尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易完成。如果目前节点正好是list的尾端,就跳至下一个bucket身,它正好指向下一个list的头部节点:

template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
  const node* old = cur;
  cur = cur->next;  //如果存在,就是它。否则进入以下if流程
  if (!cur) {
    //根据元素值,定位出下一个bucket,其起头处就是我们的目的地
    size_type bucket = ht->bkt_num(old->val);
    while (!cur && ++bucket < ht->buckets.size())
      cur = ht->buckets[bucket];
  }
  return *this;
}

template <class V, class K, class HF, class ExK, class EqK, class A>
inline __hashtable_iterator<V, K, HF, ExK, EqK, A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++(int)
{
  iterator tmp = *this;
  ++*this;
  return tmp;
}

hash table的迭代器没有后退操作,hashtable也没有定义所谓的逆向迭代器。

hash table的数据结构

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc = alloc>
class hashtable;

...

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey,
          class Alloc> //先前声明时,已给出Alloc默认值alloc
class hashtable {
public:
  typedef HashFcn hasher;
  typedef EqualKey key_equal;
  ...
private:
  //以下3者都是function  objects
  hasher hash;
  key_equal equals;
  ExtractKey get_key;

  typedef __hashtable_node<Value> node;  //hashtable节点类型
  typedef simple_alloc<node, Alloc> node_allocator;

  vector<node*,Alloc> buckets; //hashtable的桶数组,以vector完成
  size_type num_elements;      //元素个数
  ...
};

SGI STL以质数来设计表格大小,并且先将28个质数(逐渐呈现大约2倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数中,“最接近某数并大于某数”的质数:

static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,           193,         389,       769,
  1543,       3079,         6151,        12289,     24593,
  49157,      98317,        196613,      393241,    786433,
  1572869,    3145739,      6291469,     12582917,  25165843,
  50331653,   100663319,    201326611,   402653189, 805306457, 
  1610612741, 3221225473ul, 4294967291ul
};

一开始保留50个节点,由于最接近的STL质数为53,所以buckets vector 保留的是53个buckets,每个bukcets(指针,指向一个hash table 节点)的初值为0,接下来,循序加入6个元素,如图5-26所示。再插入48个元素,总元素达到54个,超过当时的bucket  vector 大小,hash table 重建,变成右图所示

判断元素落在哪个bucket;

SGI 把这个任务包装了一层,先交给 bkt_num() 函数,再由此函数调用 hash function,取得一个可以执行modulus(取模)运算的数值。

//版本1 接受实值(value)和buckets 个数
size_type bkt_num (const value_type& obj, size_t n)const{
       return bkt_num_key( get_key(obj),n); //调用版本4
}
//版本2 只接受实值(value)
size_type bkt_num(const value_type& obj) const{
     return bkt_num_key(get_key(obj));   //调用版本3
}
//版本3:只接受键值
size_type bkt_num_key(const key_type& key) const{
   return bkt_num_key(key,buckets.size());  //调用版本4
}
//版本4: 接受键值和 buckets 个数
size_type bkt_num_key(const key_type& key,size_t n)cosnt{
     return hash(key) % n;   //
}

hash functions

hash function是计算元素位置的函数,SGI将这项任务赋予了bkt_num(),再由它来调用这里提供的hash function,取得一个可以对hashtable进行模运算的值。针对char,int,long等整数类型,大部分的hash functions什么也没做,只是忠实返回原值

但对于字符字符串(const char*),就设计了一个转换函数

其余类型用特化版本

hash table 无法处理上述所列各项型别以外的元素,例如 string,double,float ,欲处理这些型别,用户必须自行为它们定义 hash function

hash_set

hash_set 以 hashtable 为底层机制,由于 hash_set 所供应的操作接口 hashtable 都提供了,所以几乎所有的hash_set操作行为,都只是转调用hashtable的操作行为而已

hash_multiset

hash_multiset 和 hash_set 实现上的唯一差别在于,前者的元素插入操作采用底层机制hashtable的insert_equal(),后者则是采用insert_unique()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
侯捷的《STL源码剖析》是一本关于STL(标准模板库)的学习笔记。这本书深入解析了STL的实现原理和设计思路,对于理解STL的内部机制和使用方法非常有帮助。这些学习笔记记录了作者在学习侯捷的《STL标准库和泛型编程》课程时的心得和总结,对于理解STL源码和进行泛型编程都具有指导意义。 这本书涉及了STL的各个模块,包括容器、迭代器、算法等,并解释了它们的实现原理和使用方法。通过学习这本书,你可以更好地理解STL的底层实现和使用技巧。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [STLSourceAnalysis:stl原始码剖析(侯捷)的学习笔记](https://download.csdn.net/download/weixin_42175776/16069622)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [候捷老师STL源码剖析视频课程笔记](https://blog.csdn.net/weixin_46065476/article/details/125547869)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [侯捷——STL源码剖析 笔记](https://blog.csdn.net/weixin_45067603/article/details/122770539)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值