C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和 set封装了二叉树等.
标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-BlackTree)。RB树的统计性能要好于一般的平衡二叉树。
a. 为何map和set的插入删除效率比用其他序列容器高?
答:因为对于关联容器来说,不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。
b. 为何每次insert之后,以前保存的iterator不会失效?
答:iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效 了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存 放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能 不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内 存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
c. 为何map和set不能像vector一样有个reserve函数来预分配数据?
答:原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。set,multiset
set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
因为是排序的,所以set中的元素不能被修改,只能删除后再添加。
向set中添加的元素类型必须重载<操作符用来排序。排序满足以下准则:
(1)非对称,若A < B为真,则B < A为假。
(2)可传递,若A < B ,B < C,则A < C。
(3)A < A永远为假。
set中判断元素是否相等:if(!(A < B || B < A)),当A < B和B < A都为假时,它们相等。map,multimap
map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序,map中元素的key不允许重复,multimap可以重复。
map <key,value>
因为是排序的,所以map中元素的key不能被修改,只能删除后再添加。key对应的value可以修改。
向map中添加的元素的key类型必须重载<操作符用来排序。排序与set规则一致。hash_map和map的区别在哪里?
构造函数。hash_map需要hash函数,等于函数;map只需要比较函数(小于函数).
存储结构。hash_map采用hash表存储,map一般采用 红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。什么时候需要用hash_map,什么时候需要用map?
总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n) 小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且 hash_map的构造速度较慢。
现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用vector, list, deque
(1).vector - 会自动增长的数组
vector < int > vec(10) //一个有10个int元素的容器
vector < float > vec(10, 0.5)//一个有10个float元素的容器,并且他们得值都是0.5
vector <int>::iterator iter;
for(iter = vec.begin(); iter != vec.end(); iter++)
{
//. . . . . . .
}
vector由于数组的增长只能向前,所以也只提供了后端插入和后端删除,
也就是push_back和pop_back。当然在前端和中间要操作数据也是可以的,
用insert和erase,但是前端和中间对数据进行操作必然会引起数据块的移动,
这对性能影响是非常大的。
最大的优势就是随机访问的能力。
vector<T1>::iterator相关的方法有:
begin():用来获得一个指向vector第一个元素的指针
end():用来获得一个指向vector最后一个元素之后的那个位置的指针,注意不是指向最后一个元素。
erase(vector<T1>::iterator):用来删除作为参数所传入的那个iterator所指向的那个元素。
(2)list - 擅长插入删除的链表
list<string>Milkshakes; list<int> Scores;
push_back, pop_backpush_front. pop_front
list是一个双向链表的实现。
为了提供双向遍历的能力,list要比一般的数据单元多出两个指向前后的指针
一个使用iterator来删除元素的例子
list<string> stringList;
list<string>::iterator iter;
advance(iter, 5); //将iterator指向stringList的第五个元素
stringList.erase(iterator);//删除
那么删除操作进行以后,传入erase()方法的iterator指向哪里了呢?它指向了删除操作前所指向的那个元素的后一个元素。
(3)deque - 拥有vector和list两者优点的双端队列.deque是双端队列可在头和尾部插入、删除元素。
(4)这三个模板的总结 比较和一般使用准则
这三个模板都属于序列类模板,可以看到他们有一些通用方法
size():得到容器大小
begin():得到指向容器内第一个元素的指针(这个指针的类型依容器的不同而不同)
end():得到指向容器内最后一个元素之后一个位置的指针
erase():删除传入指针指向的那个元素
clear():清除所有的元素
==运算符:判断两个容器是否相等
=运算符:用来给容器赋值
上面的三个模板有各自的特点
vector模板的数据在内存中连续的排列,所以随机存取元素(即通过[]运算符存取)的速度最快,这一点和数组是一致的。同样由于它的连续排列,所以它在除尾部以外的位置删除或添加元素的速度很慢,在使用vector时,要避免这种操作。
list模板的数据是链式存储,所以不能随机存取元素。它的优势在于任意位置添加 删除元素的速度。
deque模板是通过链接若干片连续的数据实现的,所以均衡了以上两个容器的特点
vector内存用完了,会以当前size大小重新申请2*size的内存,然后把原来的元素复制过去,把新元素插上,然后释放原来的内存。
一般我们释放vector里的元素使用clear,其实它不能释放内存,要想释放内存要使用swap
map是怎么实现的?查找的复杂度是多少?能不能边遍历边插入?
a. 红黑树和散列
b. O(logn)
c. 不可以,map不像vector,它在对容器执行erase操作后不会返回后一个元素的迭代器,所以不能遍历地往后删除。hash_map和map的区别在哪里?
hash_map底层是散列的所以理论上操作的平均复杂度是常数时间,map底层是红黑树,理论上平均复杂度是O(logn),下面是借鉴的网上的总结:
这 里总结一下,选用map还是hash_map,关键是看关键字查询操作次数,以及你所需要保证的是查询总体时间还是单个查询的时间。如果是要很多次操作, 要求其整体效率,那么使用hash_map,平均处理时间短。如果是少数次的操作,使用 hash_map可能造成不确定的O(N),那么使用平均处理时间相对较慢、单次处理时间恒定的map,考虑整体稳定性应该要高于整体效率,因为前提在操 作次数较少。如果在一次流程中,使用hash_map的少数操作产生一个最坏情况O(N),那么hash_map的优势也因此丧尽了。快排算法的枢轴位置是怎么选择的?
三点中值法,取整个序列的头、尾、*三个位置的元素,以其中值作为枢轴。进程和线程的差别?
答:线程是指进程内的一个执行单元,也是进程内的可调度实体.区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4)系统开销:创建撤消进程,系统都要为之分配和回收资源,系统的开销明显大于创建撤消线程
多进程与多线程,两者都可以提高程序的并发度,提高程序运行效率和响应时间。