关联式容器(Associative Containers) - SGI STL源码解析

STL中的关联式容器分为两大类:有序关联式容器无序关联式容器。前者基于红黑树实现,后者基于哈希表实现。
本文将从源码层面剖析SGI STL中关联式容器的设计与实现


红黑树 - 有序关联式容器的基石

set、map、multiset、multimap这四个容器都基于红黑树(Red-Black Tree)实现。红黑树是一种自平衡二叉搜索树,
通过特定的着色规则和旋转操作保证查找、插入、删除操作的时间复杂度为O(log n)。

红黑树的基本特性

// stl_tree.h
typedef bool _Rb_tree_Color_type;
const _Rb_tree_Color_type _S_rb_tree_red = false;   // 红色 = false
const _Rb_tree_Color_type _S_rb_tree_black = true;  // 黑色 = true

红黑树必须满足以下五条性质:

  1. 每个节点非红即黑
  2. 根节点必为黑
  3. 红节点的子节点必为黑(不能出现两个连续的红节点)
  4. 任一节点到null路径上的黑色节点数量相同(新插入元素默认为红)
  5. null节点视为黑

红黑树节点结构

// stl_tree.h

// 基类 - 不包含值域,只包含三叉链和颜色
struct _Rb_tree_node_base
{
  typedef _Rb_tree_Color_type _Color_type;
  typedef _Rb_tree_node_base* _Base_ptr;

  _Color_type _M_color;   // 节点颜色
  _Base_ptr _M_parent;    // 父节点指针,为了方便红黑树的旋转操作
  _Base_ptr _M_left;      // 左子节点指针
  _Base_ptr _M_right;     // 右子节点指针

  // 寻找以__x为根的子树的最小节点
  static _Base_ptr _S_minimum(_Base_ptr __x)
  {
    while (__x->_M_left != 0) __x = __x->_M_left;
    return __x;
  }

  // 寻找以__x为根的子树的最大节点
  static _Base_ptr _S_maximum(_Base_ptr __x)
  {
    while (__x->_M_right != 0) __x = __x->_M_right;
    return __x;
  }
};

// 模板节点类 - 继承自基类,并添加值域
template <class _Value>
struct _Rb_tree_node : public _Rb_tree_node_base
{
  typedef _Rb_tree_node<_Value>* _Link_type;
  _Value _M_value_field;  // 实际存储的值
};

这里采用了基类+模板子类的设计模式,将与值类型无关的通用结构(指针、颜色)与具体的值域分离,
这样可以复用大部分代码

红黑树迭代器

// stl_tree.h

// 基类迭代器 - 实现递增/递减逻辑
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 _M_node;  // 当前节点指针

  // 递增操作 - 寻找后继节点
  void _M_increment()
  {
    if (_M_node->_M_right != 0) {
      // 情况1: 有右子树 -> 右子树的最左节点
      _M_node = _M_node->_M_right;
      while (_M_node->_M_left != 0)
        _M_node = _M_node->_M_left;
    }
    else {
      // 情况2: 无右子树 -> 向上回溯,直到成为父节点的左子
      _Base_ptr __y = _M_node->_M_parent;
      while (_M_node == __y->_M_right) {
        _M_node = __y;
        __y = __y->_M_parent;
      }
      // 如果当前节点是右子节点,此时__y是父节点,需要跳过header
      if (_M_node->_M_right != __y)
        _M_node = __y;
    }
  }

  // 递减操作 - 寻找前驱节点
  void _M_decrement()
  {
    // 特殊处理: header节点的情况
    if (_M_node->_M_color == _S_rb_tree_red &&
        _M_node->_M_parent->_M_parent == _M_node)
      _M_node = _M_node->_M_right;  // header的右子是最大节点
    else if (_M_node->_M_left != 0) {
      // 情况1: 有左子树 -> 左子树的最右节点
      _Base_ptr __y = _M_node->_M_left;
      while (__y->_M_right != 0)
        __y = __y->_M_right;
      _M_node = __y;
    }
    else {
      // 情况2: 无左子树 -> 向上回溯,直到成为父节点的右子
      _Base_ptr __y = _M_node->_M_parent;
      while (_M_node == __y->_M_left) {
        _M_node = __y;
        __y = __y->_M_parent;
      }
      _M_node = __y;
    }
  }
};

