C++入门
C++类和对象(上)
C++类和对象(中)
C++类和对象(下)
C/C++内存管理
C++string类
C++vector类
C++list类
C++stack和queue
C++双端队列
C++模板进阶
C++IO流
C++中的继承
C++中的多态
在之前我们见到的都是序列式容器,比如:vector,list,deque,forward_list等,因为其底层为线性序列的数据结构,容器中存储的是元素本身。
关联式容器也是用来存储数据的,与序列式容器不同,其中存储的是
根据应用场景的不同,STL总共实现了两种不同结构的关联式容器:树型结构与哈希结构。 树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层,容器中的元素是一个有序的序列。 下面一依次介绍每一个容器。
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。 比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
void test6()
{
// 用数组array中的元素构造set
int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
set<int> s(array, array + sizeof(array) / sizeof(array[0]));
cout << s.size() << endl;
// 正向打印set中的元素,从打印结果中可以看出:set可去重
for (auto& e : s)
{
cout << e << " ";
}
cout << endl;
// 使用迭代器逆向打印set中的元素
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
cout << *it << " ";
}
cout << endl;
// set中值为3的元素出现了几次
cout << s.count(3) << endl;
}
结果如下:
set插入元素时遇到重复元素将不会插入,若想插入重复元素,可以用multiset。
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
map的构造:
void test2()
{
map<int, double> m;
//调用pari构造函数,构造一个匿名对象插入
m.insert(pair<int, double>(1, 1.1));
m.insert(pair<int, double>(2, 2.2));
//调用函数模板构造对象
m.insert(make_pair(3, 3.3));
map<int, double>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
}
map插入方式:
void test4()
{
string arr[] = { "苹果","橙子", "芒果", "西瓜", "西瓜", "芒果", "芒果", "苹果", "苹果", "芒果", "苹果", "苹果" };
map<string, int> fruitMap;
//插入方式1
//for (string& s : arr)
//{
// map::iterator pos = fruitMap.find(s);
// if (pos != fruitMap.end())
// {
// ++pos->second;
// }
// else
// {
// fruitMap.insert(make_pair(s, 1));
// }
//}
//插入方式2
//for (string& s : arr)
//{
// pair
// if (temp.second == false)
// {
// ++temp.first->second;
// }
//}
//插入方式3
for (string& s : arr)
{
++fruitMap[s];
}
for (auto& e : fruitMap)
{
cout << e.first << " " << e.second << endl;
}
}
operator[]的原理是:
用
如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
operator[]函数最后将insert返回值键值对中的value返回。
在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过key找到与key,对应的value然后返回其引用,不同的是:当key不存在时,operator[]用默认value与key构造键值对,然后插入,返回该默认value,at()函数直接抛异常。
同样的map也无法插入关键字(key)相同的元素,如果需要,可以使用multimap
这几个容器有个共同点是:
其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一颗AVL树具有以下性质:
它的左右子树都是AVL树
左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 ,搜索时
间复杂度O(logn )。
template<class T>
struct AVLTreeNode
{
AVLTreeNode(const T& data)
: _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
, _data(data), _bf(0)
{}
AVLTreeNode<T>* _pLeft; // 该节点的左孩子
AVLTreeNode<T>* _pRight; // 该节点的右孩子
AVLTreeNode<T>* _pParent; // 该节点的双亲
T _data;
int _bf; // 该节点的平衡因子
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
我们假设新插入节点为cur,其双亲节点为parent。在cur插入之前,parent的平衡因子可能为0,1,-1三种情况。根据cur插入方向的不同,可分为:
此时,parent的平衡因子可能有三种情况,0,±1,±2,我们进行逐个分析:
pair<Node*, bool> insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return make_pair(_root, true);
}
Node* parent = _root, * cur = _root;
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(cur, true);
}
}
cur = new Node(kv);
Node* newnode = cur;
if (kv.first > parent->_kv.first)
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
cur->_bf = 0;
//更新父节点到根结点的平衡因子
while (parent)
{
if (cur == parent->_left)
{
--parent->_bf;
}
else
{
++parent->_bf;
}
if (parent->_bf == 0)
{
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//不平衡,需要旋转
if (parent->_bf == -2)
{
if (cur->_bf == -1)
{
rotateR(parent);
}
else
{
rotateLR(parent);
}
}
else
{
if (cur->_bf == 1)
{
rotateL(parent);
}
else
{
rotateRL(parent);
}
}
break;
}
else
{
//平衡因子大于2,插入之前就不平衡了,可能其他地方出错了
assert(false);
}
}
return make_pair(newnode, true);
}
根据节点插入位置的不同,AVL树的旋转分为四种:
//右单旋
void rotateR(Node* root)
{
//根结点左孩子和根结点左孩子的右孩子
Node* rootL = root->_left;
Node* rootLR = rootL->_right;
Node* grandParent = root->_parent;
//右旋
root->_left = rootLR;
if (rootLR)
{
rootLR->_parent = root;
}
rootL->_right = root;
root->_parent = rootL;
if (grandParent == nullptr)
{
_root = rootL;
rootL->_parent = nullptr;
}
else
{
if (grandParent->_left == root)
{
grandParent->_left = rootL;
}
else
{
grandParent->_right = rootL;
}
rootL->_parent = grandParent;
}
root->_bf = 0;
rootL->_bf = 0;
}
新节点插入较高右子树的右侧—右右:左单旋
//左单旋
void rotateL(Node* root)
{
Node* rootR = root->_right;
Node* rootRL = root->_right->_left;
Node* grandParent = root->_parent;
rootR->_left = root;
root->_parent = rootR;
root->_right = rootRL;
if (rootRL)
{
rootRL->_parent = root;
}
if (grandParent == nullptr)
{
_root = rootR;
}
else
{
if (grandParent->_left == root)
{
grandParent->_left = rootR;
}
else
{
grandParent->_right = rootR;
}
}
rootR->_parent = grandParent;
root->_bf = 0;
rootR->_bf = 0;
}
//左右双旋
void rotateLR(Node* root)
{
Node* rootL = root->_left;
Node* rootLR = rootL->_right;
int bf = rootLR->_bf;
rotateL(rootL);
rotateR(root);
if (bf == -1)
{
rootLR->_bf = 0;
rootL->_bf = 0;
root->_bf = 1;
}
else if (bf == 1)
{
rootLR->_bf = 0;
rootL->_bf = -1;
root->_bf = 0;
}
else if (bf == 0)
{
rootLR->_bf = 0;
rootL->_bf = 0;
root->_bf = 0;
}
else
{
assert(false);
}
}
//右左双旋
void rotateRL(Node* root)
{
Node* rootR = root->_right;
Node* rootRL = rootR->_left;
int bf = rootRL->_bf;
rotateR(rootR);
rotateL(root);
//
if (bf == -1)
{
root->_bf = 0;
rootR->_bf = 1;
rootRL->_bf = 0;
}
else if (bf == 1)
{
root->_bf = -1;
rootR->_bf = 0;
rootRL->_bf = 0;
}
else if (bf == 0)
{
root->_bf = 0;
rootR->_bf = 0;
rootRL->_bf = 0;
}
else
{
assert(false);
}
}
总的来说,假设以parent为根的子树不平衡,平衡因子为2或-2:
当平衡因子为2时:说明右子树高,根据parent右子树的平衡因子的的不同,进行旋转:
右子树平衡因子为1,说明新增节点在右子树的右子树上,进行左单旋
右子树平衡因子为-1,说明新增节点在右子树的左子树上,进行左右双旋
当平衡因子为-2时:说明左子树高,根据parent左子树的平衡因子的的不同,进行旋转:
左子树平衡因子为-1,说明新增节点在左子树的左子树上,进行右单旋
左子树平衡因子为1,说明新增节点在左子树的右子树上,进行右左双旋
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
首先,按照二叉搜索树的规则插入新结点。
插入新结点后,判断红黑树的性质是否被破坏,大致分为以下情形:
首先假设当前结点为N,父结点为P,祖父结点为GP,叔叔结点U。
情形1: 若N为根结点,直接涂黑。
情形2: 父结点为黑色,直接插入即可。
情形3: P和U都为红,将P和U都涂黑,同时向上递归:
情形4: P为红,U为黑
情形4.1: P和N在同一边(都为父结点的左子树或右子树)
都为左子树时:
此时以祖父节点(GP)为支点进行右旋;然后将P涂黑,将GP涂红。
旋转后,P涂黑是因为要涂为原GP的黑色(往上兼容GP的父节点);而GP涂红则是因为右旋后,经过U的路径的黑色节点数量+1,涂红进行数量平衡。
都为右子树时:
此时以祖父节点(GP)为支点进行左旋;将P涂黑,将GP涂红。
情形4.2: P和N不在同一边
当P左N右时(P为左子树,N为右子树):
此时,以父节点§进行左旋,旋转后,以P作为新的平衡节点N,转至 [情形4.1.1 父N同左] 处理。
当P右N左时:
此时,以父节点§进行右旋,旋转后,以P作为新的平衡节点,此时再进行【情形4.1.2 父N同右】处理。
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( ),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。