根据STL 学习数据结构

    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、list、deque(double-ended queue的缩写,发音为“deck”),它们的差别仅在访问元素的方式,以及添加或删除元素相关操作的代价。顺序容器适配器包括:stack、queue和priority_queue。容器只定义了少量操作,大多数操作由算法库提供。如果两个容器提供了相同的操作,则它们的接口(函数名和参数个数)应该相同。 

标准容器类说明
顺序性容器
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 同一个容器的超出末端的下一个位置时,两个迭代器相等。
 
   vector和deque容器的迭代器提供了额外的运算:迭代器的算术运算和另一些关系运算,如下表所示:
 
 iter + n 在迭代器上加(减)整数值,将产生指向容器中前面(后面)第n个元素的迭代器;
 iter - n 新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置。
 iter1 += iter2 复合运算:先加(减),再赋值
 iter1 -= iter2 
 iter1 - iter2 只适用于vector和deque
 >, >=, <, <= 比较迭代器的位置关系;只适用于vector和deque
 
   关系操作符只适用于vector和deque容器,这是因为只有这两种容器为其元素提供快速、随机的访问。它们确保可根据元素位置直接有效地访问指定的容器元素。这两种容器都支持通过元素位置实现的随机访问,因此它们的迭代器可以有效地实现算术和关系运算。
迭代器范围:[first, last)是一个左闭合区间,表示范围从first开始,到last结束,但不包括last。注意:如果first不等于last,则对first反复做自增运算必须能够到达last;否则,即last位于first之前,则将发生未定义行为。
   迭代器范围使用左闭合的意义:因为这样可以统一表示空集,就无需特别处理。
   另外,使用迭代器时,要特别留意迭代器的可能的失效问题。
访问元素
 
 back() 返回容器的最后一个元素的引用。如果容器为空,则该操作未定义
 front() 返回容器的第一个元素的引用。如果容器为空,则该操作未定义
 c[n]

 返回下标为n的元素的引用;如果n<0 or n>=size(),则该操作未定义
 (注:只适用于vector和deque容器

 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容器
 
赋值与swap
 
 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的元素。
 
注意:assign操作首先删除容器内所有的元素,再将参数所指定的新元素插入到容器中。
      swap操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。但要注意这些迭代器指向了另一个容器中的元素。
容器的选用:
vector和deque容器提供了对元素的快速访问,但付出的代价是,在容器的任意位置插入或删除元素,比在容器尾部插入和删除的开销更大,因为要保证其连续存储,需要移动元素;list类型在任何位置都能快速插入和删除,因为不需要保证连续存储,但付出的代价是元素的随机访问开销较大。特征如下:
   1)与vector容器一样,在deque容器的中间insert或erase元素效率比较低;
   2)不同于vector容器,deque容器提供高效地在其首部实现insert和erase的操作,就像在尾部一样;
   3)与vector容器一样而不同于list容器的是,deque容器支持对所有元素的随机访问。
   4)在deque容器首部或尾部删除元素则只会使指向被删除元素的迭代器失效。在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器都失效。
  容器的比较:

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 专门进行数值计算的,增加特殊的数学函数。

   一些容器选用法则:
   1) 如果程序要求随机访问元素,则应使用vector或deque容器;
   2) 如果程序必须在容器的中间位置插入或删除元素,则应采用list容器;
   3)如果程序不是在容器的中间位置,而是在容器首部或尾部插入或删除元素,则应采用deque容器;
   4)如果只需要在读取输入时在容器的中间位置插入元素,然后需要随机访问元素,则可以在输入时将元素读入到一个list容器中,然后对容器排序,再将排序后的list容器复制到vector容器中。
   5)如果程序既需要随机访问,又需要在容器的中间位置插入或删除元素,此时应当权衡哪种操作的影响较大,从而决定选择list容器还是vector或deque容器。注:此时若选择使用vector或deque容器,可以考虑只使用它们和list容器所共有的操作,比如使用迭代器而不是下标,避免随机访问元素等,这样在必要时,可以很方便地将程序改写为使用list容器。
容器适配器
     适配器(adaptor)是标准库中通用的概念,包括容器适配器、迭代器适配器和函数适配器。本质上,适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现,只是发生了接口转换而已。
     标准库提供了三种顺序容器适配器:queue, priority_queue和stack。
     所有适配器都定义了两个构造函数:默认构造函数用于创建空对象,而带一个容器参数的构造函数将参数容器的副本作为其基础值。
     默认的stack和queue都基于deque容器实现,而priority_queue则在vector容器上实现。在创建适配器时,通过将一个顺序容器指定为适配器的第二个类型参数,可覆盖其关联的基础容器类型。例如:
    stack<int, vector<int> > int_stack;  // 此时,int-stack栈是基于vector实现
    对于给定的适配器,其关联的容器必须满足一定的约束条件。stack适配器所关联的基本容器可以是任意一种顺序容器类型,因为这些容器类型都提供了push_back、pop_back和back操作;queue适配器要求其关联的基础容器必须提供pop_front操作,因此其不能建立在vector容器上;priority_queue适配器要求提供随机访问功能,因此不能建立在list容器上。
 
   两个相同类型的适配器可以做==, !=, <, >, <=, >=这些关系运算,只要其基本元素类型支持==和<两个操作即可。这与容器大小比较原则一致。
这与容器大小比较原则一致。
 s.empty() 如果栈为这人,则true;否则返回false
 s.size() 返回栈中元素的个数
 s.pop() 删除栈顶元素,但不返回其值
 s.top() 返回栈顶元素的值,但不删除该元素
 s.push(item) 在栈项压入新元素
 
队列和优先级队列
   标准库队列使用了先进先出(FIFO)的存储和检索策略,进入队列的元素被放置在尾部,下一个被取出的元素则取自队列的首部。
   priority_queue默认使用元素类型的 < 操作符来确定它们之间的优先级关系,用户也可以定义自己的优先级关系。在优先级队列中,新元素被放置在比它优先级低的元素的前面。
 
 q.empty() 如果队列为空,则返回true;否则返回false
 q.size() 返回队列中元素的个数
 q.pop() 删除队首元素,但不返回其值
 q.front() 返回队首元素的值,但不删除该元素
 (注:该操作只适用于队列
 q.back() 返回队尾元素的值,但不删除该元素
 (注:该操作只适用于队列
 q.top()

 返回具有最高优先级的元素值,但不删除该元素
 (注:该操作只适用于优先级队列。MSDN也为queue提供了该操作

 q.push(item) 对于queue,在队尾压入一个新元素;
 对于priority_queue,在基于优先级的适当位置插入新元素

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值