// 模板迭代器类 - 添加value_type相关的类型定义
template <class _Value, class _Ref, class _Ptr>
struct _Rb_tree_iterator : public _Rb_tree_base_iterator
{
  typedef _Value value_type;
  typedef _Ref reference;
  typedef _Ptr pointer;

  reference operator*() const { return _Link_type(_M_node)->_M_value_field; }
  pointer operator->() const { return &(operator*()); }

  _Self& operator++() { _M_increment(); return *this; }
  _Self& operator--() { _M_decrement(); return *this; }
};

关键理解:红黑树的迭代器是bidirectional iterator(双向迭代器),不支持随机访问,
只能通过递增/递减操作移动。这是红黑树本身的结构特性决定的

旋转与再平衡

// 左旋 - 将x旋转为其右子y的左子,x下降,y上升
inline void
_Rb_tree_rotate_left(_Rb_tree_node_base* __x, _Rb_tree_node_base*& __root)
{
  _Rb_tree_node_base* __y = __x->_M_right;
  __x->_M_right = __y->_M_left;
  if (__y->_M_left != 0)
    __y->_M_left->_M_parent = __x;
  __y->_M_parent = __x->_M_parent;

  if (__x == __root)
    __root = __y;
  else if (__x == __x->_M_parent->_M_left)
    __x->_M_parent->_M_left = __y;
  else
    __x->_M_parent->_M_right = __y;
  __y->_M_left = __x;
  __x->_M_parent = __y;
}

// 右旋 - 对称操作
inline void
_Rb_tree_rotate_right(_Rb_tree_node_base* __x, _Rb_tree_node_base*& __root)
{
  _Rb_tree_node_base* __y = __x->_M_left;
  __x->_M_left = __y->_M_right;
  if (__y->_M_right != 0)
    __y->_M_right->_M_parent = __x;
  __y->_M_parent = __x->_M_parent;

  if (__x == __root)
    __root = __y;
  else if (__x == __x->_M_parent->_M_right)
    __x->_M_parent->_M_right = __y;
  else
    __x->_M_parent->_M_left = __y;
  __y->_M_right = __x;
  __x->_M_parent = __y;
}

旋转操作是红黑树保持平衡的核心手段。左旋和右旋是对称的操作,
用于调整树的平衡性,插入和删除时都需要调用旋转来维持红黑性质

插入后的再平衡

inline void
_Rb_tree_rebalance(_Rb_tree_node_base* __x, _Rb_tree_node_base*& __root)
{
  __x->_M_color = _S_rb_tree_red;  // 新节点必为红
  while (__x != __root && __x->_M_parent->_M_color == _S_rb_tree_red) {
    // 如果父节点是红(违背了性质3),需要调整
    if (__x->_M_parent == __x->_M_parent->_M_parent->_M_left) {
      _Rb_tree_node_base* __y = __x->_M_parent->_M_parent->_M_right;
      // 情况1: 叔叔节点存在且为红 -> 变色即可
      if (__y && __y->_M_color == _S_rb_tree_red) {
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __y->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        __x = __x->_M_parent->_M_parent;  // 继续向上检查
      }
      else {
        // 情况2: 叔叔为黑或不存在
        if (__x == __x->_M_parent->_M_right) {
          // 先左旋(变成直线形)
          __x = __x->_M_parent;
          _Rb_tree_rotate_left(__x, __root);
        }
        // 再右旋 + 变色
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        _Rb_tree_rotate_right(__x->_M_parent->_M_parent, __root);
      }
    }
    else {
      // 对称情况,处理右子树的插入
      _Rb_tree_node_base* __y = __x->_M_parent->_M_parent->_M_left;
      if (__y && __y->_M_color == _S_rb_tree_red) {
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __y->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        __x = __x->_M_parent->_M_parent;
      }
      else {
        if (__x == __x->_M_parent->_M_left) {
          __x = __x->_M_parent;
          _Rb_tree_rotate_right(__x, __root);
        }
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        _Rb_tree_rotate_left(__x->_M_parent->_M_parent, __root);
      }
    }
  }
  __root->_M_color = _S_rb_tree_black;  // 根节点必为黑
}

插入操作的时间复杂度为O(log n),其中再平衡过程是主要的开销

