STL源码阅读——set 源码阅读

前言

最近刷题有用到set, 于是突然来了兴致,就想了解 STL 中set 的实现。说起set,立刻就能想到的有几点,set 底层用红黑树实现,set 是天然有序的,set 可以去重(不包含重复元素,当然 multiset 可以)。然而还是想对 set 的实现做个了解,比如到底是怎么插入删除的?侯捷大佬的 《STL源码剖析》很不错,菜鸡本鸡没有好好看。带着侯捷大佬的名言“源码面前无秘密”,看了一下 set 的源码,做个简单记录。主要关注 set 的插入删除数据,至于红黑树的实现,就不深究了。不同C++版本的实现的也不尽相同,不做一概而论。

源码位置:阅读源码首先需要知道源码位置在哪儿。stl源码一般存放在 include/.../bits/ 文件夹下。

Windows:用编译器直接就可以打开头文件的具体源码,不多说。

Linux:linux底下一般头文件都是存放在 /usr/include 底下,C++源码在 C++文件夹下,找到里面的bits文件夹。比如我的就是/usr/include/c++/5/bits。bits外面是C++的头文件,一般都是只做头文件包含,具体STL实现在bits文件夹下。

/** @file include/set
 *  This is a Standard C++ Library header.
 */

#ifndef _GLIBCXX_SET
#define _GLIBCXX_SET 1

#pragma GCC system_header

#include     // 红黑树实现
#include     // 主要 set 类定义已经方法实现,以下源码实现都是此文件中
#include     //multiset 
#include 

#ifdef _GLIBCXX_DEBUG
# include 
#endif

#ifdef _GLIBCXX_PROFILE
# include 
#endif

#endif /* _GLIBCXX_SET */

set基本定义

template,
	   typename _Alloc = std::allocator<_Key> >
    // 三个模板参数,第一个就是数据类型;第二个比较函数,仿函数,默认是less;
    // 第三个是分配器, 默认 std::allocator
    class set
    {
      // concept requirements
      typedef typename _Alloc::value_type                   _Alloc_value_type;
      __glibcxx_class_requires(_Key, _SGIAssignableConcept)
      __glibcxx_class_requires4(_Compare, bool, _Key, _Key,
				_BinaryFunctionConcept)
      __glibcxx_class_requires2(_Key, _Alloc_value_type, _SameTypeConcept)

    public:
      // typedefs:
      //@{
      /// Public typedefs.        typedef 定义一些别名,后面源码中以别名代替
      typedef _Key     key_type;
      typedef _Key     value_type;
      typedef _Compare key_compare;
      typedef _Compare value_compare;
      typedef _Alloc   allocator_type;

    private:
      typedef typename _Alloc::template rebind<_Key>::other _Key_alloc_type;

      typedef _Rb_tree,
		       key_compare, _Key_alloc_type> _Rep_type;
      _Rep_type _M_t;  // Red-black tree representing set.    // 红黑树别名

代码过多,只展示部分。一个容器的使用,首先要看容器的定义,然后是容器的使用方法。可知 set 有三个模板参数,第一个数据类型,第二个比较函数,第三个是分配器。比较函数都是放在 stl_function.h 文件中,less,greater 等等都是实现的仿函数,set 默认是 less。一般分配器就用std::allocator,就不自己再指定了,当然分配器也有其他实现,侯捷大佬有讲。

 /// One of the @link comparison_functors comparison functors@endlink.
  template    // less 函数为例
    struct less : public binary_function<_Tp, _Tp, bool>
    {
      bool    // 这代码写得就很规范,看着很舒服,该 const 的时候 const
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x < __y; }
    };

所以一个set定义应该如下所示:

/**
**
set ss1;                // 后两个用缺省默认类型
set> ss2;     // 指定比较函数, set 数据以升序存储
set> ss3;    // set 数据以降序存储
**
**/

// 举个栗子
#include 
#include 
using namespace std;

