STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和 set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在 STL使用过程中,并不会感到陌生。
因为STL 常用, 所以通过STL 来学习 数据结构, 同时加深对STL 容器的认识
map 用的是红黑树()
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),所以被STL选择作为了关联容器的内部结构。本文并不会介绍详细AVL树和RB树的实现以及他们的优劣,关于RB树的详细实现参看红黑树: 理论与实现(理论篇)。本文针对开始提出的几个问题的回答,来向大家简单介绍map和set的底层数据结构。
为何map和set的插入删除效率比用其他序列容器高?
大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:
A
/ /
B C
/ / / /
D E F G
因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系
set 和map都是无序的保存元素 只能通过它提供的接口对里面的元素进行访问
set 集合, 用来判断某一个元素是不是在一个组里面 使用的比较少
map 映射 相当于字典 把一个值映射成另一个值 如果想创建字典的话使用它好了
string vector list deque set 是有序容器
string 是basic_string 的实现,
在内存中是连续存放的 为了提高效率,都会有保留内存,如string s= "abcd",这时s使用的空间可能就是255, 当string再次往s里面添加内容时不会再次分配内存 直到内容>255刊才会再次申请内存 因此提高了它的性能
当内容>255时 string会先分配一个新内存 然后再把内容复制过去 再复制先前的内容
对string的操作 如果是添加到最后时 一般不需要分配内存 所以性能最快
如果是对中间或是开始部分操作 如往那里添加元素或是删除元素 或是代替元素 这时需要进行内存复制 性能会降低
如果删除元素 string一般不会释放它已经分配的内存 为了是下次使用时可以更高效
由于string会有预保留内存 所以如果大量使用的话 会有内存浪费 这点需要考虑
还有就是删除元素时不释放过多的内存 这也要考虑
string中内存是在堆中分配的 所以串的长度可以很大 而char[] 是在栈中分配的 长度受到可使用的最大栈长度限制
如果对知道要使用的字符串的最大长度 那么可以使用普通的char[] 实现而不必使用string
string用在串长度不可知的情况 或是变化很大的情况
如果string已经经历了多次添加删除 现在的尺寸比最大的尺寸要小很多 想减少string使用的大小 可以使用
string s = "abcdefg";
string y(s); //因为再次分配内存时 y只会分配与s中内容大一点的内存 所以浪费不会很大
s.swap(y);//减少s使用的内存
如果内存够多的话就不用考虑这个了
capacity是查看现在使用内存的函数
大家可以试试看string分配一个一串后的capacity返回值
还有其它操作后的返回值
第二个是vector
vector就是动态数组 它也是在堆中分配内存 元素连续存放 有保留内存 如果减少大小后央存也不会释放 如果新值.当前大小时才会再分配内存
对最后元素操作最快 (在后面添加删除最快 ) 此时一般不需要移动内存 只有保留内存不够时才需要
对中间和开始处进行添加删除元素操作需要移动内存 如果你的元素是结构或是类 那么移动的同时还会进行构造和析构操作 所以性能不高
访问方面 对任何元素的访问都是O(1) 也就是是常数的 所以vector常用来保存需要经常进行随机访问的内容 并且不需要经常对中间元素进行添加删除操作
相比较可以看到vector的属性与string差不多 同样可以使用capacity看当前保留的内存
使用swap来减少它使用的内存
总结
需要经常随机访问请用vector
list
list就是链表 元素也是在堆中存放
每个元素都是放在一块内存中
list没有空间预留习惯 所以每分配一个元素都会从内存中分配
每删除一个元素都会释放它占用的内存 这与上面不同 可要看好了
list在哪里添加删除元素性能都很高 不需要移动内存 当然也不需要对每个元素都进行构造与析构了 所以常用来做随机操作容器
但是访问list里面的元素时就开始和最后访问最快
访问其它元素都是O(n) 所以 如果需要经常随机访问的话 还是使用其它的好
总结
如果你喜欢经常添加删除大对象的话 那么请使用list
要保存的对象不大 构造与析构操作不复杂 那么可以使用vector代替
list<指针> 完全是性能最低的做法 这种情况下还是使用vector<指针>好
因为指针没有构造与析构 也不占用很大内存
deque
双端队列
也是在堆中保存内容的
它的保存形式如下
[堆1]
...
[堆2]
...
[堆三]
每个堆保存好几个元素
然后堆和堆之间有指针指向
看起来像是list和vector的结合品
不过确实也是如此
deque的让你可以在前面快速的添加删除元素
或是在后面快速的添加删除元素
然后还可以比较高的随机访问速度
vector是可以快速的在最后添加删除元素 并可以快速的访问任意元素
list是可以快速的在所有地方添加删除元素 但是只能快速的访问最开始与最后的元素
deque在开始和最后添加元素都一样快 并提供了随机访问方法 像vector一样使用[]访问任意元素 但是 随机访问速度比不上vector快 因为它要内部处理堆跳转
deque也有保留空间 另外 由于deque不要求连续空间 所以可以保存的元素比vector更大 这点也要注意一下 还有就是在前面和后面添加元素时都不需要移动其它块的元素 所以 性能也很高
STL 使用总结
本文主要讨论C++标准库中的顺序容器及相应的容器适配器,这些内容主要涉及顺序容器类型:vector、list、deque,顺序容器适配器类型:stack、queue、priority_queue。
标准库中的容器分为顺序容器和关联容器。顺序容器(sequential container)内的元素按其位置存储和访问,顾名思义,这些内部元素是顺序存放的;顺序容器内的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。而关联容器的元素按键(key)排序。
标准容器类 | 说明 |
顺序性容器 | |
vector | 从后面快速的插入与删除,直接访问任何元素 |
deque | 从前面或后面快速的插入与删除,直接访问任何元素 |
list | 双链表,从任何地方快速插入与删除 |
关联容器 | |
set | 快速查找,不允许重复值 |
multiset | 快速查找,允许重复值 |
map | 一对多映射,基于关键字快速查找,不允许重复值 |
multimap | 一对多映射,基于关键字快速查找,允许重复值 |
容器适配器 | |
stack | 后进先出 |
queue | 先进先出 |
priority_queue | 最高优先级元素总是第一个出列 |
容器类型
vector | 容器,支持快速随机访问(连续存储) |
list | 链表,支持快速插入/删除 |
deque | 双端队列,支持随机访问(连续存储),两端能快速插入和删除 |
stack | 栈 |
queue | 队列 |
priority_queue | 优先级队列 |
*iter | 返回类型iter所指向的元素的引用 |
iter->mem | 对iter进行解引用,并取得指定成员 |
++iter | 给iter加1,使其指向容器中下一个元素 |
iter++ | |
--iter | 给iter减1,使其指向容器中前一个元素 |
iter-- | |
iter1 == iter2 | 当两个迭代器指向同一个容器中的同一元素,或者当它们都指向 |
iter1 != iter2 | 同一个容器的超出末端的下一个位置时,两个迭代器相等。 |
iter + n | 在迭代器上加(减)整数值,将产生指向容器中前面(后面)第n个元素的迭代器; |
iter - n | 新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置。 |
iter1 += iter2 | 复合运算:先加(减),再赋值 |
iter1 -= iter2 | |
iter1 - iter2 | 只适用于vector和deque |
>, >=, <, <= | 比较迭代器的位置关系;只适用于vector和deque |
back() | 返回容器的最后一个元素的引用。如果容器为空,则该操作未定义 |
front() | 返回容器的第一个元素的引用。如果容器为空,则该操作未定义 |
c[n] | 返回下标为n的元素的引用;如果n<0 or n>=size(),则该操作未定义 |
at[n] | 返回下标为n的元素的引用;如果下标无效,则抛出异常out_of_range异常 (注:只适用于vector和deque容器) |
erase(p) | 删除迭代器p所指向的元素。返回一个迭代器,它指向被删除的元素后面的元素。如果p指向容器内最后一个元素,则返回的迭代器指向容器的超出末端的下一个位置;如果p本身就是指向超出末端的下一个位置的迭代器,则该函数未定义 |
erase(b, e) | 删除[b, e)内的所有元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一个位置的迭代器,则返回的迭代器也指向超出末端的下一个位置。 |
clear() | 删除容器内的所有元素,返回void |
pop_back() | 删除容器内的最后一个元素,返回void。如果容器为空,则该操作未定义。 |
pop_front() | 删除容器内的第一个元素,返回void。如果c为空容器,则该操作未定义 (注:只适用于list和deque容器) |
c1 = c2 | 删除容器c1的所有元素,然后将c2的元素复制给c1。c1和c2的类型必须相同。 |
c1.swap(c2) | 交换内容:调用该函数后,c1中存放的是c2原来的元素,c2中存放的是c1原来的元素。c1和c2的类型必须相同。该函数的执行速度通常要比将c2的元素复制到c1的操作快。 |
c.assign(b, e) | 重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e必须不是指向c中元素的迭代器。 |
c.assign(n, t) | 将容器c重新设置为存储n个值为t的元素。 |
vector (连续的空间存储,可以使用[]操作符)快速的访问随机的元素,快速的在末尾插入元素,但是在序列中间岁间的插入,删除元素要慢,而且如果一开始分配的空间不够的话,有一个重新分配更大空间,然后拷贝的性能开销。
deque (小片的连续,小片间用链表相连,实际上内部有一个map的指针,因为知道类型,所以还是可以使用[],只是速度没有vector快)快速的访问随机的元素,快速的在开始和末尾插入元素,随机的插入,删除元素要慢,空间的重新分配要比vector快,重新分配空间后,原有的元素不需要拷贝。对deque的排序操作,可将deque先复制到vector,排序后在复制回deque。
list (每个元素间用链表相连)访问随机元素不如vector快,随机的插入元素比vector快,对每个元素分配空间,所以不存在空间不够,重新分配的情况。
set:内部元素唯一,用一棵平衡树结构来存储,因此遍历的时候就排序了,查找也比较快的哦。
map :一对一的映射的结合,key不能重复。
stack :适配器,必须结合其他的容器使用,stl中默认的内部容器是deque。先进后出,只有一个出口,不允许遍历。
queue: 是受限制的deque,内部容器一般使用list较简单。先进先出,不允许遍历。
vector<bool> 与bitset<> ,前面的可以动态改变长度。
priority_queue: 插入的元素就有优先级顺序,top出来的就是优先级最高的了
valarray 专门进行数值计算的,增加特殊的数学函数。
s.empty() | 如果栈为这人,则true;否则返回false |
s.size() | 返回栈中元素的个数 |
s.pop() | 删除栈顶元素,但不返回其值 |
s.top() | 返回栈顶元素的值,但不删除该元素 |
s.push(item) | 在栈项压入新元素 |
q.empty() | 如果队列为空,则返回true;否则返回false |
q.size() | 返回队列中元素的个数 |
q.pop() | 删除队首元素,但不返回其值 |
q.front() | 返回队首元素的值,但不删除该元素 (注:该操作只适用于队列) |
q.back() | 返回队尾元素的值,但不删除该元素 (注:该操作只适用于队列) |
q.top() | 返回具有最高优先级的元素值,但不删除该元素 |
q.push(item) | 对于queue,在队尾压入一个新元素; 对于priority_queue,在基于优先级的适当位置插入新元素 |