红黑树的数据结构

// stl_tree.h

template <class _Key, class _Value, class _KeyOfValue, class _Compare, class _Alloc>
class _Rb_tree : protected _Rb_tree_base<_Value, _Alloc> {
protected:
  typedef _Rb_tree_node_base* _Base_ptr;
  typedef _Rb_tree_node<_Value> _Rb_tree_node;

  // 模板参数说明:
  // _Key: 键类型
  // _Value: 值类型(对于map是pair,对于set是Key本身)
  // _KeyOfValue: 从Value中提取Key的函数对象
  // _Compare: 键比较函数

private:
  size_type _M_node_count;    // 节点数量
  _Compare _M_key_compare;     // 键比较函数

  // 维护三个特殊指针(header节点的设计)
  _Link_type& _M_root() const
    { return (_Link_type&) _M_header->_M_parent; }
  _Link_type& _M_leftmost() const  // 指向最小节点
    { return (_Link_type&) _M_header->_M_left; }
  _Link_type& _M_rightmost() const  // 指向最大节点
    { return (_Link_type&) _M_header->_M_right; }

  // 静态方法用于节点操作
  static _Link_type& _S_left(_Link_type __x)
    { return (_Link_type&)(__x->_M_left); }
  static _Link_type& _S_right(_Link_type __x)
    { return (_Link_type&)(__x->_M_right); }
  static _Link_type& _S_parent(_Link_type __x)
    { return (_Link_type&)(__x->_M_parent); }
  static reference _S_value(_Link_type __x)
    { return __x->_M_value_field; }
  // 提取键的关键方法 - 使用KeyOfValue函数对象
  static const _Key& _S_key(_Link_type __x)
    { return _KeyOfValue()(_S_value(__x)); }
};

header节点的设计

SGI STL的红黑树实现中,引入了一个header节点(不存储实际数据),这是一个巧妙的设计:

  • header的parent指向根节点
  • header的left指向最小节点(最左子)
  • header的right指向最大节点(最右子)
  • begin()返回最左子节点,end()返回header节点

这样设计使得:

  1. begin()操作可以在O(1)时间内完成
  2. end()操作可以用header直接表示
  3. 迭代器的++/–操作更容易实现

_KeyOfValue与_Select1st

在分析set和map之前,必须理解这两个函数对象的作用

// stl_function.h

// _Identity: 返回自身,用于set - key就是value
template <class _Tp>
struct _Identity : public unary_function<_Tp,_Tp> {
  const _Tp& operator()(const _Tp& __x) const { return __x; }
};

// _Select1st: 提取pair的第一个元素,用于map - 从pair中取出key
template <class _Pair>
struct _Select1st : public unary_function<_Pair, typename _Pair::first_type> {
  const typename _Pair::first_type& operator()(const _Pair& __x) const {
    return __x.first;
  }
};

set - 键值集合

set是最简单的关联式容器,其特点是:键即值,键唯一,自动排序

set的实现

// stl_set.h

template <class _Key, class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
          class _Alloc = __STL_DEFAULT_ALLOCATOR(_Key) >
class set {
  // requirements:
  __STL_CLASS_REQUIRES(_Key, _Assignable);
  __STL_CLASS_BINARY_FUNCTION_CHECK(_Compare, bool, _Key, _Key);

public:
  typedef _Key     key_type;      // 键类型
  typedef _Key     value_type;     // 值类型(与键相同)
  typedef _Compare key_compare;   // 键比较函数
  typedef _Compare value_compare; // 值比较函数(与键比较相同)

private:
  // 关键: _Identity意味着key就是value
  typedef _Rb_tree<key_type, value_type,
                  _Identity<value_type>, key_compare, _Alloc> _Rep_type;
  _Rep_type _M_t;  // 内部真正存储数据的红黑树

public:
  // 所有迭代器都是const的 - set不允许修改元素
  typedef typename _Rep_type::const_iterator iterator;
  typedef typename _Rep_type::const_iterator const_iterator;
  ...
};

核心理解:set的_Identity<value_type>表示"从value中提取key的方式就是返回value本身",
这意味着set的键和值是同一个东西

set的插入操作

