笔者在使用Python进行平常开发时,最经常使用的数据结构就是list和dict,其中dict在Python底层是基于hash_table来实现的,咱们今天就介绍一下对标dict的STL关联容器Map。html
在使用map时总让我有种在使用NoSQL的感受,由于说到底都是key-value,感受STL中的map能够对标LevelDB之类的普通NoSQL,倒更加的贴切。ios
结合C++11来讲,目前STL支持的map类型包括:map、multimap、unordered_map。虽然都是map,可是在使用和底层实现上会有一些差别,所以深刻理解一下才能更好将map用于平常开发工做中。面试
map的全部元素都是pair,同时具有key和value,其中pair的第一个元素做为key,第二个元素做为value。map不容许相同key出现,而且全部pair根据key来自动排序,其中pair的定义在以下:数组
template <typename T1, typename 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) { }
};复制代码
从定义中看到pair使用模板化的struct来实现的,成员变量默认都是public类型。bash
map的key不能被修改,可是value能够被修改,STL中的map是基于红黑树来实现的,所以能够认为map是基于红黑树封装了一层map的接口,底层的操做都是借助于RB-Tree的特性来实现的,再来进一步看下map的定义,以下(舒适提示:代码主要体现map与RB-Tree在实现上的交互,大体看懂便可):微信
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
typedef Key key_type; //key类型
typedef T data_type; //value类型
typedef T mapped_type;
typedef pair<const Key, T> value_type; //元素类型, const要保证key不被修改
typedef Compare key_compare; //用于key比较的函数
private:
//内部采用RBTree做为底层容器
typedef rb_tree<key_type, value_type,
identity<value_type>, key_compare, Alloc> rep_type;
rep_type t; //t为内部RBTree容器
public:
//iterator_traits相关
typedef typename rep_type::const_pointer pointer;
typedef typename rep_type::const_pointer const_pointer;
typedef typename rep_type::const_reference reference;
typedef typename rep_type::const_reference const_reference;
typedef typename rep_type::difference_type difference_type;
//迭代器相关
typedef typename rep_type::iterator iterator;
typedef typename rep_type::const_iterator const_iterator;
typedef typename rep_type::const_reverse_iterator reverse_iterator;
typedef typename rep_type::const_reverse_iterator const_reverse_iterator;
typedef typename rep_type::size_type size_type;
//迭代器函数
iterator begin() { return t.begin(); }
const_iterator begin() const { return t.begin(); }
iterator end() { return t.end(); }
const_iterator end() const { return t.end(); }
reverse_iterator rbegin() { return t.rbegin(); }
const_reverse_iterator rbegin() const { return t.rbegin(); }
reverse_iterator rend() { return t.rend(); }
const_reverse_iterator rend() const { return t.rend(); }
//容量函数
bool empty() const { return t.empty(); }
size_type size() const { return t.size(); }
size_type max_size() const { return t.max_size(); }
//key和value比较函数
key_compare key_comp() const { return t.key_comp(); }
value_compare value_comp() const { return value_compare(t.key_comp()); }
//运算符
T& operator[](const key_type& k)
{
return (*((insert(value_type(k, T()))).first)).second;
}
friend bool operator== __STL_NULL_TMPL_ARGS (const map&, const map&);
friend bool operator< __STL_NULL_TMPL_ARGS (const map&, const map&);
}复制代码
map的成员函数(部分经常使用)能够分为几类:数据结构
map的insert操做是很是常见的,而且插入操做不存在迭代器失效问题,其中根据insert的重载类型,能够支持多种插入方式,在C++11中增长了第四种重载函数,本文暂列举C++98的3种重载定义,先看下insert的几种定义:app
//single element (1)
pair<iterator,bool> insert (const value_type& val);
//with hint (2)
iterator insert (iterator position, const value_type& val);
//range (3)
template <class InputIterator>
void insert (InputIterator first, InputIterator last);复制代码
上述代码给出了3种插入方式分别是单元素pair、指定位置、范围区间,看下实际的例子:
less
//map::insert (C++98)
#include <iostream>
#include <map>
int main ()
{
std::map<char,int> mymap;
//first insert function version (single parameter):
//注意返回值 是两个 迭代器和是否成功
mymap.insert ( std::pair<char,int>('a',100) );
mymap.insert ( std::pair<char,int>('z',200) );
std::pair<std::map<char,int>::iterator,bool> ret;
ret = mymap.insert ( std::pair<char,int>('z',500) );
if (ret.second==false) {
std::cout << "element 'z' already existed";
std::cout << " with a value of " << ret.first->second << '\n';
}
// second insert function version (with hint position):
//因为map的key的有序性 插入位置对效率有必定的影响
std::map<char,int>::iterator it = mymap.begin();
mymap.insert (it, std::pair<char,int>('b',300)); // max efficiency inserting
mymap.insert (it, std::pair<char,int>('c',400)); // no max efficiency inserting
// third insert function version (range insertion):
//第三种重载 范围区间
std::map<char,int> anothermap;
anothermap.insert(mymap.begin(),mymap.find('c'));
// showing contents:
//迭代器遍历
std::cout << "mymap contains:\n";
for (it=mymap.begin(); it!=mymap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
std::cout << "anothermap contains:\n";
for (it=anothermap.begin(); it!=anothermap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}复制代码
清除操做也是很是重要的操做,而且存在迭代器失效问题,删除操做一样在C++98中有3个重载函数,定义以下:ide
void erase (iterator position);
size_type erase (const key_type& k);
void erase (iterator first, iterator last);复制代码
能够看到这三个函数分别支持:迭代器位置删除、指定key删除、迭代器范围删除,看下实际的例子:
// erasing from map
#include <iostream>
#include <map>
int main ()
{
std::map<char,int> mymap;
std::map<char,int>::iterator it;
// insert some values:
mymap['a']=10;
mymap['b']=20;
mymap['c']=30;
mymap['d']=40;
mymap['e']=50;
mymap['f']=60;
it=mymap.find('b');
mymap.erase (it); // erasing by iterator
mymap.erase ('c'); // erasing by key
it=mymap.find ('e');
mymap.erase ( it, mymap.end() ); // erasing by range
// show content:
for (it=mymap.begin(); it!=mymap.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}复制代码
交换操做能够实现两个相同类型的map的交换,即便map元素容量不一样,这个操做看着很神奇而且效率很高,能够想下是如何实现的,举个使用栗子:
// swap maps
#include <iostream>
#include <map>
int main () {
std::map<char,int> foo,bar;
foo['x']=100;
foo['y']=200;
bar['a']=11;
bar['b']=22;
bar['c']=33;
foo.swap(bar);
std::cout << "foo contains:\n";
for (std::map<char,int>::iterator it=foo.begin(); it!=foo.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
std::cout << "bar contains:\n";
for (std::map<char,int>::iterator it=bar.begin(); it!=bar.end(); ++it)
std::cout << it->first << " => " << it->second << '\n';
return 0;
}复制代码
代码输出:
foo contains:
a => 11
b => 22
c => 33
bar contains:
x => 100
y => 200复制代码
前面说了一些定义,如今介绍今天的重点内容Map与红黑树。
从定义能够看到map的定义中本质上是在内部实现了一棵红黑树,由于红黑树的增删改查的全部操做均可以在有时间保证的前提下完成,然而这些操做也正是map所须要的。
换句话说map应该提供的接口功能,红黑树也都有,从而map的全部操做都是内部转向调用RB-Tree来实现的。
提到红黑树感受很难很复杂而且离咱们平常开发很远,其实否则,红黑树不只是做为AVL的工程版本,在增长节点颜色、不严格平滑等特性实现了更高效的插入和删除。
更重要的是RB-Tree做为一种基础的数据结构,常常被用于构建其余对外的结构,咱们今天说的map以及STL的set底层都是基于红黑树的,因此要把RB-Tree当作是一种基础构造类型的数据结构。
SGI STL并无直接只用RB-Tree,而是对其进行了模板化处理以及增长一些有益节点,从而来更加高效的为STL中的容器服务,因此STL中使用的能够认为是变种的红黑树。
好比SGI STL针对RB-Tree采用了header技巧,header指向根节点的指针,与根节点互为对方的父节点,而且left和right指向左子树最小和右子树最大,如图所示:
这里引入一个常见的问题:
面试官:stl的map是基于红黑树实现的,那么为何要基于红黑树?
这个问题也算是高频问题,不少人上来就回答红黑树的各类好处,那确实也没错,可是这样也最多算答上来一半。
其实也是如此,没有对比就没有发言权,总说A好,不和BCD对比一下怎么知道?
map的场景本质上就是动态查找过程,所谓动态就是其中包含了插入和删除,而且数据量会比较大并且元素的结构也比较随意,而且都是基于内存来实现的,所以咱们就须要考虑效率和成本,既要节约内存又要提升调整效率和查找速度。
说到查找问题 必然有几种备选的数据结构不乏:
固然还有一些其余我可能不知道的,可是热门的基本都在这里了,那么一一来看进行优劣势分析:
在网上也并无这种为何不用跳表的对比,笔者试着想了一下:
对于这个对比必定要结合map的发明年代背景来讲,就好像咱们如今问10年前的人们为何不用微信、拼多多同样,由于那时候压根没有或者刚出来暂时没有推广等诸多缘由。
SGI STL是大约1994年左右开发的,然而跳跃链表是1990年左右由William Pugh发明的,也就是能够认为两者是同时期的产物,然而Redis是2005年以后的产物,再看看红黑树是1978年左右发明出现的,因此对发明STL的那帮大牛,最开始考虑跳跃链表的可能性很小。
在国外的网站上有求知者试着用跳表实现map可是性能和红黑树有必定的差距,另外跳表自己是几率平衡的,节点大小相比于红黑树更大,可是我以为这并非致命的问题,或许过些年STL就会出现基于跳表的实现版本,这个很难说,因此不能一棒子打死说必须是红黑树。
从上面我所知道的数据结构来看,选择红黑树有不少历史缘由和性能考虑、时代在进步,并不必定后续就不会出现基于Treap树堆和B树的map版本,或许如今就有公司或者大佬本身使用跳表、Treap、B树来实现map。
因此咱们不能教条地记忆stl使用红黑树的缘由,至于真正的缘由只有创造者知道,不要把推测当作结论,没有最好只有更好。