STL容器介绍
-
概括:STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。
-
STL 容器种类和功能
- 序列容器:
vector 向量容器
、list 列表容器
、deque 双端队列容器
。——> 容器不是排序的。元素在容器中的位置同元素的值无关。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 - 排序容器:
set
集合容器、multiset
多重集合容器、map
映射容器以及multimap
多重映射容器。——> 元素默认是由小到大排序好的。插入元素会插入到适当的位置。 - 哈希容器:
unordered_set
哈希集合、unordered_multiset
哈希多重集合、unordered_map
哈希映射以及unordered_multimap
哈希多重映射。——> 元素是未排序的,元素的位置由哈希函数确定。
- 迭代器
-
常用的迭代器按功能强弱分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器 5 种。
- 前向迭代器(
forward iterator
):++p,p++,*p,被复制 or 赋值,只能使用 == 和 != 运算符比较,可以互相赋值
- 双向迭代器(
bidirectional iterator
):除“前向功能”之外,还可以进行 --p or p-- 操作(即一次向后移动一个位置)
- 随机访问迭代器(
random access iterator
):p+=i:使得 p 往后移动 i 个元素。
p-=i:使得 p 往前移动 i 个元素。
p+i:返回 p 后面第 i 个元素的迭代器。
p-i:返回 p 前面第 i 个元素的迭代器。
p[i]:返回 p 后面第 i 个元素的引用。
- 前向迭代器(
-
不同容器的迭代器
容器 对应的迭代器类型 array
、vector
、deque
随机访问迭代器 list
、set/multiset
、map/multimap
双向迭代器 forward_list
、unordered_map/unordered_multimap
、unordered_set/unordered_multiset
前向迭代器 stack
、queue
、priority_queue
不支持迭代器 注意:容器适配器
stack
和queue
没有迭代器,它们包含有一些成员函数,可以用来对元素进行访问。 -
迭代器的 4 种定义方式
迭代器定义方式 具体格式 正向迭代器 容器类名::iterator 迭代器名;
常量正向迭代器 容器类名::const_iterator 迭代器名;
反向迭代器 容器类名::reverse_iterator 迭代器名;
常量反向迭代器 容器类名::const_reverse_iterator 迭代器名;
常量迭代器
和非常量迭代器
的区别:除了用*迭代器名
访问迭代器指向的元素,非常量迭代器还能修改其指向的元素!反向迭代器
和正向迭代器
的区别:- 对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;
- 对反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素。
-
eg:支持随机访问迭代器的
vector
,支持双向迭代器的list
// 以下操作均允许: for (int i = 0; i < v.size(); ++i) cout << v[i] <<" "; // 像普通数组一样访问vector容器中的元素 vector<int>::iterator i; // 创建一个正向迭代器(其他 3 种定义迭代器的方式也支持) for (i = v.begin(); i != v.end(); ++i) // 用 != 比较两个迭代器 cout << *i << " "; for (i = v.begin(); i < v.end(); ++i) // 用 < 比较两个迭代器!【注意!】 cout << *i << " "; i = v.begin(); while (i < v.end()) { //间隔一个输出 cout << *i << " "; i += 2; // 随机访问迭代器支持 "+= 整数" 的操作 } /*--------- list(双向迭代器) ---------*/ // 以下操作允许: list<int>::const_iterator i; //创建一个常量正向迭代器(其他 3 种定义迭代器的方式也支持) for(i = v.begin(); i != v.end(); ++i) cout << *i; // 以下操作不允许!!! for(i = v.begin(); i < v.end(); ++i) cout << *i; // 双向迭代器不支持用 “<” 进行比较!!!【注意!】 for(int i=0; i<v.size(); ++i) cout << v[i]; // 双向迭代器不支持用下标随机访问元素:
-
- 补充:在
C++
中普通数组
也是容器。数组的迭代器 == 指针,而且 = 随机访问迭代器。例如,对于数组 int a[10],int * 类型的指针就是其迭代器( a、a+1、a+2 都是数组 a 的迭代器)。 - 注意:迭代器的功能是遍历容器,在遍历的同时可以访问(甚至修改)容器中的元素,但迭代器
不能用来初始化
空的容器。
array
-
创建并初始化:
array<double, 10> values; // 包含 10 个浮点型元素,但各个元素的值是不确定的(array 容器不会做默认初始化操作) array<double, 10> values {}; // 将所有的元素初始化为 0 或者和默认元素类型等效的值 array<double, 10> values {0.5,1.0,1.5,,2.0}; // 像创建常规数组那样进行初始化(只初始化了前4个,剩余元素都会被初始化为 0.0)
-
成员函数:
begin()/end()[or cbegin()/cend()]、rbegin()/rend()[or crbegin()/crend()]
for (auto first = values.begin(); first != values.end(); ++first) { cout << *first << " "; } for (auto first = values.rbegin(); first != values.rend(); ++first) { cout << *first << " "; }
底层实现
-
底层:
一段连续的线性内存空间(即:普通数组)
-
array
和普通数组
的联系和区别:
1. array 容器是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。
2. 和 C++ 普通数组存储数据方式一样,array 容器存储的所有元素一定会位于连续且相邻
的内存中。
3. 代替普通数组
,最直接的好处就是array
模板类中已经为我们写好了很多实用的方法,可以大大提高我们编码效率。例如,array
容器提供的at()
成员函数,可以有效防止越界操纵数组的情况;fill()
函数可以实现数组的快速初始化;swap()
函数可以轻松实现两个相同数组(类型相同,大小相同)中元素的互换。array<int, 5>a{1,2,3}; cout << &a[2] << " " << &a[0] + 2 << endl; //输出结果为: 004FFD58 004FFD58 strcpy(&a[0], "csdn"); strcpy(a.data(), "csdn"); // strcpy() 在拷贝字符串的同时,会自动在最后添加 '\0'
-
类型大小都相同的
array
容器:
1. 可以直接直接做赋值
2. 可以用任何比较运算符直接比较 -
总之,读者可以这样认为,array 容器就是普通数组的
“升级版”
,使用普通数组能实现的,使用 array 容器都可以实现,而且无论是代码功能的实现效率,还是程序执行效率,都比普通数组更高。
常用成员函数
- 访问元素
/*------- 访问单个 -------*/
array<int, 5> values{1,2,3,4,5};
values[4] = values[3] + 2.O*values[1]; // 通过 "容器名[]" 的方式
values.at (4) = values.at(3) + 2.O*values.at(1); // 避免越界访问
get<3>(values); // Output values[3],即:通过get<n> 模板函数获取到容器的第 n 个元素
*( values.data() + 1); // Output values[1],通过data()得到指向容器首个元素的指针
/*------- 访问多个 -------*/
// 利用for循环遍历访问
vector
-
创建并初始化
vector<double> values; // 空的 vector 容器,容器中没有元素,没有为其分配空间。 vector<int> values {2, 3, 5, 7, 11, 13, 17, 19}; // 创建的同时指定初始值以及元素个数 vector<double> values(20); // 开始时就有 20 个元素,它们的默认初始值都为 0。 vector<double> values(20, 1.0); // 为这20个元素可以指定一个其它值 // 利用圆括号()中的第2个参数 int num = 20; double value = 1.0; vector<double> values(num, value); vector<double>value2(values); // 用一对指针或者迭代器来指定初始值的范围 int array[] = {1,2,3,4,5,6}; vector<int>values(array, array + 4); // values 将保存{1,2,3,4} vector<int>value2(begin(values), begin(values)+3); // value2保存{1,2,3}
-
注意:vector 容器在
申请更多内存
的同时,容器中的所有元素可能会被复制或移动到新的内存地址
,这会导致之前创建的迭代器失效
!vector<int>values{1,2,3}; cout << "values 容器首个元素的地址:" << values.data() << endl; // values 容器首个元素的地址:0096DFE8 auto first = values.begin(); auto end = values.end(); values.reserve(20); // 增加 values 的容量!!! cout << "values 容器首个元素的地址:" << values.data() << endl; // values 容器首个元素的地址:00965560 /* first = values.begin(); // 重新初始化一遍 end = values.end(); */ while (first != end) { // 若不重新初始化,则运行直接崩溃! cout << *first; ++first; }
底层实现
-
底层:
一段连续的线性内存空间(即:普通数组)
使用3
个迭代器(可以理解成指针)来表示的://_Alloc 表示内存分配器,此参数几乎不需要我们关心 template <class _Ty, class _Alloc = allocator<_Ty>> class vector{ ... protected: pointer _Myfirst; pointer _Mylast; pointer _Myend; };
其中,
_Myfirst
指向的是vector
容器对象的起始字节位置;_Mylast
指向当前最后一个元素的末尾字节;_Myend
指向整个vector
容器所占用内存空间的末尾字节。如上图:通过这
3
个迭代器,就可以表示出一个已容纳 2 个元素,容量为 5 的vector
容器。-
将 3 个迭代器两两结合,还可以表达不同的含义,例如:
-
_Myfirst
和_Mylast
可以用来表示vector
容器中目前已被使用的内存空间
; -
_Mylast
和_Myend
可以用来表示vector
容器目前空闲的内存空间
; -
_Myfirst
和_Myend
可以用表示vector
容器的容量
。
-
-
对于空的 vector 容器,由于没有任何元素的空间分配,因此
_Myfirst
、_Mylast
和_Myend
均为null
。
-
-
扩容
的过程需要经历以下 3 步:——> 解释了vector
容器扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
1. 完全弃用现有的内存空间,重新申请更大的内存空间;
2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
3. 最后将旧的内存空间释放。
常用成员函数
-
访问元素(
补充部分
)/*------- 访问单个 -------*/ vector<int> values{1,2,3,4,5}; values.front() = 10; //修改首元素 values.back() = 20; //修改尾元素 cout << *(values.data() + 2) << endl; //输出容器中第 3 个元素的值 *(values.data() + 1) = 10; //修改容器中第 2 个元素的值 cout << *(values.data() + 1) << endl;
-
尾部添加一个元素(
emplace_back()
和push_back()
)vector<int> values{}; values.push_back(1); // == values.emplace_back(1);
emplace_back()
和 push_back()
的区别:就在于底层实现的机制不同。——> 后同!
-
push_back()
在实现时,首先创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);——> 先调用构造函数
,再调用移动构造函数
(若无移动构造,但有拷贝构造,则调用拷贝构造) -
emplace_back()
在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。——> 只调用一个构造函数
——>emplace_back()
的执行效率比push_back()
高! -
指定位置插入一个或多个元素(
insert()
和emplace()
)区别:
emplace()
在插入元素时,是在容器的指定位置直接构造元素,而不是像insert()
那样先单独生成,再将其复制(或移动)到容器中。——> 在实际使用中,推荐优先使用emplace()
。注意:emplace() 每次只能插入一个元素,而不能是多个!!!
-
删除元素
函数 功能 pop_back()【常和swap()搭配】
先调用 swap() 函数交换要删除的** 目标元素
和容器最后一个元素
的位置,然后使用 pop_back() 删除该目标元素
**。erase(pos)
删除指定位置** pos
**处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。erase(begin,end)
删除位于迭代器 ** [beg,end)
**指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。remove() 【常和erase(begin,end)搭配】
删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量。 clear()
删除容器中所有的元素,使其变成空。该函数会改变大小( size
变为 0),但不改变其容量(capacity
)。vector<int>demo{ 1,2,3,4,5 }; //交换要删除元素和最后一个元素的位置 swap(*(std::begin(demo)+1), *(std::end(demo)-1)); //等同于 swap(demo[1],demo[4]) demo.pop_back(); // 删除目标元素 //交换要删除元素和最后一个元素的位置 auto iter = std::remove(demo.begin(), demo.end(), 3); demo.erase(iter, demo.end()); // 删掉这些 "无用" 的元素,减小其容器大小
注意:
erase
方法不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效
(list
除外),所以不能使用erase(iter++)
的方式,但是erase
方法的返回值是下一个有效迭代器
,故可以如下使用:iter = demo.erase(iter);
2.16
- 如何避免
vector
容器进行不必要的扩容?——> 利用reserve()
函数- 参考网址:https://blog.csdn.net/qq_41895747/article/details/103977885
2.17
-
vector swap()
成员方法还可以这样用!——> 能够去除上一节的多余容量
问题-
参考网址:https://blog.csdn.net/weixin_42510222/article/details/117272496
-
可以套用如下的语法格式:
vector(x).swap(x);
其中x
指当前要操作的容器。该代码执行流程可细分为以下 3 步:-
先执行
vector(x)
,此表达式会调用vector
模板类中的拷贝构造函数,从而创建出一个临时的vector
容器(后续称其为tempvector
)。值得一提的是,tempvector
临时容器并不为空,因为我们将x
作为参数传递给了拷贝构造函数,该函数会将x
容器中的所有元素拷贝一份,并存储到tempvector
临时容器中。尤其注意:vector 模板类中的拷贝构造函数
只会为拷贝的元素分配存储空间
。即:tempvector
临时容器中没有空闲的存储空间
,其容量等于存储元素的个数。 -
然后借助
swap()
成员方法对tempvector
临时容器和x
容器进行调换,此过程不仅会交换2
个容器存储的元素,还会交换它们的容量。即:经过swap()
操作,x
容器具有了tempvector
临时容器存储的所有元素和容量,同时tempvector
也具有了原x
容器存储的所有元素和容量。 -
当整条语句执行结束时,临时的
tempvector
容器会被销毁,其占据的存储空间都会被释放。注意,这里释放的其实是原x
容器占用的存储空间。
-
-
同理:如何清空
vector
的容量?答:vector().swap(x);
——> 中间会生成一个空的vector
的临时对象。
-
2.18(关键)
-
切忌,
vector<bool>
不是存储bool
类型元素的vector
容器!——> 尽量避免使用vector<bool>
,改用deque<bool>
orbitset
orvector<int>
来代替。 -
原因:
1. 严格意义上讲,vector<bool>
并不是一个 STL 容器;
2.vector<bool>
底层存储的并不是 bool 类型值。 -
替代方案:
- 那么,如果在实际场景中需要使用
vector<bool>
这样的存储结构,该怎么办呢?很简单,可以选择使用deque<bool>
或者bitset
来替代vector<bool>
。
- 那么,如果在实际场景中需要使用
-
分析:
对于是否为 STL 容器,C++ 标准库中有明确的判断条件,其中一个条件是:如果
cont
是包含对象T
的STL
容器,且该容器中重载了[ ]
运算符(即支持operator[]
),则以下代码必须能够被编译:T *p = &cont[0];
所以,若
vector<bool>
是一个 STL 容器,则下面这段代码是可以通过编译的:// 创建一个 vector<bool> 容器 vector<bool>cont{0,1}; // 试图将指针 p 指向 cont 容器中第一个元素 bool *p = &cont[0]; // 很遗憾!该代码无法通过编译!
解答:
vector<bool>
底层采用了独特的存储机制:为了节省空间,vector<bool>
底层在存储各个bool
类型值时,每个bool
值都只使用一个比特位(二进制位)
来存储。即:在vector<bool>
底层一个字节可以存储 8 个 bool 类型值
。在这种存储机制的影响下,operator[ ]
势必就需要返回一个指向单个比特位
的引用,但显然这样的引用是不存在
的,因为bool
类型一般占用一个字节
长度,所以返回的应该是指向单个字节
的引用,等号左右两边出现冲突
!—> 同样对于指针
来说,其指向的最小单位是字节
,无法
另其指向单个比特位
。—>vector<bool>
并不是一个 STL 容器!
deque
-
创建并初始化
// 拷贝普通数组,创建deque容器 int a[] = { 1,2,3,4,5 }; std::deque<int> d(a, a + 5); // 适用于所有类型的容器 std::array<int, 5> arr{ 11,12,13,14,15 }; std::deque<int> d(arr.begin()+2, arr.end()); //拷贝arr容器中的{13,14,15}
-
当向
deque
容器添加元素时,deque
容器会直接申请更多的内存空间,同时其包含的所有元素可能会被复制或移动
到新的内存地址(原来占用的内存会释放),这会导致之前创建的迭代器失效
。deque<int>d; d.push_back(1); auto first = d.begin(); cout << *first << endl; d.push_back(1); //添加元素,会导致 first 失效 cout << *first << endl; // 报错!!!需要在之前加上:first = d.begin();等重新生成迭代器的操作!
底层实现
-
底层:一个中央控制器(
map数组
)和多个缓冲区(不同区域的连续空间
) -
和
vector
容器采用连续的线性空间不同,deque
容器存储数据的空间是由一段一段等长
的连续空间构成,各段
空间之间并不一定是连续的
,可以位于在内存的不同区域。为了管理这些位于不同区域的连续空间
,deque 容器用数组(数组名假设为map
)存储着各个不同区域的连续空间的首地址
。也就是说,map 数组中存储的都是指针
,指向那些真正用来存储数据的各个连续空间
。通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”
的效果。换句话说,当 deque 容器需要在头部或尾部
增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾
添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部
。问:如果 map 数组满了怎么办?答:再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。
-
deque
虽然也提供随机访问的迭代器,但是其迭代器并不是普通的指针,其复杂程度比vector高很多,因此除非必要,否则一般使用vector而非deque。deque迭代器的“++”、“--”
操作是远比vector迭代器繁琐,其主要工作在于缓冲区边界,如何从当前缓冲区跳到另一个缓冲区。 -
除了维护 map 数组,还需要维护 start、finish 这 2 个 deque 迭代器。以下为 deque 容器的定义:
// _Alloc为内存分配器 template<class _Ty, class _Alloc = allocator<_Ty>> class deque{ ... protected: iterator start; iterator finish; map_pointer map; ... } // deque的迭代器数据结构如下: struct __deque_iterator { ... T* cur; // 迭代器所指缓冲区当前的元素 T* first; // 迭代器所指缓冲区第一个元素 T* last; // 迭代器所指缓冲区最后一个元素 map_pointer node; // 指向map中的node ... }
其中,start 迭代器记录着 map 数组中首个连续空间的信息,finish 迭代器记录着 map 数组中最后一个连续空间的信息。
需要注意:
和普通 deque 迭代器不同,start 迭代器中的 cur 指针指向的是连续空间中首个元素;而 finish 迭代器中的 cur 指针指向的是连续空间最后一个元素的下一个位置。
-
deque
和vector
的最大差异:-
deque
允许于常数时间内对头端
进行元素的插入或移除操作(因为它不像vector
一样把所有对象保存在一个连续的内存块,而是多个连续的内存块。并且在一个映射结构中保存对这些块以及顺序的跟踪)
-
deque
没有capacity(空间、容量)
观念,因为它是动态地以分段连续空间
组合而成,可以随时增加一段新的空间并链接起来(无需重新分配空间,而是随时进行原地扩充,没有固定的capacity
)。换句话说,像vector
那样“因旧空间不足而【重新配置】一块更大空间,然后复制元素,再释放旧空间”
这样的事情在deque
中是不会发生的。也因此,deque
没有必要提供所谓的空间预留(reserved)
功能。 -
deque
相较于vector
复杂度很高,所以例如对deque
进行排序操作,为了最高效率,可将deque
先完整复制到一个vector
身上,将vector
排序后(利用STL
的sort
算法),再复制回deque
。——> 常用技巧!!!
-
-
注意:
deque
容器中存储元素并不能保证所有元素都存储到连续的内存空间中。
常用成员函数
-
访问元素(
补充部分
)deque<int> d{ 1,2,3,4,5 }; d.front() = 10; //修改首元素 d.back() = 20; //修改尾元素
注意:和
vector
容器不同,deque
容器没有提供data()
成员函数,同时deque
容器在存储元素时,也无法保证其会将元素存储在连续的内存空间中,因此尝试使用指针
去访问deque
容器中指定位置处
的元素,是非常危险的。 -
添加和删除元素
// ------- insert 用法 deque<int> d{ 1,2 }; //第一种格式用法 d.insert(d.begin() + 1, 3); //{1,3,2} //第二种格式用法 d.insert(d.end(), 2, 5); //{1,3,2,5,5} //第三种格式用法 array<int, 3> test{ 7,8,9 }; d.insert(d.end(), test.begin(), test.end()); //{1,3,2,5,5,7,8,9} //第四种格式用法 d.insert(d.end(), { 10,11 }); //{1,3,2,5,5,7,8,9,10,11}
list
-
又称:双向链表容器。第一个元素的前向指针总为
null
,因为它前面没有元素;同样,尾部元素的后向指针也总为null
。 -
缺点:不能像
array
和vector
那样,通过位置直接访问元素。要访问list
容器中的第 6 个元素,它不支持容器对象名[6]
这种语法格式,正确的做法:从容器中第一个
元素或最后一个
元素开始遍历
容器,直到找到
该位置。 -
应用场景:对序列进行大量添加或删除元素的操作(时间复杂度为
O(1)
),而直接访问元素的需求却很少的情况。
-
创建并初始化
//拷贝普通数组,创建list容器 int a[] = { 1,2,3,4,5 }; std::list<int> values(a, a+5); //拷贝其它类型的容器,创建 list 容器 std::array<int, 5>arr{ 11,12,13,14,15 }; std::list<int>values(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
-
注意:
1. 遍历容器时比较迭代器之间的关系,用的是!= 运算符
,因为它不支持 < 等运算符
。(list
使用双向迭代器,而不是前面容器的随机访问迭代器)
2.list
容器在进行插入(insert()
)、接合(splice()
)等操作时,都不会
造成原有的list
迭代器失效,甚至进行删除操作时,也只有指向被删除
元素的迭代器失效
,其他迭代器不受
任何影响。
底层实现
-
list
容器的底层实现:双向循环链表 ——>list 容器实际上就是一个带有头节点的双向循环链表
其中:
a)
为双向链表和b)
双向循环链表。注意:使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持。
-
list
和vector
、和deque
的区别:
1.list
不再能够像vector
一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;
2.list
不像vector
那样有可能在空间不足时做重新配置、数据移动的操作,所以插入操作都不会造成原有的list
迭代器失效;
3.list
是一个环状双向链表,所以它只需要一个指针
;
常用成员函数
-
添加(插入)元素方法
// ----- insert() 用法 std::list<int> values{ 1,2 }; //第一种格式用法 values.insert(values.begin() , 3); //{3,1,2} //第二种格式用法 values.insert(values.end(), 2, 5); //{3,1,2,5,5} //第三种格式用法 std::array<int, 3>test{ 7,8,9 }; values.insert(values.end(), test.begin(), test.end()); //{3,1,2,5,5,7,8,9} //第四种格式用法 values.insert(values.end(), { 10,11 }); //{3,1,2,5,5,7,8,9,10,11} // ----- splice() 用法:将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。 //创建并初始化 2 个 list 容器 list<int> mylist1{ 1,2,3,4 }, mylist2{10,20,30}; list<int>::iterator it = ++ mylist1.begin(); //指向 mylist1 容器中的元素 2 //调用第一种语法格式 mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4 // mylist2: // it 迭代器仍然指向元素 2 //调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处 mylist2.splice(mylist2.begin(), mylist1, it); // mylist1: 1 10 20 30 3 4 // mylist2: 2 // it 迭代器仍然指向元素 2 //调用第三种语法格式,将 [mylist1.begin(),mylist1.end())范围内的元素移动到 mylist2.begin() 位置处 mylist2.splice(mylist2.begin(), mylist1, mylist1.begin(), mylist1.end()); //mylist1: //mylist2: 1 10 20 30 3 4 2
补充:
splice()
成员方法移动元素的方式:将存储该元素的节点从list
容器底层的链表中摘除
,然后再链接
到当前list
容器底层的链表中。故:当使用splice()
成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中删除
。 -
对于
vector
、deque
、list
都提供了empty()
成员方法和size()
成员方法,都可用来判断容器是否为空。// 如下代码等价: if(cont.size() == 0) if(cont.empty())
问题:实际场景推荐使用哪一种呢?
——> 答:
建议使用 empty() 成员方法。
因为无论是哪种容器,使用此方法都可以保证在O(1)
时间复杂度内完成对“容器是否为空”
的判断;但对于list
容器来说,使用size()
成员方法判断“容器是否为空”
,可能要消耗O(n)
的时间复杂度。—> 该结论还适用之后更多容器!!!——> 原因:和
list
模板类提供了独有的
splice()
成员方法有关。链表的优势在于插入很快(无需经过拷贝就可以直接链接到其它链表中,且整个过程只需要消耗O(1)
的时间复杂度),所以设计将splice()
方法的时间复杂度设计为O(1)
。但此时size()
的执行效率就不可能达到O(1)
,因为需要添加更新size
变量(直接返回给size()
方法)的代码,但更新方式无疑是通过遍历链表
来实现的,而遍历的复杂度一般为O(n)
。 -
删除元素
// unique() : 删除容器中相邻的重复元素,只保留一份。 static bool cmp_1(double a, double b) { return (int)a == (int)b; } class cmp_2 { public: bool operator()(double a, double b) { // 函数对象参数列表不能用auto return (int)a == (int)b; } }; list<double> mylist{ 1,1.2,1.2,3,4,4.5,4.6 }; //删除相邻重复的元素,仅保留一份 mylist.unique(); // {1, 1.2, 3, 4, 4.5, 4.6} // 重定义规则:lamba函数、函数对象、函数指针 mylist.unique([](const auto& a, const auto& b) { // {1, 3, 4} return (int)a == (int)b; }); mylist.unique(cmp_1); // {1, 3, 4} mylist.unique(cmp_2()); // {1, 3, 4} // remove_if() : 删除容器中满足条件(重定义规则:lamba函数、函数对象、函数指针)的元素。 std::list<int> mylist{ 15, 36, 7, 17, 20, 39, 4, 1 }; // 删除 mylist 容器中能够使 lamba 表达式成立的所有元素。 mylist.remove_if([](int value) {return (value < 10); }); // {15 36 17 20 39} // 其他两种方式此处不再列出,同上
补充:forward_list(单链表)
-----------------------
有序
关联式容器的底层:「红黑树」
pair
-
创建并初始化
// 1) 默认构造函数,即创建空的 pair 对象 pair(); // 2) 直接使用 2 个元素初始化成 pair 对象 pair (const first_type& a, const second_type& b); // 3) 拷贝(复制)构造函数,即借助另一个 pair 对象,创建新的 pair 对象 template<class U, class V> pair (const pair<U,V>& pr); // 4) 移动构造函数 template<class U, class V> pair (pair<U,V>&& pr); // 5) 使用右值引用参数,创建 pair 对象 template<class U, class V> pair (U&& a, V&& b); // 调用构造函数 1),也就是默认构造函数 pair <string, double> pair1; // 调用第 2) 种构造函数 pair <string, string> pair2("STL教程","http://stl/"); // 调用第 3) 种,拷贝(复制)构造函数 pair <string, string> pair3(pair2); // 调用第 4) 种,移动构造函数 pair <string, string> pair4(make_pair("C++教程", "http://cplus/")); // 调用第 5) 种构造函数(参数是:匿名 string 对象【临时对象】) pair <string, string> pair5(string("Python教程"), string("http://python/")); // 补充:手动为 pair1 对象【赋值】 pair1.first = "Java教程"; pair1.second = "http:///java/";
创建
pair4
对象时,调用了make_pair()
函数,返回值(是一个临时对象
)作为参数传递给pair()
构造函数时,其调用的是移动构造函数
,而不是拷贝构造函数
。——> 得到如下等价形式:pair <string, string> pair4 = make_pair("C++教程", "http://cplus/"); pair <string, string> pair4(make_pair("C++教程", "http://cplus/"));
-
pair
运算规则:对于进行比较的 2 个pair
对象,先比较pair.first
元素的大小,如果相等则继续比较pair.second
元素的大小。注意:对于进行比较的 2 个 pair 对象,其对应的键和值的
类型必须相同
,否则将没有可比性!
常用成员函数
-
交换成员函数
swap()
pair <string, int> pair1("pair", 10); pair <string, int> pair2("pair2", 20); // 交换 pair1 和 pair2 的键值对。前提:这 2 个 pair 对象的键和值的类型要相同!!! pair1.swap(pair2);
map
-
默认情况下,
map
容器选用less<T>
排序规则(其中T
表示键的数据类型),其会根据键的大小对所有键值对做升序
排序。 -
注意:使用 map 容器存储的各个键值对,键的值既不能重复也不能被修改(键的类型会用
const
修饰)。 -
创建并初始化
map<string, int>myMap{ make_pair("C语言教程",10),make_pair("STL教程",20) }; // 通过调用 map 容器的拷贝(复制)构造函数,即可成功创建一个和 myMap 完全一样的 newMap 容器。 map<string, int>newMap(myMap); // 当有临时的 map 对象作为参数,传递给要初始化的 map 容器时,此时就会调用 移动构造函数。 map<string, int> disMap() { map<string, int> temp{ {"C语言教程",10},{"STL教程",20} }; return temp; } map<string, int>newMap_2(disMap()); // 修改默认排序规则创建 map<string, int, less<string> >myMap_2{ {"C语言教程",10},{"STL教程",20} }; map<string, int, greater<string> >myMap_3{ {"C语言教程",10},{"STL教程",20} };
-
map
容器配备的是双向迭代器(bidirectional iterator)
。所以map
容器迭代器只能进行++p、p++、--p、p--、*p
操作,并且迭代器之间只能使用 == 或者 != 运算符
进行比较。 -
获取键对应值
-
利用
[ ]
运算符直接访问数组中元素重要注意:
只有当
map
容器中确实存有包含该指定键的键值对,借助重载的[ ]
运算符才能成功获取该键对应的值;反之,若当前
map
容器中没有包含该指定键的键值对,则此时使用[ ]
运算符将不再是访问容器中的元素,而变成了向该map
容器中增添一个键值对
。其中,该键值对的键
用[ ]
运算符中指定的键,其对应值
取决于map
容器规定键值对中值的数据类型:若是基本数据类型
,则值为0
;若是string
类型,其值为""
【空字符串
】(即使用该类型的默认值
作为键值对的值)。 -
使用
at()
成员方法【推荐!既简单又安全!】注意点:与
[ ]
的不同之处在于,如果在当前容器中查找失败
,该方法不会向容器中添加新的键值对,而是直接抛出out_of_range
异常。
-
常用成员函数
-
【迭代器有关】
成员函数成员函数
功能
find(key)
在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
lower_bound(key)
返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
upper_bound(key)
返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
equal_range(key)
该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对(map 容器键值对唯一,因此该范围最多包含一个键值对)。
count(key)
在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。
-
lower_bound(key)
和upper_bound(key)
成员方法:——> 更多用于multimap
容器,map
容器很少用到。lower_bound(key)
返回的是指向第一个键不小于(>=)
key
的键值对的迭代器;upper_bound(key)
返回的是指向第一个键大于(>)
key
的键值对的迭代器;
-
equal_range(key)
成员方法:——> 更多用于multimap
容器,因为map
容器键值对唯一,因此该范围最多包含一个键值对(即:=key的那个键值对)
!该方法可以看做是
lower_bound(key)
和upper_bound(key)
的结合体,该方法会返回一个pair
对象,其中的 2 个元素都是迭代器类型,其中pair.first
实际上就是lower_bound(key)
的返回值,而pair.second
则等同于upper_bound(key)
的返回值。map<string, string>myMap_1{ {"STL教程","http://c.biancheng.net/stl/"}, {"C语言教程","http://c.biancheng.net/c/"}, {"Java教程","http://c.biancheng.net/java/"} }; //创建一个 pair 对象,来接收 equal_range() 的返回值 pair <map<string, string>::iterator, map<string, string>::iterator> myPair = myMap_1.equal_range("C语言教程"); //通过遍历,输出 myPair 指定范围内的键值对 for (auto iter = myPair.first; iter != myPair.second; ++iter) { cout << iter->first << " " << iter->second << endl; } // 输出:C语言教程 http://c.biancheng.net/c/
-
insert()
成员方法:- 无需指定插入位置,直接插入
// 1、引用传递一个键值对 pair<iterator,bool> insert (const value_type& val); // 2、以右值引用的方式传递键值对 template <class P> pair<iterator,bool> insert (P&& val);
其中:
val
参数表示键值对变量,同时该方法会返回一个pair
对象,其中pair.first
表示一个迭代器,pair.second
为一个bool
类型变量:- 如果成功插入
val
,则该迭代器指向新插入的val
,bool
值为true
; - 如果插入
val
失败,则表明当前map
容器中存有和val
的键相同的键值对(该键值对用p
表示),此时返回的迭代器指向p
,bool
值为false
。
//创建一个空 map 容器 map<string, string> mymap; //创建一个真实存在的键值对变量 pair<string, string> STL = { "STL教程","http://c.biancheng.net/stl/" }; //创建一个接收 insert() 方法返回值的 pair 对象 pair<map<string, string>::iterator, bool> ret; //插入 STL,由于 STL 并不是临时变量!因此会以第一种方式传参 ret = mymap.insert(STL); //插入失败样例(已存在STL的键值对) ret = mymap.insert(STL); //以右值引用的方式传递临时的键值对变量 ret = mymap.insert({ "C语言教程","http://c.biancheng.net/c/" }); //该插入方式的等价形式如下: //等价形式-1、调用 pair 类模板的构造函数 ret = mymap.insert(pair<string,string>{ "C语言教程","http://c.biancheng.net/c/" }); //等价形式-2、调用 make_pair() 函数(推荐!安全又效率高!) ret = mymap.insert(make_pair("C语言教程", "http://c.biancheng.net/c/")); //等价形式-3、利用 value_type(迭代器所指对象的型别) ret = mymap.insert(map<string, string>::value_type("C语言教程", "http://c.biancheng.net/c/")); // 打印结果: cout << ret.first->first << '\t' << ret.first->second << endl; //等价形式-4、利用下标插入(不安全) mymap["C语言教程"] = "http://c.biancheng.net/c/"
重要结论:
- 插入
成功
,则返回的pair
对象中包含一个指向新插入
的键值对的迭代器和值为1
的bool
变量 - 插入
失败
,也会返回一个pair
对象,其中包含一个指向map
容器中键为已存在
的键值对和值为0
的bool
变量。 - 无论是
局部定义
的键值对变量还是全局定义
的键值对变量,都采用普通引用传递
的方式;而对于临时
的键值对变量,则以右值引用
的方式传参。
-
指定位置插入新键值对
// 1、以普通引用的方式传递 val 参数 iterator insert (const_iterator position, const value_type& val); // 2、以右值引用的方式传递 val 键值对参数 template <class P> iterator insert (const_iterator position, P&& val);
其中:
val
为要插入的键值对变量。注意和第 1 种方式
的语法格式不同,这里返回的是迭代器
,而不再是pair 对象
:- 插入
成功
,insert()
方法会返回一个指向map
容器中已插入键值对的迭代器(新键值对键会插入到该迭代器指向的键值对的【前面】
); - 插入
失败
,insert()
方法同样会返回一个迭代器,该迭代器指向map
容器中和val
具有相同键的那个键值对。//指定要插入的位置 map<string, string>::iterator it = mymap.begin(); //向 it 位置以普通引用的方式插入 STL auto iter1 = mymap.insert(it, STL); //插入失败样例 auto iter3 = mymap.insert(it, STL); //向 it 位置以右值引用的方式插入临时键值对 auto iter2 = mymap.insert(it, pair<string, string>("C语言教程", "http://c.biancheng.net/c/"));
- 插入
-
插入指定区域内的所有键值对
template <class InputIterator> void insert (InputIterator first, InputIterator last);
其中:
first
和last
都是迭代器,它们的组合[first,last)
可以表示某map
容器中的指定区域,该区域包括first
迭代器指向的元素,但不包含last
迭代器指向的元素。//创建一个空 map 容器 map<string, string>copymap; //指定插入区域 map<string, string>::iterator first = ++mymap.begin(); map<string, string>::iterator last = mymap.end(); //将<first,last>区域内的键值对插入到 copymap 中 copymap.insert(first, last);
-
允许一次插入多个键值对
void insert ({val1, val2, ...});
其中:
vali
都表示的是键值对变量(pair
对象)。//创建空的 map 容器 std::map<std::string, std::string>mymap; //向 mymap 容器中添加 3 个键值对 mymap.insert({ {"STL教程", "http://c.biancheng.net/stl/"}, { "C语言教程","http://c.biancheng.net/c/" }, { "Java教程","http://c.biancheng.net/java/" } });
-
emplace()
和emplace_hint()
方法-
emplace()
方法的语法template <class... Args> pair<iterator,bool> emplace (Args&&... args);
注释详见:与
insert()
方法形式-1
相同 -
emplace_hint()
方法的语法template <class... Args> iterator emplace_hint (const_iterator position, Args&&... args);
注释详见:与
insert()
方法形式-2
相同
-
3.7
map
容器operator[]
和insert()
效率对比- 结论:如果要
更新已存在
的map
元素,operator[]
更好,但如果要增加一个新元素
,insert()
方法则更有优势。
3.9
C++ map
容器3
种插入键值对的方法,谁的效率更高?- 详见:https://blog.csdn.net/qq_44004011/article/details/115359611
- 结论:C++11新增的
emplace()
和emplace_hint()
都比insert
效率高- 使用
insert()
向 map 容器中插入键值对的过程是,先创建该键值对,然后再将该键值对复制或者移动到map
容器中的指定位置; - 使用
emplace()
或emplace_hint()
插入键值对的过程是,直接在map
容器中的指定位置构造该键值对。
- 使用
multimap
- 注意:和
map
容器相比,multimap
没有提供at()
成员方法,也没有重载[]
运算符。
set
-
从语法上讲 set 容器并没有强制对存储元素的类型做 const 修饰,即 set 容器中存储的元素的值是可以修改的。但是,C++ 标准为了防止用户修改容器中元素的值,对所有可能会实现此操作的行为做了限制,使得在正常情况下,用户是无法做到修改 set 容器中元素的值的。对于初学者来说,切勿尝试直接修改 set 容器中已存储元素的值,这很有可能破坏 set 容器中元素的有序性,
最正确的修改 set 容器中元素值的做法是:先删除该元素,然后再添加一个修改后的元素。
-
创建并初始化
set<string> myset{ "http:/java/","http:/stl/","http:/python/" }; set<string> copyset(myset); // 等同于:set<string> copyset = myset set<string> retSet() { set<string> myset{ "http:/java/","http:/stl/","http:/python/" }; return myset; } // 注意,由于 retSet() 函数的返回值是一个临时 set 容器,因此在初始化 copySet 容器时, // 其内部调用的是 set 类模板中的移动构造函数,而非拷贝构造函数。 set<string> copySet(myset); // 等同于:set<string> copySet = retSet(); // 修改默认排序规则创建 set<string> copyset(++myset.begin(), myset.end());
-
set
容器配备的是双向迭代器(bidirectional iterator)
。所以set
容器迭代器只能进行++p、p++、--p、p--、*p
操作,并且迭代器之间只能使用 == 或者 != 运算符
进行比较。
常用成员函数
-
【迭代器有关】
成员函数成员函数 功能 find(val)
在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
lower_bound(val)
返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
upper_bound(val)
返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
equal_range(val)
该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。
count(val)
在当前 set 容器中,查找值为 val 的元素的个数,并返回。注意,由于 set 容器中各元素的值是唯一的,因此该函数的返回值最大为 1。
- 和
map
分析一样:equal_range(val)
函数的返回值是一个pair
类型数据,其包含2
个迭代器,表示set
容器中和指定参数 val 相等的元素所在的区域
,但由于set
容器中存储的元素各不相等,因此该函数返回的这2
个迭代器所表示的范围中,最多只会包含 1 个元素(即:=val的那个元素)
。——> 更适用于multiset
容器!
- 和
-
insert()
成员方法:-
无需指定插入位置,直接插入
//创建并初始化set容器 set<string> myset; // 准备接受 insert() 的返回值 pair<set<string>::iterator, bool> retpair; // 采用【普通引用】传值方式 string str = "http://c.biancheng.net/stl/"; retpair = myset.insert(str); // 采用【右值引用】传值方式 retpair = myset.insert("http://c.biancheng.net/python/");
- 当向
set
容器添加元素成功时,该迭代器指向set
容器新添加的元素,bool
类型的值为true
; - 如果添加失败,即证明原
set
容器中已存有相同的元素,此时返回的迭代器就指向容器中相同的此元素,同时bool
类型的值为false
。
- 当向
-
指定位置插入新键值对
//创建并初始化set容器 set<string> myset; //准备接受 insert() 的返回值 set<string>::iterator iter; //采用【普通引用】传值方式 string str = "http://c.biancheng.net/stl/"; iter = myset.insert(myset.begin(),str); //采用【右值引用】传值方式 iter = myset.insert(myset.end(),"http://c.biancheng.net/python/");
- 当向
set
容器添加元素成功时,该迭代器指向容器中新添加的元素; - 当添加失败时,证明原
set
容器中已有相同的元素,该迭代器就指向set
容器中相同的这个元素。
- 当向
-
插入指定区域内的所有键值对
//创建一个同类型的空 set 容器 set<std::string> otherset; //利用 myset 初始化 otherset otherset.insert(++myset.begin(), myset.end());
- 同样的:组合
[first = ++myset.begin(),last = myset.end())
可以表示另一set
容器中的一块区域,该区域包括first
迭代器指向的元素,但不包含last
迭代器指向的元素。
- 同样的:组合
-
允许一次插入多个键值对
-
-
emplace()
和emplace_hint()
方法 ——> 和map
分析一样,不但能实现向set
容器添加新元素的功能,其实现效率也比insert()
成员方法更高。 -
删除数据:
-
erase()
方法:// 删除 set 容器中值为 val 的元素 size_type erase (const value_type& val); // 删除 position 迭代器指向的元素 iterator erase (const_iterator position); // 删除 [first,last) 区间内的所有元素 iterator erase (const_iterator first, const_iterator last);
- 第 1 种格式,其返回值为一个整数,表示成功删除的元素个数;
- 后 2 种格式,返回值都是一个迭代器,其指向的是
set
容器中删除元素之后的第一个元素。(若要删除的元素就是set
容器的最后一个元素,则该方法返回的迭代器就指向新set
容器中最后一个元素之后的位置
(等价于end()
方法返回的迭代器)
// 创建并初始化 set 容器 set<int>myset{1,2,3,4,5}; // 1) 调用第一种格式的 erase() 方法 int num = myset.erase(2); // 删除元素 2,myset={1,3,4,5} // 2) 调用第二种格式的 erase() 方法 set<int>::iterator iter = myset.erase(myset.begin()); // 删除元素 1,myset={3,4,5} // 3) 调用第三种格式的 erase() 方法 set<int>::iterator iter2 = myset.erase(myset.begin(), --myset.end());// 删除元素 3,4,myset={5}
-
clear()
方法:语法格式为:void clear();
——> 删除set
容器中存储的所有元素
-
3.17
-
STL关联式容器自定义排序规则(
2种方法
)——> 更具体的可以详见:priority_queue
自定义排序方法-
利用
函数对象
实现自定义排序规则//定义函数对象类 class cmp { public: //重载 () 运算符 bool operator ()(const string &a,const string &b) { //按照字符串的长度,做升序排序(即存储的字符串从短到长) return (a.length() < b.length()); } };
-
利用
重载关系运算符
实现自定义排序 -
补充方法一:利用
函数指针
实现自定义排序 -
补充方法二:利用
lambda表达式
实现自定义排序
-
3.18
- 如何修改关联式容器中键值对的键?
- 总结:
map
和multimap
容器中元素的键是无法直接修改的,但借助const_cast
,我们可以直接修改set
和multiset
容器中元素的非键部分。
multiset
-----------------------
无序
关联式容器的底层:「哈希表」关联式容器
的底层实现采用的树存储结构,更确切的说是红黑树
结构;无序容器
的底层实现采用的是哈希表
的存储结构。- 和关联式容器相比,无序容器具有以下 2 个特点:
- 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
- 和关联式容器相比,无序容器擅长通过
指定键查找对应的值
(平均时间复杂度为O(1)
);但对于使用迭代器遍历
容器中存储的元素,无序容器的执行效率则不如关联式容器。
- 应用场景:
若涉及【大量使用迭代器遍历容器】的操作,建议首选关联式容器;反之,若更多的操作是【通过键获取对应的值】,则应首选无序容器。
hashtable
-
C++ STL 标准库中,不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用
“链地址法”(又称“开链法”)
解决数据存储位置发生冲突的哈希表(“哈希冲突”
),如下图:
其结构就是
一个 buckets(vector)+ 多个 list
,每个vector
元素代表一个key
,list
为key
相同的节点集合。(之所以选择vector
为存放桶buckets
元素的基础容器,主要是因为vector
容器本身具有动态扩容能力,无需人工干预。) -
哈希表存储结构还有一个重要的属性,称为
负载因子(load factor)
。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。无序容器中,负载因子的计算方法为:负载因子 = 容器存储的总键值对 / 桶数
默认情况下,无序容器的最大负载因子为
1.0
。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意:此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。——> 这也就解释了:为什么在操作无序容器过程中,键值对的存储顺序有时会“莫名”
的发生变动。 -
如何
向前操作
?首先尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list
内,所以利用节点的next
指针即可轻易完成前进操作,如果目前正巧是list
的尾端,就跳至下一个bucket
身上,那正是指向下一个list
的头部节点。注意:
hashtable
的迭代器没有后退操作,所以没有重载 -- 操作符
,也没有定义逆向迭代器reverse iterator
。 -
基于
hashtable
的unordered_set
和unordered_map
:查找速度更快,因为元素个数大于bucket
的数量就扩容,所以一个元素查找次数一般是1~2次
,比红黑树快,但红黑树有优秀的排序的功能。所以在使用set
和map
时,若追求高效的执行速度
,就基于hashtable
,若需要自动排序
,就基于红黑树。
补充:红黑树
网址:http://data.biancheng.net/view/85.html
-
本质是一棵
二叉查找树(BST)
,此外加上两个特点:- 树中的每个结点增加了一个用于存储颜色的标志域;
- 通过对任意一条从根到叶子的路径上各个节点着色方式的限制,确保没有一条路径会比其他路径长两倍,整棵树要接近于
“平衡”
的状态。
-
性质:——> 决定是否为红黑树!
-
树中的每个结点颜色不是红的,就是黑的;
-
根结点的颜色是黑的;
-
所有为 nil 的叶子结点的颜色是黑的;(注意:叶子结点说的只是为空(nil 或 NULL)的叶子结点!)
-
如果此结点是红的,那么它的两个孩子结点全部都是黑的;
-
对于每个结点,从该结点到到该结点的所有子孙结点的所有路径上包含有相同数目的黑结点;
-
-
红黑树与
AVL
比较:-
AVL是
严格平衡
的,频繁的插入和删除,会引起频繁的rebalance
,导致效率降低;红黑树是弱平衡
的,算是一种折中,插入最多旋转2次,删除最多旋转3次。——> 证明:https://blog.csdn.net/m0_37707561/article/details/122967286 -
对于一棵具有 n 个结点的红黑树,树的高度至多为:
2lg(n + 1)
。——> 由此可推出红黑树进行查找操作时的时间复杂度为O(lgn)
,因为对于高度为h
的二叉查找树的运行时间为O(h)
,而包含有n
个结点的红黑树本身就是最高为lgn
(简化之后)的查找树(h = lgn
),所以红黑树的时间复杂度为O(lgn)
。证明:https://blog.csdn.net/ThePythonFucker/article/details/123346415?spm=1001.2014.3001.5502
-
红黑树在查找、插入删除的复杂度都是
O(logn)
,且性能稳定,所以STL
里面很多结构包括map
底层都是使用的红黑树。
-
-
红黑树与
BST
比较:- 节点成本。
- 平衡二叉树的时间复杂度是
O(logn)
,红黑树的时间复杂度为O(lgn)
,两者都表示的都是时间复杂度为对数关系(lg
函数为底是10
的对数,用于表示时间复杂度时可以忽略)。 - 二叉查找树的时间复杂度会受到其树深度的影响,而红黑树可以保证在最坏情况下的时间复杂度仍为
O(lgn)
。当数据量多到一定程度时,使用红黑树比二叉查找树的效率要高。