系列文章目录
STL源码剖析笔记——迭代器
STL源码剖析笔记——vector
STL源码剖析笔记——list
STL源码剖析笔记——deque、stack,queue
STL源码剖析笔记——Binary Heap、priority_queue
STL源码剖析笔记——AVL-tree、RB-tree、set、map、mutiset、mutimap
STL源码剖析笔记——哈希表、unordered_set、unordered_map、unordered_mutiset、unordered_mutimap
STL源码剖析笔记——仿函数(函数对象)
STL源码剖析笔记——适配器(adapters)
1. AVL-tree(平衡二叉树)
二叉搜索树(Binary Search Tree,BST)是一种常见的数据结构,具有以下性质:
1.对于任意节点 n,其左子树中所有节点的值都小于 n 的值。
2.对于任意节点 n,其右子树中所有节点的值都大于 n 的值。
左右子树也分别是二叉搜索树。在二叉搜索上进行的一些操作,如查找(Search)、插入(Insertion)、删除(Deletion)的时间复杂度是O(log n),但最坏情况下(如下图,退化为链)会变成 O(n)。
所以提出了平衡二叉搜索树(AVL-tree),在二叉搜索树的基础上加了一个平衡条件:任何节点的两个子树的高度最大差别为 1。平衡二叉搜索树(AVL-tree)在最好和最坏情况下的操作时间复杂度都是O(log n)。
在对avl树进行插入和删除操作时,有可能会破坏树的平衡状态,所以操作结束之后要将树重新调整为平衡状态。可以通过一个简单的技巧来实现:插入一个数据之后,从该节点往根节点找,找到最近的不平衡节点,然后从该不平衡节点朝插入的节点方向(包括不平衡节点)数三个节点,以这三个节点为根节点和根节点的两个子树,重新构建avl。
以下图为例:插入数据为10,向上找不平衡点;节点9平衡,节点6平衡,节点4不平衡,所以节点4是最近的不平衡点,在该路径上数三个节点,即为4,6,9,以6为根节点,4和9分别为子树,重新构建avl,最终树重新平衡。
2. RB-tree(红黑树)
红黑树是一种自平衡的二叉搜索树,它在插入和删除操作时会通过一系列的旋转和颜色调整来保持树的平衡,从而保证了在最坏情况下的查找、插入和删除操作的时间复杂度都是 O(log n),其中 n 是树中节点的数量。
红黑树具有以下规则:
1.每个节点要么是红色,要么是黑色。
2.根节点是黑色的。
3.叶子节点是黑色的。它们不存储任何数据,只是作为树结构的辅助。
4.不能有两个相连的红色节点,即红色节点不能相邻,每个红色节点的子节点都是黑色的。
5.从任意节点出发,到达其每个叶子节点的路径上的黑色节点数量必须相同,这被称为黑高度。这个性质保证了树的平衡。
基于这些规则,红黑树具有以下特性:
1.任意路径上的黑色节点数量相同,从而保持了平衡性。
2.红色节点的存在降低了整体高度(由于红色节点的存在,允许树的某些分支更深,而另一些分支较浅,从而在整体上降低了树的高度),减少了搜索的开销。
3.操作序列中的旋转次数相对较少,维护平衡的代价相对较低。
红黑树和AVL树都是自平衡二叉搜索树,用于在动态数据集上进行高效的插入、删除和搜索操作。下面是红黑树和AVL树的比较:
红黑树:红黑树保证了一种弱平衡,即树的高度不超过2log(n + 1)。这使得红黑树在插入和删除操作时需要更少的旋转操作,具有更高的灵活性。适用于在插入和删除操作较频繁、搜索操作相对较少的场景,例如在数据库索引中。
AVL树:AVL树是一种严格的平衡树,这确保了AVL树在平衡方面表现更好,这使得AVL树在搜索操作上更加高效,但在插入和删除操作时可能需要更多的旋转来维持平衡。适用于搜索操作频繁、插入和删除操作相对较少的场景,以及对于对树的平衡性要求较高的场景。
3. set
set的特性是所有元素都会根据元素的键值自动被排序,它的底层实现容器是RB-tree(红黑树),可以进行高效的插入和删除操作。set不允许两个元素有相同的键值。
可以通过set的迭代器改变set的元素值吗?不行,因为set元素值就是其键值,关系到set元素的排列规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set iterators是一种 const iterators。当客户端对set进行元素新增操作 (insert )或删除操作 (erase) 时,操作之前的所有迭代器,在操作完成之后都依然有效。但原来迭代器指向的节点可能已经不再包含有效数据。因此,对迭代器的解引用操作可能导致未定义行为。
set的操作基本上都是对底层红黑树操作的引用:
begin(); 返回set容器的第一个元素,其中反向遍历用rbegin()
end(); 返回set容器的最后一个元素,其中反向遍历用rend()
clear(); 删除set容器中的所有的元素
empty(); 判断set容器是否为空
max_size(); 返回set容器可能包含的元素最大个数
size(); 返回当前set容器中的元素个数
count(); 用来查找set中某个某个键值出现的次数。这个函数在set并不是很实用,因为一个键值在set只可能出现0或1次,这样就变成了判断某一键值是否在set出现过了。
erase(iterator); 删除定位器iterator指向的值
erase(first,second); 删除定位器first和second之间的值
erase(key_value); 删除键值key_value的值
insert(key_value); 插入元素
4. mutiset
multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复(即插入重复的值),因此它的插入操作采用的是底层机制RB-tree的 insert_equal() 而非 insert_unique()。
5. map
map的特性是所有元素都会根据元素的键值自动被排序。map的所有元素都是pair,同时拥有实值(value)和键值(key)。pair的第一元素被视为键值, 第二元素被视为实值。map不允许两个元素拥有相同的键值。
不能通过迭代器更改map的键值,但可以修改map的实值。map iterators既不是mutable iterators,也不是const iterators。当客户端对map进行元素新增操作 (insert )或删除操作 (erase) 时,操作之前的所有迭代器,在操作完成之后都依然有效。但原来迭代器指向的节点可能已经不再包含有效数据。因此,对迭代器的解引用操作可能导致未定义行为。
map的操作基本上都是对底层红黑树操作的引用:
begin() 返回指向 map 头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数
empty() 如果 map 为空则返回 true
end() 返回指向 map 末尾的迭代器
erase() 删除一个元素
find() 查找一个元素
insert() 插入元素
key_comp() 返回比较元素 key 的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向 map 尾部的逆向迭代器
rend() 返回一个指向 map 头部的逆向迭代器
size() 返回 map 中元素的个数
swap() 交换两个 map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素 value 的函数
6. mutimap
mutimap的特性以及用法和map完全相同,唯一的差别在于它允许键值重复(即插入重复的值),因此它的插入操作采用的是底层机制RB-tree的 insert_equal() 而非 insert_unique()。