// set中唯一允许的插入方式是insert_unique
pair<iterator,bool> insert(const value_type& __x) {
  pair<typename _Rep_type::iterator, bool> __p = _M_t.insert_unique(__x);
  return pair<iterator, bool>(__p.first, __p.second);
}

insert_unique保证了set中不会有重复的元素。如果插入的键已存在,insert_unique会返回{existing_iterator, false}

set与multiset的区别

set使用insert_unique,multiset使用insert_equal

// multiset使用insert_equal,允许重复键
iterator insert(const value_type& __x) {
  return _M_t.insert_equal(__x);
}

map - 键值对容器

map的特点是:键唯一,自动排序,每个键对应一个值

map的实现

// stl_map.h

template <class _Key, class _Tp,
          class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),
          class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class map {
public:
  typedef _Key                  key_type;
  typedef _Tp                   data_type;
  typedef _Tp                   mapped_type;  // 与data_type同义
  typedef pair<const _Key, _Tp> value_type;   // 键值对

  class value_compare : public binary_function<value_type, value_type, bool> {
  friend class map<_Key,_Tp,_Compare,_Alloc>;
  protected:
    _Compare comp;
    value_compare(_Compare __c) : comp(__c) {}
  public:
    // 比较两个pair时,只比较first(键)
    bool operator()(const value_type& __x, const value_type& __y) const {
      return comp(__x.first, __y.first);
    }
  };

private:
  // 关键: _Select1st用于从pair中提取key
  typedef _Rb_tree<key_type, value_type,
                   _Select1st<value_type>, key_compare, _Alloc> _Rep_type;
  _Rep_type _M_t;
  ...
};

核心理解:map使用_Select1st<value_type>从pair中提取键(first),这样红黑树在比较时可以只比较键

map::operator[]

operator[]是map中最特别的一个函数

// stl_map.h
_Tp& operator[](const key_type& __k) {
  iterator __i = lower_bound(__k);
  // __i->first is greater than or equivalent to __k.
  if (__i == end() || key_comp()(__k, (*__i).first))
    __i = insert(__i, value_type(__k, _Tp()));  // 插入默认值
  return (*__i).second;  // 返回值的引用
}

特性

  1. 如果键不存在,会插入一个默认值(value_type的默认构造)
  2. 返回的是值的引用,所以可以用于修改
  3. 这也是map不能用const map调用的原因

map与multimap的区别

与set/multiset相同,map使用insert_unique,multimap使用insert_equal


multiset与multimap

multiset和multimap与set和map几乎完全相同,唯一的区别是:

  • 使用insert_equal而非insert_unique
  • 允许键重复

例如multimap的count()可以返回大于1的值,而map的count()只能是0或1


哈希表 - 无序关联式容器的基石

C++11引入的unordered_set、unordered_map等容器,在SGI STL中对应的是hash_set、hash_map等。
这些容器基于哈希表实现,查找、插入、删除操作平均时间复杂度为O(1)

hashtable节点

// stl_hashtable.h

template <class _Val>
struct _Hashtable_node
{
  _Hashtable_node* _M_next;  // 指向同桶中的下一个节点
  _Val _M_val;              // 存储的值
};

与红黑树的节点不同,哈希表节点只有单向链表指针,没有父指针和子指针,
因为哈希表是桶数组的结构,而非树形结构

hashtable的核心结构

// stl_hashtable.h

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable {
public:
  typedef _Key key_type;
  typedef _Val value_type;
  typedef _HashFcn hasher;      // 哈希函数
  typedef _EqualKey key_equal;   // 键相等判断

private:
  hasher                _M_hash;           // 哈希函数对象
  key_equal             _M_equals;         // 相等判断对象
  _ExtractKey           _M_get_key;        // 从value提取key
  vector<_Node*,_Alloc> _M_buckets;         // 桶数组
  size_type             _M_num_elements;   // 元素个数
  ...
};

关键设计

  1. _M_buckets是vector,存储每个桶的头指针
  2. 每个桶内是单向链表,相同哈希值的元素链在一起
  3. _M_hash用于计算哈希值,_M_equals用于比较键是否相等

素数表与桶数量

// stl_hashtable.h

// 28个素数,用于确定桶的数量
enum { __stl_num_primes = 28 };

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