int main()
{
    set ss1;            // 使用默认比较函数
    set> ss2;    // 指定 less 为比较函数
    set> ss3;    // 指定greater 为比较函数
    for(int i=0;i<10;i++){
        ss1.insert(i);
        ss2.insert(i);
        ss3.insert(i);
    }
    cout <<"ss1: ";    // 迭代器遍历 set
    for(auto item : ss1) cout <

 结果如下:

STL源码阅读——set 源码阅读_第1张图片

set构造函数

虽然一般没怎么用 set 的其他构造方法,但看到了也还是记录一下:

	
  /**
   *  @brief  Default constructor creates no elements.
   */
  set()    // 默认无参构造函数
  : _M_t() { }

  /**
   *  @brief  Creates a %set with no elements.
   *  @param  __comp  Comparator to use.
   *  @param  __a  An allocator object.
   */
  explicit
  set(const _Compare& __comp,
  const allocator_type& __a = allocator_type())    // 显式指定比较函数,带参构造
  : _M_t(__comp, _Key_alloc_type(__a)) { }

  /**
   *  @brief  Builds a %set from a range.
   *  @param  __first  An input iterator.
   *  @param  __last  An input iterator.
   */
  template    // 区间构造 set
set(_InputIterator __first, _InputIterator __last)
: _M_t()
{ _M_t._M_insert_unique(__first, __last); }    // set 的构造即是红黑树的构造

  /**
   *  @brief  Builds a %set from a range.
   *  @param  __first  An input iterator.
   *  @param  __last  An input iterator.
   *  @param  __comp  A comparison functor.
   *  @param  __a  An allocator object.
   */
  template
set(_InputIterator __first, _InputIterator __last,
	const _Compare& __comp,
	const allocator_type& __a = allocator_type()) // 区间构造,带参比较函数,缺省分配器参数
: _M_t(__comp, _Key_alloc_type(__a))
	{ _M_t._M_insert_unique(__first, __last); }

  /**
   *  @brief  %Set copy constructor.
   *  @param  __x  A %set of identical element and allocator types.
   */
  set(const set& __x)                // 拷贝构造函数
  : _M_t(__x._M_t) { }

  /**
   *  @brief  %Set assignment operator.
   *  @param  __x  A %set of identical element and allocator types.
   */
  set&
  operator=(const set& __x)        // 重载 = 操作符,复制构造函数
  {
_M_t = __x._M_t;
return *this;
  }

以前也没深究过,没想到set 有这么多构造方式,直接上栗子吧:

// 举个栗子
#include 
#include 
using namespace std;

template 
struct mless : public less{
    bool operator()(const tp& val1, const tp& val2) const{
        return val1 > val2;
    }
};

int main()
{
    set ss1;            // 使用默认比较函数
    set> ss2;    // 指定greater 为比较函数
    
    mless comp;                // 自定义mless函数,将 < 写成 > 
    set> ss3(comp); // 带参比较函数构造,其实不懂为什么要有这个,比较函数在模板参数里就确定好了
    for(int i=0;i<10;i++){
        ss1.insert(i);
        ss2.insert(i);
        ss3.insert(i);
    }
    
    // 通过迭代器进行区间构造,两个 set 之间定义(模板)要一致,否则会报错
    // eg. set ss4(++ss2.begin(), ss2.end())  报错
    set ss4(++ss1.begin(), ss1.end()); 
    
    set> ss5(ss2);    // 拷贝构造函数,同样set定义要一致,模板一致
    set ss6 = ss4;                 // 赋值构造函数,要求同上
    
    cout <<"ss1: ";    // 迭代器遍历 set
    for(auto item : ss1) cout <

结果如下: 

STL源码阅读——set 源码阅读_第2张图片

初始化之后就是一个基本的函数,比如begin(), end(), size()等等,没啥特殊的。直接看插入元素的实现。 

set 元素插入

 主要就是想看看 set 的插入代码,如何实现,直接上源码分析:

// 直接进行值插入,最常用的插入方式
  std::pair
  insert(const value_type& __x)
  {
std::pair __p =
  _M_t._M_insert_unique(__x);
return std::pair(__p.first, __p.second);
  }
  

// 指定位置插入
  iterator
  insert(const_iterator __position, const value_type& __x)
  { return _M_t._M_insert_unique_(__position, __x); }


// 区间插入,这跟区间构造函数调用的同样的函数
	template
  void
  insert(_InputIterator __first, _InputIterator __last)
  { _M_t._M_insert_unique(__first, __last); }

//------------------------------------------------------------------------------
// 主要看 insert(val) 函数的 _M_insert_unique() 函数

template
#ifdef __GXX_EXPERIMENTAL_CXX0X__
    template
#endif
    pair::iterator, bool>
    _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
#ifdef __GXX_EXPERIMENTAL_CXX0X__    // 老版本函数名
    _M_insert_unique(_Arg&& __v)
#else
    _M_insert_unique(const _Val& __v)   // 新版本函数名
#endif
    {
      _Link_type __x = _M_begin();    // x 初始化为根节点
      _Link_type __y = _M_end();    // y 初始化为容器end
      bool __comp = true;
      while (__x != 0)
	{
	  __y = __x;    // y 用来记录 x
	  __comp = _M_impl._M_key_compare(_KeyOfValue()(__v), _S_key(__x)); // comp 比较函数
        // comp 默认为 less, v < _x 则 x = x->left; 否则 x = x->right; 
	  __x = __comp ? _S_left(__x) : _S_right(__x);
	}
      iterator __j = iterator(__y);    // 此时 x==NULL, y 为叶子节点
      if (__comp)    // 最后一次比较,如果 v < y
	{
	  if (__j == begin())    // 表示 y 即为根节点 或者 树为空, 此时满足条件,直接插入 
	    return pair
	      (_M_insert_(__x, __y, _GLIBCXX_FORWARD(_Arg, __v)), true);
	  else
	    --__j;    // 将 __j 置于 y 上一个位置,也即 __j < y
	}

    /** 
    ** 比较重要的一点,决定了等于无法插入;能插入情况应该是 __j < v < y
    ** 如果 __j < v 不满足,则说明此前 让comp(v, x)不满足的条件是 v==x, 而不是 v > x
    ** 以此筛除掉 相等的情况,不插入
    **/
      if (_M_impl._M_key_compare(_S_key(__j._M_node), _KeyOfValue()(__v)))
	return pair
	  (_M_insert_(__x, __y, _GLIBCXX_FORWARD(_Arg, __v)), true);
      return pair(__j, false);
    }

通过以上分析,发现相等的节点是无法插入到 set 里的,这里相等判定就是 !comp(a,b) && !comp(b,a) 。简单栗子:

#include 
#include 
using namespace std;

int main()
{
    set ss;
    for(int i=0;i<10;i++)
        ss.insert(1);
    cout <<"size: " <

 那如果想要插入重复的数值呢?可以用multiset,当然也可以自定义比较仿函数,自定义比较函数,自然想怎么定义就怎么定义,如下栗子。(甚至可以将set 变成无序,但这种做法破坏了 set 平衡二叉搜索树的特性。就是好玩而已,没有意义。)

// 在小于中加入 = 关系,这样可以让重复的数字插入。但是这样会造成一个问题,就是将 '=' 关系抹除了
// 这样的话,在等值 删除时就会出现问题;
// 这个栗子只是说明容器的比较函数很灵活,自定义程度很高。实际这种函数 不可取
template 
struct mless {    
    bool operator()(const tp& a, const tp& b) const{
        return a<=b;
    }
};

set删除之 erase

删除函数比较简单。首先几个删除接口 ,第一个和第三个,容器通用的删除方法,没啥好讲的,迭代器指哪儿删哪儿。主要看一个等值删除,这里就是用的  set 内值不相等的特性。

  // 迭代器删除,这个很容易理解,指哪儿删哪儿
  void
  erase(iterator __position)
  { _M_t.erase(__position); }

  // 等值删除,重点看这个,返回一个 size_type 类型
  size_type
  erase(const key_type& __x)
  { return _M_t.erase(__x); }
  
  // 区间删除
  void
  erase(iterator __first, iterator __last)
  { _M_t.erase(__first, __last); }



//--------------------------------------------------------
// 具体删除操作还是在 红黑树 实现文件里 stl_tree.h
template
// 指明 size_type 是一个 typename, 为了编译。
// 在有的时候为了 和 class区别开,也会手动指定typename,高速编译器,这是一个模板参数
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::size_type  
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
erase(const _Key& __x)
{
  pair __p = equal_range(__x);    
    // 这里用的是一个equal_range, 说明 multiset 的等值删除调用的也是这一个函数 
  const size_type __old_size = size();
  erase(__p.first, __p.second);    //  利用返回迭代器,进行区间删除
  return __old_size - size();    // 返回删除操作之后的容器大小
}


//-----------------------------------------------------------
// equal_range 函数 实现
template
pair::iterator,
 typename _Rb_tree<_Key, _Val, _KeyOfValue,
		   _Compare, _Alloc>::iterator>    // 返回参数为 迭代器对,即区间起始
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
equal_range(const _Key& __k)
{
  _Link_type __x = _M_begin();
  _Link_type __y = _M_end();
  while (__x != 0)
 {
    // 判断相等,还是之前说的 !comp(a, b) && !comp(b,a), 即为 a == b
  if (_M_impl._M_key_compare(_S_key(__x), __k))    
	__x = _S_right(__x);
  else if (_M_impl._M_key_compare(__k, _S_key(__x)))
	__y = __x, __x = _S_left(__x);
  else
	{
	  _Link_type __xu(__x), __yu(__y);
	  __y = __x, __x = _S_left(__x);
	  __xu = _S_right(__xu);
	  return pair(_M_lower_bound(__x, __y, __k),    // 这是是求 相等的下限
				_M_upper_bound(__xu, __yu, __k));    // 相等的上限
	}
 } 
  return pair(iterator(__y),
				  iterator(__y));
}

分析了删除的源码,看个简单的删除的栗子吧:

#include 
#include 
using namespace std;

int main()
{
    set ss;
    for(int i=0;i<10;i++)
        ss.insert(i);
    
    // 等值删除,删除数值 5,并输出操作之后的容器大小
    cout <<"size: " <

STL源码阅读——set 源码阅读_第3张图片

你可能感兴趣的:(C++)