STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有4种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(红黑树)作为其底层结构,容器中的元素是一个有序的序列。
1.map的介绍
- map是关联容器,它是按照特定的次序,即按照key值来比较,存储有键值key和值value组合而成的元素。
- 在map中,键值key通常用于排序和唯一标识元素,而值value中存储与键值key关联的内容。键值key和值value的类型可能不同,并且在map内部key与value通过成员类型value_type绑定在一起,为其取别名pair:typedef pair value_type;
- map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时可以得到一个有序的序列)
- map支持下标访问符,即在[]中放入key,就可以找到key对应的value。
- map通常被实现为平衡二叉搜索树(红黑树)
map的使用
其中:
key:键值对中key的类型。T:键值对中value的类型
Compare:比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)的参数不需要传递,如果无法比较是(自定义类型),需要用户自己显示传递比较规则(一般按照函数指针或仿函数来传递)。
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。
在使用map时,需加头文件#include<map>
1.1 map的构造
函数声明 | 功能介绍 |
map(const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type()); | 构造一个空的map |
| 用[first,last]区间中的元素构造map |
| map的拷贝构造 |
1.2 map的迭代器
函数声明 | 功能介绍 |
iterator begin() | 返回第一个元素的位置 |
iterator end() | 返回最后一个元素的下一个位置 |
const_iterator begin() const | 返回第一个元素的const迭代器 |
const_iterator end() const | 返回最后一个元素的const迭代器 |
reverse_iterator rbegin() | 返回第一个元素位置的反向迭代器即end |
reverse_iterator rend() | 返回最后一个下一个位置的方向迭代器即begin |
const_reverse_iterator rbegin() const | 返回第一个元素位置的const反向迭代器即end |
const_reverse_iterator rend() const | 返回最后一个元素下一个位置的反向迭代器即begin |
1.3 map的容量与元素访问
函数声明 | 功能介绍 |
bool empty() const | 检测map中的元素是否为空,是返回true,否则返回false |
size_type size() const | 返回map中有效元素的个数 |
mapped_type& operator[](const key_type& k) | 返回key对应的value |
注意:在元素访问时,有一个与operator[]类似的操作at()函数,该函数通过key找到与key对应的value然后返回其引用。当key不在map中时,operator[]用默认value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。
此外,map还有元素修改操作接口,如:insert、erase、swap、clear、find、count等
【map总结】
- map中元素是键值对,map中的key是唯一的,并且不能修改
- map默认按照小于的方式对key进行比较
- map中的元素如果用迭代器去遍历,可以得到一个有序的序列
- map的底层是用红黑树实现的,查找效率为O(logN)
- 支持[]操作符,operator[]实际进行插入查找
1.4 multimap
multimap和map的唯一不同在于:map中的key是唯一的,而multimap中key是可以重复的。所以multimap中没有重载operator[]操作符
2.set的介绍
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须唯一。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序
- set中通过key访问单个元素的速度通常比unordered_set容器慢,但set允许根据顺序对子集进行直接迭代。
- set在底层使用红黑树实现。
T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
set的使用
2.1 set的构造
2.2 set的迭代器
2.3 set的容量
multiset
1. multiset中再底层中存储的是<value, value>的键值对
2. mtltiset的插入接口中只需要插入即可
3. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
5. multiset中的元素不能修改
6. 在multiset中找某个元素,时间复杂度为O(log2 N)
7. multiset的作用:可以对元素进行排序
以上是map和set的简单介绍,下面进入主题,模式实现map和set
要实现map和set,我们需要先实现红黑树以及构建结点和迭代器
#pragma once
#include<iostream>
using namespace std;
enum Color
{
RED,
BLACK
};
template<class ValueType>
//构造红黑树结点
struct RBTreeNode
{
//ValueType,如果是map,则为pair<K,V>;如果是set,则为k
ValueType _Value;
RBTreeNode<ValueType>* _left;
RBTreeNode<ValueType>* _right;
RBTreeNode<ValueType>* _parent;
Color _color;
RBTreeNode(const ValueType& v)
: _Value(v)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _color(RED)
{}
};
//红黑树的迭代器
template<class ValueType>
class RBTreeIterator
{
typedef RBTreeNode<ValueType> Node;
typedef RBTreeIterator<ValueType> self;
public:
RBTreeIterator(Node* node)
:_node(node)
{}
RBTreeIterator(const self& node)
:_node(node._node)
{}
ValueType& operator*()
{
return _node->_Value;
}
ValueType* operator->()
{
return &_node->_Value;
}
self& operator==(const self& node)
{
return _node == node->_node;
}
bool operator!=(const self& node)
{
return _node != node._node;
}
self& operator++()
{
if (_node->_right)
{
Node* subR = _node->_right;
while (subR->_left)
{
subR = subR->_left;
}
_node = subR;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
_node = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
self& operator--()
{
if (_node->_left)
{
Node* subL = _node->_left;
while (subL->_right)
{
subL = subL->_right;
}
_node = subL;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
private:
Node* _node;//迭代器本质上是结点的指针
};
template<class K,class V,class KeyOfValue>
class RBTree
{
typedef V ValueType;
typedef RBTreeNode<ValueType> Node;
public:
typedef RBTreeIterator<ValueType> Iterator;
RBTree()
:_root(nullptr)
{}
Iterator Begin()
{
Node* cur = _root;
while (cur && cur->_left)
{
cur = cur->_left;
}
return Iterator(cur);
}
Iterator End()
{
return Iterator(nullptr);
}
pair<Iterator, bool> Insert(const ValueType& v)
{
if (nullptr == _root)
{
_root = new Node(v);
_root->_color = BLACK;
return make_pair(Iterator(_root), true);
}
KeyOfValue keyofvalue;
Node* cur = _root;
Node* parent = cur;
while (cur)
{
if (keyofvalue(v) < keyofvalue(cur->_Value))
{
parent = cur;
cur = cur->_left;
}
else if (keyofvalue(v) > keyofvalue(cur->_Value))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(Iterator(cur), false);
}
}
cur = new Node(v);
if (keyofvalue(v) < keyofvalue(parent->_Value))
{
cur->_parent = parent;
parent->_left = cur;
}
else
{
cur->_parent = parent;
parent->_right = cur;
}
Node* newnode = cur;
while (parent && RED == parent->_color)
{
//此时grandfather一定存在,因为parent存在,且颜色不是黑色,则parent一定不是根
Node* grandfather = parent->_parent;
//parent在左侧的情况
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && RED == uncle->_color)
{
//叔叔结点存在且为红色
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//叔叔结点不存在,或叔叔结点存在且为黑色
if (cur == parent->_right)
{
RotateL(parent);
swap(cur, parent);
}
grandfather->_color = RED;
parent->_color = BLACK;
RotateR(grandfather);
break;
}
}
//parent在右侧的情况
else
{
Node* uncle = grandfather->_left;
if (uncle && RED == uncle->_color)
{
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
RotateR(parent);
swap(parent, cur);
}
RotateL(grandfather);
grandfather->_color = RED;
parent->_color = BLACK;
break;
}
}
}
++_size;
_root->_color = BLACK;
return make_pair(Iterator(newnode), true);
}
Iterator Find(const K& key)
{
KeyOfValue keyofvalue;
Node* cur = _root;
while (cur)
{
if (keyofvalue(key) < keyofvalue(cur->_Value))
{
cur = cur->_left;
}
else if (keyofvalue(key) > keyofvalue(cur->_Value))
{
cur = cur->_right;
}
else
{
return Iterator(cur);
}
}
return Iterator(nullptr);
}
size_t Size()const
{
return _size;
}
bool Empty()const
{
return 0 == _size;
}
protected:
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
subR->_left = parent;
Node* pparent = parent->_parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else
{
pparent->_right = subR;
}
}
subR->_parent = pparent;
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
Node* pparent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root)
{
_root = subL;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
}
else
{
pparent->_right = subL;
}
}
subL->_parent = pparent;
}
private:
Node* _root;
size_t _size;
};
map的简易模拟实现
因为map底层结构是红黑树,因此在map中直接封装一颗红黑树,然后将其接口进行包装
#include"RBTree.h"
#include<string>
template<class K, class V>
class MyMap
{
public:
typedef pair<K, V> ValueType;
struct KeyOfValue
{
const K& operator()(const ValueType& kv)
{
return kv.first;
}
};
typedef typename RBTree<K, ValueType, KeyOfValue>::Iterator Iterator;
pair<Iterator, bool> Insert(const ValueType& v)
{
return tree.Insert(v);
}
V& operator[](const K& key)
{
pair<Iterator, bool> ret = tree.Insert(make_pair(key, V()));
return ret.first->second;
}
Iterator Begin()
{
return tree.Begin();
}
Iterator End()
{
return tree.End();
}
private:
RBTree<K, ValueType, KeyOfValue> tree;
};
void TestMyMap()
{
MyMap<string, string> dict;
dict.Insert(make_pair("apple", "苹果"));
dict.Insert(make_pair("water", "水"));
dict.Insert(make_pair("sister", "姐妹"));
MyMap<string, string>::Iterator it = dict.Begin();
while (it != dict.End())
{
cout << it->first<< " ";
++it;
}
cout << endl;
dict["sister"] = "姐姐";
it = dict.Begin();
while (it != dict.End())
{
cout << it->second << " ";
++it;
}
}
set的简易模拟实现
同样的道理,因为set底层结构是红黑树,因此在set中直接封装一颗红黑树,然后将其接口进行包装
#include"RBTree.h"
template<class K>
class MySet
{
typedef K ValueType;
struct KeyOfValue
{
const K& operator()(const ValueType& key)
{
return key;
}
};
public:
typedef typename RBTree<K, ValueType, KeyOfValue>::Iterator Iterator;
pair<Iterator, bool> Insert(const ValueType& key)
{
return tree.Insert(key);
}
Iterator Begin()
{
return tree.Begin();
}
Iterator End()
{
return tree.End();
}
Iterator Find(const K& key)
{
return tree.Find(key);
}
size_t Size()const
{
return tree.Size();
}
private:
RBTree<K, ValueType, KeyOfValue> tree;
};
void TestMySet()
{
MySet<int> s;
s.Insert(4);
s.Insert(2);
s.Insert(3);
s.Insert(1);
s.Insert(18);
s.Insert(15);
MySet<int>::Iterator it = s.Begin();
while (it != s.End())
{
cout << *it << " ";
++it;
}
cout << endl;
}
从上面的代码可以看出,map和set只是一层壳,里面只是封装了红黑树,通过传入KeyOfValue参数的不同决定是返回key还是pair中的first。这里很好的运用了模板和代码的复用思想。
关联式容器面试题
1.说说你所知道的容器有哪些?
- 顺序容器:array、vector、deque、list、forward-list
- 关联式容器(红黑树实现):set、multiset、map、multimap
- 无序容器(hash实现):hash_set、hash_multiset、hash_map、hash_multimap
2.map与set的区别?使用map有哪些优势?
map和set的区别在于:
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
map进行快速查找,通过key查找value,附带作用是对key进行排序。
3.map的底层原理,说一下红黑树?
4.map的迭代器会失效吗?什么情况下会失效?map迭代器在删除元素时会失效。详细解析请看大佬优秀博客:聊聊map和vector的迭代器失效问题
5.AVL树与红黑树相比,红黑树的优势在哪里?
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O($log_2 N$),红黑树不追求绝对平
衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删
的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
6.红黑树是如何插入和旋转的?
红黑树的插入和二叉搜索树的插入方法是一样的,但是插入节点默认是红色,然后根据红黑树的规则进行旋转和变色。