STL(Standard Template Library),即标准模板库,它涵盖了常用的数据结构和算法, 并且具有跨平台的特点。将泛型编程思想和STL库用于系统设计中,明显降低了开发强度, 提高了程序的可维护性及代码的可重用性。这也是越来越多的笔试和面试中考查STL相关 知识的原因。
STL是C++标准函数库的一部分。STL的基本观念就是把数据和操作分离。图11.1所 示的是STL组件之间的合作。
由图11.1可知,STL中数据由容器类别来加以管理,操作则由可定制的算法来完成。 迭代器在容器和算法之间充当黏合剂,它使得任何算法都可以和任何容器进行交互运作。 STL含有容器、算法、迭代器组件。
兼容。C++标准库中存在两套C的函数库,一套是带有h扩展名的(比如<stdio.h>),而另 一套则没有(比如<cstdio>)。它们确实没有太大的不同。
(2)语言支持(language support)部分,包含了一些标准类型的定义以及其他特性的定 义。这些内容被用于标准库的其他地方或是具体的应用程序中。
(3)诊断(diagnostics)部分,提供了用于程序诊断和报错的功能,包含了异常处理 (exceptionhandling)、断言(assertions)、错误代码(error numbercodes)三种方式。
(4)通用工具(generalutilities)部分,这部分内容为C++标准库的其他部分提供支持。 当然,你也可以在自己的程序中调用相应功能,比如动态内存管理工具、日期/时间处理工 具。记住,这里的内容也已经被泛化了(采用了模板机制)。
(5)字符串(string)部分,用来代表和处理文本。它提供了足够丰富的功能。
(6)国际化(internationalization)部分,作为0OP特性之一的封装机制在这里扮演着 消除文化和地域差异的角色,采用locale和facet可以为程序提供众多国际化支持,包括对 各种字符集的支持、日期和时间的表示、数值和货币的处理等等。
(7)容器(containers)部分,是STL的一个重要组成部分,涵盖了许多数据结构,如 链表、vector(类似于大小可动态增加的数组)、queue(队列)、stack(堆栈)……string也 可以看作一个容器,适用于容器的方法同样也适用于string。
(8)算法(algorithms)部分,是STL的一个重要组成部分,包含了大约70个通用算法, 用于操控各种容器,同时也可以操控内建数组。
(9)迭代器(iterators)部分,是STL的一个重要组成部分。它使得容器和算法能够完 美地结合。事实上,每个容器都有自己的迭代器,只有容器自己才知道如何访问自己的元 素。它有点像指针,算法通过迭代器来定位和操控容器中的元素。
(10)数值(numerics)部分,包含了一些数学运算功能,提供了复数运算的支持。
(11)输入/输出(input/output)部分,就是经过模板化了的原有标准库中的iostream部 分。它提供了对C++程序输入/输出的基本支持。在功能上保持了与原有iostream的兼容, 增加了异常处理的机制,并支持国际化(internationalization)。
总体上,在C++标准函数库中,STL主要包含了容器、算法、迭代器。string也可以算 作STL的一部分。
【答案】
STL,即标准模板库,是一个具有工业强度的、高效的C++程序库。它是最新的C++ 标准函数库中的一个子集,包括容器、算法、迭代器组件。
面试题2具体说明STL如何实现vector
考点:对vector的理解及其实现细节
出现频率:★★★★
【解析】
vector的定义如下。
template<class _Ty, class _A = allocator<_Ty>>
class vector {
...
};
这里省略了中间的成员。其中_Ty类型用于表示vector中存储的元素类型,_A默认为 allocator<_Ty>类型。
这里需要说明的是allocator类,它是一种“内存配置器”,负责提供内存管理(可能包 含内存分配、释放、自动回收等能力)相关的服务。于是对于程序员来说,就不用关心内 存管理方面的问题了。
vector支持随机访问,因此为了效率方面的考虑,它内部使用动态数组的方式实现。 当进行insert或push_back等增加元素的操作时,如果此时动态数组的内存不够用,就要 动态地重新分配,一般是当前大小的两倍,然后把原数组的内容拷贝过去。所以,在一般 情况下,其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。例如下面的 程序。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v; //初始时无元素,容量为0
cout << v.capacity() << '\t' << v.size() << endl;
v.push_back(1); //容量不够,分配1个元素内存
cout << v.capacity() << '\t' << v.size() << endl;
v.push_back(2); //容量不够,分配2个元素内存
cout << v.capacity() << '\t' << v.size() << endl;
v.push_back(3); //容量不够,分配4个元素内存
cout << v.capacity() << '\t' << v.size() << endl;
v.push_back(4);
v.push_back(5); //容量不够,分配8个元素内存
cout << v.capacity() << '\t' << v.size() << endl;
v.push_back(6);
v.push_back(7);
v.push_back(8);
v.push_back(9); //容量不够,分配16个元素内存
cout << v.capacity() << '\t' << v.size() << endl;
return 0;
}
下面是各个执行步骤。
(1)代码第7行,初始化时v无元素(size为0),且容量(capacity)也为0。
(2)代码第9行,在数组末尾添加元素1,由于容量不够,因此allocator分配1个int 大小的内存,并把整数1复制到这个内存中。
(3)代码第11行,在数组末尾添加元素2。此时容量为1,但元素个数需要变为2,因 此容量不够。于是allocator先分配原来容量的2倍大小的内存(2个int大小的内存),然后 把原来数组中的1和新加入的2拷贝到新分配的内存中,最后释放原来数组的内存。
(4)代码第13行,在数组末尾添加元素3。此时容量为2,而元素个数需要变为3,因 此容量也不够。和(3)相同,allocator分配4个int的内存,然后把原来数组中的1、2以及新加入的3拷贝到新分配的内存,最后释放原数组的内存。
(5)代码第15行,在数组末尾添加元素3。此时容量为4,而元素个数需要变为3,因 此容量足够,不需要分配内存,直接把4拷贝到数组的最后即可。
以后的操作不再赘述。注意,vector的size()和capacity()是不同的,前者表示数组中元 素的多少,后者表示数组有多大的容量。由上面的分析可以看出,使用vector的时候需要 注意内存的使用。如果频繁地进行内存的重新分配,会导致效率低下。
【答案】
vector内部是使用动态数组的方式实现的。如果动态数组的内存不够用,就要动态地重 新分配,一般是当前大小的两倍,然后把原数组的内容拷贝过去。所以,在一般情况下, 其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。它的内部使用allocator 类进行内存管理,程序员不需要自己操作内存。
面试题3 看代码回答问题——vector容器中iterator的使用
考点:vector中iterator的使用
出现频率:★★★★
vector<int> array;
array.push_back(1);
array.push_back(2);
array.push_back(3);
for (vector<int>::size_type i = array.size() - 1; i >= 0; --i) //反向遍历array数组
{
cout << array[i] << endl;
}
当运行代码时,没有输出321,而是输出了一大堆很大的数字,为什么? 于是修改代码
for (vector<int>::size_type j = array.size(); j > 0; j--) 反向遍历array数组
{
cout << "element is " << array[j - 1] << endl;
}
这样就输出了321,到底是为什么呢?
【解析】
由于vector支持随机访问,并且重载了[运算符,因此可以像数组那样(比如a[i])来 访问vector中的第i+1个元素。
现在来简单分析vector中size_type的定义过程。为方便起见,下面省略了某些头文件。
在vector定义中可以看到:
typedef _A::size_type size_type;
而_A是allocator<_Ty>,因此查看allocator的定义,不难发现:
typedef SIZT size_type;
而_SIZT的定义为:
#define sIZT size_t
最后size_t的定义为:
typedef unsigned int size t;
因此最后的结果为:size_type是个unsigned int类型成员。我们知道,无符号的整数是 大于等于0的,因此上面第1段代码(代码第5行)中的i>=0永远为true,程序一直循环, 输出很多随机数,最后程序崩溃。而第2段代码(代码第1行)使用了i>0作为循环的条件, 即i为0时结束循环。
面试题4 看代码找错——vector容器的使用
考点:理解vector容器的使用
出现频率:★★★★
typedef vector IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(3);
//删除array数组中所有的2
for (IntArray::iterator itor = array.begin(); itor != array.end(); ++itor)
{
if (2 == *itor)
array.erase(itor);
}
【解析】
这道题有两个错误。
(1)代码第1行中没有使用类型参数,这将会导致编译错误。由于array需要添加int 类型的元素,因此代码第1行定义vector时应该加上int类型。
typedef vector<int>IntArray;
(2)代码第8~12行的for循环,这里只能删除array数组中的第一个2,而不能删
除所有的2。这是因为,每次调用“array.erase(itor );”后,被删除元素之后的内容会自 动往前移,导致迭代漏项,应在删除一项后使itor--,使之从已经前移的下一个元素起继 续遍历。
正确的程序如下。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(3);
//删除array数组中所有的2
for (IntArray::iterator itor = array.begin(); itor != array.end();)
{
if (2 == *itor)
{
itor = array.erase(itor);
}
else
{
++itor;
}
}
//测试删除后array中的内容
for (IntArray::iterator itor = array.begin(); itor != array.end(); ++itor)
{
cout << *itor << endl;
}
}
执行结果为:
1
3
【答案】
(1)没有使用类型参数,会导致编译错误。
(2)只能删除array数组中的第一个2,而不能删除所有的2。因为每次调用 “array.erase(itor );”后,被删除元素之后的内容会自动往前移,导致迭代漏项。
面试题5 把一个文件中的整数排序后输出到另一个文件中
考点:运用vector容器解决实际问题
出现频率:★★★★
【解析】
这个题目涉及文件操作以及排序。我们可以使用vector容器来简化文件操作。在读文 件的时候用push_back把所有的整数放入一个vector<int>对象中,在写文件时用口操作符直 接把vector<int>对象输出到文件。代码如下。
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
//对data容器中的所有元素进行冒泡排序
void Order(vector<int>& data)
{
int count = data.size(); 获得vector中的元素个数
for (int i = 0; i < count; i++)
{
for (int j = 0; j < count - i - 1; j++)
{
if (data[j] > data[j + 1]) //如果当前元素比下一个元素大,则交换
{ //结果为升序排列
int temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
}
int main()
{
vector<int> data;
ifstream in("d:\\data.txt");
if (!in) //打开输出文件失败
{
cout << "infile error!" << endl;
return 1;
}
int temp;
while (!in.eof()) //从文件中读取整数
{
in >> temp;
data.push_back(temp); //把读取的证书放入data容器中
}
in.close();
Order(data); //冒泡排序
ofstream out("d:\\result.txt");
if (!out) //打开输出文件失败
{
cout << "outfile error!" << endl;
return 1;
}
for (int i = 0; i < data.size(); i++)
out << data[i] << " "; //把data容器中的所有元素输出至文件
out.close();
return 0;
}
程序中的Order函数使用冒泡排序把data容器中的所有元素进行了升序。下面说明主 函数的各个步骤。
(1)代码第26行,定义了一个空的vector<int>容器。
(2)代码第27~32行,定义了一个输入文件流,如果打开输入文件(data.txt)失败, 则主函数返回。
(3)代码第34~38行,把输入文件(data.txt)中的所有整数放入vector容器中。
(4)代码第40行,调用Order函数对vector容器中的所有元素进行排序(升序)
(5)代码第41~46行,定义了一个输出文件流,如果打开输出文件(result.txt)失败, 则主函数返回。
(6)代码第47~48行,把data容器中的所有元素输出至result.txt中。
面试题6 list和vector有什么区别
考点:理解list和vector的区别
出现频率:★★★★
【解析】
vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好地 支持随机存取(使用[□操作符访问其中的元素)。但由于它的内存空间是连续的,所以在中间进 行插入和删除操作会造成内存块的拷贝(复杂度是O(n))。另外,当该数组后的内存空间不够 时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能 通过指针来进行数据的访问。这个特点使得它的随机存取变得非常没有效率,需要遍历中 间的元素,搜索复杂度O(n),因此它没有提供[操作符的重载。但由于链表的特点,它可以 以很好的效率支持任意地方的删除和插入。
由于list和vector上面的这些区别,list:iterator与vector:iterator也有一些不同。请看 下面的例子。
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
vector<int> v;
list<int> l;
for (int i = 0; i < 8; i++) //往v和1中分别添加元素
{
v.push_back(i);
l.push_back(i);
}
cout << "v[2] = " << v[2] << endl;
//cout << "l[2] = " << l[2] << endl; //编译错误,1ist没有重载[]
cout << (v.begin() < v.end()) << endl;
//cout << (l.begin() < l.end()) << endl; //编译错误,list::iterator没有重载<或>
cout << *(v.begin() + 1) << endl;
vector<int>::iterator itv = v.begin();
list<int>::iterator itl = l.begin();
itv = itv + 2;
//itl = itl + 2; //编译错误,1ist::iterator没有重载+
itl++; itl++; //list::iterator中重载了++,只能使用++进行迭代访问
cout << *itv << endl;
cout << *itl << endl;
return 0;
}
由于vector拥有一段连续的内存空间,能非常好地支持随机存取,因此vector<int>:: iterator支持“+”、“+=”、“<”等操作符。
而list的内存空间可以是不连续的,它不支持随机访问,因此list<int>::iterator不支持 “+”、“+=”、“<”等操作符运算。因此代码第20、26行会有编译错误。只能使用“++”进
行迭代,例如代码第27行,使用两次itl++来移动itl。还有list也不支持[运算符,因此代
码第18行出现编译错误。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,就使用vector;如果需 要大量的插入和删除,而不关心随机存取,则应使用list。
【答案】
vector拥有一段连续的内存空间,因此支持随机存取。如果需要高效的随机存取,而不 在乎插入和删除的效率,就使用vector。
list拥有一段不连续的内存空间,因此支持随机存取。如果需要大量的插入和删除,而 不关心随机存取,则应使用list。
面试题7 分析代码问题并修正———list和vector容器的使用
考点:理解list和vector的使用
出现频率:★★★
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
list list1;
for (int i = 0; i < 0; i++)
{
list1.push_back(i);
}
for (list::iterator it = list1.begin(); it != list1.end(); ++it)
{
if (*it % 2 == 0)
{
list1.erase(it);
}
}
return 0;
}
【解析】
本题有下面两个方面的问题。
第一个问题是listI以及它的iterator都缺少类型参数,代码第8行和第15行都有编译错误。
由于在代码第12行添加的元素类型为int,因此需要将第8行和第15行中的list:改成list<int>::。
第二个问题是当改正了上面编译的错误后,运行程序会导致程序崩溃。因为当第一次
执行代码第19行(调用erase方法删除元素)后,it原来指向的元素内存已经被释放掉了, 因此进入下一次循环后,获得it的值就出现了访问违规。
这里要注意:vector使用的是数组方式,当删除一个元素后,此元素的后面元素会自动 往前移动。而list由于使用链表结构,因此执行erase时会释放链表的节点内存。但是可以 通过erase的返回值获得原来链表的下一个元素。
改正后的代码如下。
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
list<int> list1; //指定存放int类型的元素
for (int i = 0; i < 8; i++)
{
list1.push_back(i); //把整数放入list1中
}
//iterator也需要指明是int类型元素的迭代器
for (list<int>::iterator it = list1.begin(); it != list1.end(); )
{
if (*it % 2 == 0)
{
it = list1.erase(it); //得到下一个元素的位置
}
else
{
++it; //*it%2 != 0遍历下一个元素
}
}
for (list<int>::iterator it = list1.begin(); it != list1.end(); ++it)
{
cout << *it << endl; //输出list1内的各个元素
}
return 0;
}
执行了代码第10~13行,0~8这9个数字被放入listI中。这里简单分析一下删除过程。
当it指向第一个元素,即0时,由于0是2的倍数,因此18行的if判断为真,执行删 除操作,it返回下一个元素位置,即指向原来的1。如果*it%2!=0,则遍历下一个位置。最后第28~31行打印list1的所有元素。执行结果如下。
面试题8 stl::deque是一种什么数据类型
考点:理解deque容器的内部结构
出现频率:★★★
stl:dequc是一种什么数据类型?()
A.动态数组
B.链表
C.堆栈
D.树
【解析】
deque是由一段一段定量的连续空间组成的,因此是动态数组类型。
【答案】
A
面试题9 在做应用时如何选择vector和deque
vector和deque有什么区别?在做一个应用时,如果选择? 考点:理解vector和deque的区别 出现频率:★★★
【解析】
deque称为双向队列容器,表面上与vector非常相似,甚至能在许多实现中直接替换 yector。比较deque和vector两者的成员函数,可以发现下面两点。
(1)deque比vector多了push_front()和pop_front()两个函数,而这两个函数都是对于首 部进行操作的。于是得到第一个使用deque的情况,就是当程序需要从首尾两端进行插入或 删除元素操作的时候。
(2)deque中不存在capacity()和reserve()成员函数。在vector中,这两个函数分别用于 获取和设置容器容量,例如下面的代码段。
int main()
{
vector<int> v;
v.push_back(1);
cout << v.capacity() << endl;
v.reserve(10);
cout << v.capacity() << endl;
return 0;
}
代码第3行,此时v的容量为0。
代码第4行,添加一个元素到末尾,此时v的容量(capacity)为1,元素个数(size)为1。
代码第6行,调用reserve设置v的容量,此时v的容量为10,元素个数仍然为1。
代码第4行,又添加一个元素到末尾,此时v的元素个数为2。由于元素个数小于容量, 因此容量没有扩充,仍旧为10。
执行结果为:
因此,对于vector来说,如果有大量的数据需要push back,则应当使用reserve()函数 先设定其容量大小,否则会出现许多次容量扩充操作,导致效率很低。
而deque使用一段一段的定量内存,在进行内存扩充时也只是加一段定量内存,因此不 存在容量的概念,也就没有capacityO和reserve()成员函数。
最后,在插入(insert)操作上,deque和vector有很大的不同。由于vector是一块连续 的内存,所以插入的位置决定执行效率,位置越偏向数组首部,效率越低。而deque中的内 存是分段连续的,因此在不同段中的插入效率都相同。
【答案】
vector和deque的不同点:内部数据管理不同。为了提高效率,vector在添加元素之前 最好调用reserve()设置容量,而deque则不需要。
选择的方法:一般情况下选择vector;但当需要从首尾两端进行插入或删除元素操作的 时候,应该选择deque。
面试题10 看代码找错——适配器stack和queue的使用
考点;理解适配器stack和queue的使用
出现频率:★★★★
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
int main()
{
stack<int, vector<int>> s;
queue<int, vector<int>> q;
for (int i = 1; i < 10; i++)
{
s.push(i);
q.push(i);
}
while (!s.empty())
{
cout << s.top() << endl;
s.pop();
}
while (!q.empty())
{
cout << q.front() << endl;
q.pop();
}
return 0;
}
【解析】
代码第9~10行,使用vector分别定义了stack和queue。
代码第12~16行,把1~9放入s和q中。
代码第18~22行,循环打印s的栈顶元素,并且不断退栈,最终s变为空栈,打印的 顺序为与入栈的顺序相反,即987654321。
代码第24~28行,与前面的操作方法一样,但是这里会出现编译错误。这是因为,queue 是先进先出的,入队(调用push)是对队尾进行操作,由于q使用vector作为其序列容器,因此 实际调用的是vector的push_back成员函数。而出队(调用pop)是对队首进行操作,此时q实 际需要调用vector的pop front成员函数,而vector没有这个pop_front成员。因此出现编译错误。
stack是后进先出的,入栈和退栈都是对尾部进行操作,而vector有相应的push_back 和pop_back成员函数,因此代码第18~22行能够顺利执行。
【答案】
由于vector没有pop_front函数,因此代码第27行出现编译错误。
面试题11 举例说明set的用法
考点:理解关联容器set的使用
出现频率:★★★
【解析】
这里演示set中元素的插入、删除和遍历操作。程序示例如下。
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
set<string> strset;
set<string>::iterator si;
strset.insert("cantaloupes");//插入6个元素,其中有两个grapes
strset.insert("apple");
strset.insert("orange");
strset.insert("banana");
strset.insert("grapes");
strset.insert("grapes");
strset.erase("banana");
for (si = strset.begin(); si != strset.end(); si++)
{
cout << *si << " "; //打印set中所有元素
}
cout << endl;
return 0;
}
下面是示例程序的说明。
口代码第10~15行,往set集合中分别插入6个元素。由于字符串"grapes"重复了, 因此实际插入的只有5个字符串。
口代码第16行,删除集合中内容为"banana"的元素。
口代码第17~20行,使用迭代器si遍历set集合。
执行结果:
面试题12 举例说明map的用法
考点:理解关联容器map的使用
出现频率:★★★
【解析】
map中存放的每一个元素都是一个键值对,下面的程序演示了常用的map操作。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
map<int, string> mapstring; //键为int类型,值为string类型
mapstring.insert(pair<int, string>(1, "one")); //插入4个元素
mapstring.insert(pair<int, string>(4, "four"));
mapstring.insert(pair<int, string>(3, "three"));
mapstring.insert(pair<int, string>(2, "two"));
mapstring.insert(pair<int, string>(4, "four four")); //4已经存在,插入失败
mapstring[1] = "one one"; //1已经存在,修改健为1对应的值
mapstring[5] = "five"; //5不存在,添加键为5且值为"five*的元素
cout << mapstring[1] << endl; //打印键为1对应元素的值
mapstring.erase(2); //删除键为2的元素
map<int, string>::iterator f = mapstring.find(2); //查找键为2的元素
if (f != mapstring.end()) //判断查找是否成功,如果成功,则不相等
{
mapstring.erase(f);
}
map<int, string>::iterator it = mapstring.begin();
while (it != mapstring.end()) //使用迭代器遍历map中所有元素
{
cout << (*it).first << " " << (*it).second << endl; //打印元素的键和值
it++;
}
return 0;
}
上面的程序中,mapstring是存放pair<int,string>类型的元素。 插入操作,insert成员函数或使用[门操作符都可以进行插入。但它们有一点区别:当map中已经存在此键时,insert插入失败,例如代码第13行的插入没有作用;而[]操作符则修改 此键所对应的元素,例如代码第15行则修改键为1对应的值。
查找操作,代码第20行查找键为2的元素,如果查找成功,则迭代器指向键为2的元 素,否则指向末尾,即mapstring.end(。
删除操作,这里演示了两种删除,和别的容器一样,可以删除迭代器指向的元素位置 (或者两个迭代器的区间删除)。由于map中元素的键是唯一的,因此map也提供了以键删
除的操作(如代码第19行)。
遍历操作,与其他stl容器遍历操作类似,使用迭代器对begin()和end(之间进行迭代。it指 向的是都pairsint,string>元素。pair有两个成员:first和second,分别表示键(key)和值(value)。
程序输出结果:
可以看到,map遍历的结果是按照元素的键(key)的升序排列。这是默认情况,如果 想让它们降序排列,只需要把map和iterator的声明都改为<int,string,greater<int>>就可以 了,其中greater<int>表示按从大到小排列,并且键的类型是int。
面试题13 STL中map内部是怎么实现的
考点:理解关联容器map的内部使用
出现频率:★★★
【解析】
标准的STL关联容器(包括set和map以及set的衍生体multiset和map的衍生体 multimap)的内部结构是一个平衡二叉树(balanced binarytree)。平衡二叉树有下面几种。
□ AVL-tree:
口RB-tree;
□ AA-tree。
STL的底层机制都是以RB-tree(红黑树)完成的。RB-tree也是一个独立容器,但并不 给外界使用。红黑树这个名字的得来就是由于树的每个结点都被着上了红色或者黑色,节 点所着的颜色被用来检测树的平衡性。在对节点插入和删除的操作中,可能会被旋转来保 持树的平衡性。平均和最坏情况下的插入、删除、查找时间都是0(lgn)。
一个红黑树是一棵二叉查找树,除了二叉查找树带有的一般要求外,它还具有下列的属性。
口结点为红色或者黑色。
□所有叶子结点都是空节点,并且被着为黑色。
□如果父结点是红色的,那么两个子节点都是黑色的。
口结点到其子孙节点的每条简单路径上都包含相同数目的黑色节点。 口根结点是黑色的。
【答案】
map底层是以红黑树实现的。
面试题14 map和hashmap有什么区别
考点:理解关联容器map和hashmap的区别
出现频率:★★★
【答案】
有以下几点区别。
口底层数据结构不同,map是红黑树,hashmap是哈希表。
□ map的优点在于元素可以自动按照键值排序,而hash map的优点在于它的各项操 作的平均时间复杂度接近常数。
□ map属于标准的一部分,而hash map则不是。
面试题15 什么是STL算法
考点:理解STL算法的概念和作用