// 找到 >= n 的最小素数
inline unsigned long __stl_next_prime(unsigned long __n)
{
  const unsigned long* __first = __stl_prime_list;
  const unsigned long* __last = __stl_prime_list + (int)__stl_num_primes;
  const unsigned long* pos = lower_bound(__first, __last, __n);
  return pos == __last ? *(__last - 1) : *pos;
}

为什么用素数作为桶数量?
使用素数作为桶数量可以使得哈希值分布更均匀,减少冲突。
当除数是素数时,对于哈希值h,(h % prime)的结果分布更均匀

哈希函数

// stl_hash_fun.h

template <class _Key> struct hash { };  // 默认不提供,需要特化

// char* 的特化版本
__STL_TEMPLATE_NULL struct hash<char*>
{
  size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};

// 字符串哈希实现
inline size_t __stl_hash_string(const char* __s)
{
  unsigned long __h = 0;
  for ( ; *__s; ++__s)
    __h = 5*__h + *__s;  // h = h * 5 + char
  return size_t(__h);
}

// 内置类型的特化
__STL_TEMPLATE_NULL struct hash<int> {
  size_t operator()(int __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
  size_t operator()(unsigned int __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<long> {
  size_t operator()(long __x) const { return __x; }
};
// ... 其他内置类型

注意:SGI STL的hash函数只提供了内置类型和char的特化,
没有提供string的特化。用户需要自己提供或使用标准库的unordered_

哈希表迭代器

// stl_hashtable.h

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
struct _Hashtable_iterator {
  typedef hashtable<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc>
          _Hashtable;
  typedef _Hashtable_iterator<_Val, _Key, _HashFcn,
                              _ExtractKey, _EqualKey, _Alloc>
          iterator;
  typedef forward_iterator_tag iterator_category;  // 向前迭代器
  typedef _Val value_type;
  typedef ptrdiff_t difference_type;
  typedef size_t size_type;
  typedef _Val& reference;
  typedef _Val* pointer;

  _Node* _M_cur;      // 当前节点
  _Hashtable* _M_ht; // 指向哈希表本体

  // 递增操作
  iterator& operator++() {
    const _Node* __old = _M_cur;
    _M_cur = _M_cur->_M_next;  // 先在桶内向后移动
    if (!_M_cur) {
      // 桶内已到末尾,寻找下一个非空桶
      size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val);
      while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size())
        _M_cur = _M_ht->_M_buckets[__bucket];
    }
    return *this;
  }
};

重要:哈希表迭代器是forward_iterator(单向迭代器),只能向前移动

插入操作

// stl_hashtable.h

// insert_unique - 不允许重复键
pair<iterator, bool> insert_unique_noresize(const value_type& __obj)
{
  const size_type __n = _M_bkt_num(__obj);  // 计算桶编号
  _Node* __first = _M_buckets[__n];

  // 在桶内查找是否已存在
  for (_Node* __cur = __first; __cur; __cur = __cur->_M_next)
    if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj)))
      return pair<iterator, bool>(iterator(__cur, this), false);

  // 头插法插入新节点
  _Node* __tmp = _M_new_node(__obj);
  __tmp->_M_next = __first;
  _M_buckets[__n] = __tmp;
  ++_M_num_elements;
  return pair<iterator, bool>(iterator(__tmp, this), true);
}

// insert_equal - 允许重复键
iterator insert_equal_noresize(const value_type& __obj)
{
  const size_type __n = _M_bkt_num(__obj);
  _Node* __first = _M_buckets[__n];

  // 查找是否已存在(允许重复)
  for (_Node* __cur = __first; __cur; __cur = __cur->_M_next)
    if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj))) {
      // 找到后,在当前位置后插入
      _Node* __tmp = _M_new_node(__obj);
      __tmp->_M_next = __cur->_M_next;
      __cur->_M_next = __tmp;
      ++_M_num_elements;
      return iterator(__tmp, this);
    }

  // 不存在则头插
  _Node* __tmp = _M_new_node(__obj);
  __tmp->_M_next = __first;
  _M_buckets[__n] = __tmp;
  ++_M_num_elements;
  return iterator(__tmp, this);
}

头插法的原因

  1. 头插O(1),尾插需要遍历
  2. 新插入的元素被访问的概率更高(局部性原理)

负载因子与再哈希

