最近刷题有用到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 */
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 <
结果如下:
虽然一般没怎么用 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 <
结果如下:
初始化之后就是一个基本的函数,比如begin(), end(), size()等等,没啥特殊的。直接看插入元素的实现。
主要就是想看看 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 内值不相等的特性。
// 迭代器删除,这个很容易理解,指哪儿删哪儿
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: " <