// stl_hashtable.h

template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
  ::resize(size_type __num_elements_hint)
{
  const size_type __old_n = _M_buckets.size();
  if (__num_elements_hint > __old_n) {
    // 需要扩容
    const size_type __n = _M_next_size(__num_elements_hint);  // 找到下一个素数
    if (__n > __old_n) {
      vector<_Node*, _All> __tmp(__n, (_Node*)(0),
                                 _M_buckets.get_allocator());
      __STL_TRY {
        // 重新哈希所有元素
        for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) {
          _Node* __first = _M_buckets[__bucket];
          while (__first) {
            // 重新计算桶编号
            size_type __new_bucket = _M_bkt_num(__first->_M_val, __n);
            _M_buckets[__bucket] = __first->_M_next;
            // 头插到新桶
            __first->_M_next = __tmp[__new_bucket];
            __tmp[__new_bucket] = __first;
            __first = _M_buckets[__bucket];
          }
        }
        _M_buckets.swap(__tmp);
      }
      // 异常处理...
    }
  }
}

什么时候再哈希?
当元素数量接近桶数量时(负载因子过高),冲突会加剧,性能下降。
SGI STL在insert操作前会调用resize,扩展桶数量


hash_set与hash_map

hash_set和hash_map是SGI STL对无序关联式容器的实现

// stl_hash_set.h

template <class _Value,
          class _HashFcn  __STL_DEPENDENT_DEFAULT_TMPL(hash<_Value>),
          class _EqualKey __STL_DEPENDENT_DEFAULT_TMPL(equal_to<_Value>),
          class _Alloc =  __STL_DEFAULT_ALLOCATOR(_Value) >
class hash_set {
private:
  // 内部使用hashtable,_Identity表示key就是value
  typedef hashtable<_Value, _Value, _HashFcn, _Identity<_Value>,
                    _EqualKey, _Alloc> _Ht;
  _Ht _M_ht;
  ...
};

// stl_hash_map.h

template <class _Key, class _Tp,
          class _HashFcn  __STL_DEPENDENT_DEFAULT_TMPL(hash<_Key>),
          class _EqualKey __STL_DEPENDENT_DEFAULT_TMPL(equal_to<_Key>),
          class _Alloc =  __STL_DEFAULT_ALLOCATOR(_Tp) >
class hash_map {
private:
  // 内部使用hashtable,_Select1st从pair中提取key
  typedef hashtable<pair<const _Key,_Tp>,_Key,_HashFcn,
                    _Select1st<pair<const _Key,_Tp> >,_EqualKey,_Alloc> _Ht;
  _Ht _M_ht;
  ...
};

hash_multiset和hash_multimap使用insert_equal,与hash_set/hash_map的区别相同


有序容器 vs 无序容器

时间复杂度对比

操作 有序容器(RBTree) 无序容器(HashTable)
查找 O(log n) O(1) 平均,O(n) 最坏
插入 O(log n) O(1) 平均
删除 O(log n) O(1) 平均
遍历(有序) O(n),自然有序 O(n),遍历后排序

选择建议

  1. 需要有序遍历 → 选择有序容器(set/map)
  2. 查找性能要求极高 → 选择无序容器(hash_set/hash_map)
  3. 需要范围查找(如lower_bound/upper_bound)→ 选择有序容器
  4. 自定义类型作为键 → 需要提供比较函数(有序)或哈希函数(无序)

SGI STL的设计模式总结

  1. 组件复用:set/map/multiset/multimap底层都是_Rb_tree,只是模板参数不同
  2. 函数对象适配_Identity_Select1st让同一套树结构适应不同的提取键需求
  3. hash容器同理:都基于hashtable
  4. insert_unique vs insert_equal:区分唯一键和可重复键容器的核心

总结

SGI STL的关联式容器设计体现了STL的核心思想:数据结构和算法分离,通过迭代器粘合

红黑树和哈希表分别是有序无序关联式容器的基石,
而set/map/multiset/multimap和hash_set/hash_map/hash_multiset/hash_multimap只是
相同的底层数据结构上,通过不同的模板参数(主要是KeyOfValue/ExtractKey)实现差异化

理解这一点后,STL关联式容器的实现就变得清晰了。

— 节选自侯捷《STL源码剖析》

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