C++ Primer 阅读笔记:迭代器和容器 小结
原创 by zoe.zhang
0.写在前面的话
我是在2011年学的C++,但是那一年恰好是C++11新标准的一年,但是大学上学的C++还是基于C++98的风格的,使用的编译器也是VC6.0,啊,插一句话,虽然VC6现在看起来有些简陋,而且也不支持C++新标准,但是因为它的轻便,以及有些年代感的编码界面,我自己感觉它就像是一柄钝剑,加上是我接触的第一个编译器,因此对它还是怀有敬意的。当然,现在用的VS2013,编程友好,功能强大,也是非常棒的了。它是支持C++11标准的。C++11相对C++98来说有了很多新变化,根据书上说,是为C++打磨了一套新的体系。当年的C++学习的基础现在想来真的很是浅显,不过语言真的很有趣啊,阅读C++ primer这本大厚书的时候,居然很多时候并不觉得枯燥,反而仿佛看到一扇新的大门一样,忍不住惊叹设计的巧妙,啊,不,美妙。
总的来说,自己在学习C++的路上还有许多事情和实践需要做。
这一篇文章是和C++中的容器相关,基本当年大一的时候是没有接触过的内容,如今捧卷来读,只觉自己知道得太少。好了,切入正题。
C++11的标准库得到了广泛的赞誉以及被众多程序员接受和使用。STL标准库中封装了许多复杂优秀的数据结构/容器,也提供了大量的基于迭代器的以及等常用的数据结构算法和操作,
在C++标准中,STL被组织为下面的17个头文件:<algorithm>、<deque>、<functional>、<iterator>、<array>、<vector>、<list>、<forward_list>、<map>、<unordered_map>、<memory>、<numeric>、<queue>、<set>、<unordered_set>、<stack>和<utility>等等。
这一章主要是关于容器的学习记录。容器主要分为顺序容器(如vector,list等),关联容器(map,set等),对于这些容器C++提供了插入、排序,删除、查找等等一系列通用或者部分特殊的常用操作。这里要稍微理解一些这些容器与我们在算法中经常见到的数据结构中的关系。
vector:类似于增强型数组结构的封装;list:链表式结构;
set/map:基于高效的平衡二叉树(红黑树),红黑树具有很好的统计特性,作为关联容器的内部结构。关于红黑树/AV-L树放到其他章节再讨论。
还有一点为什么容器类被称作容器,因为容器实际上是一种类模板,可以像容器一样存放大多数类型的对象。
1.迭代器
迭代器是使用容器的基础,除掉数组、vector、string等对象可以使用下标运算符,迭代器(iterator)是一种更加通用的机制。在使用容器之前需要深刻理解迭代器的用法,STL提供的泛型算法是基于迭代器对象或者说是一对迭代器生成的范围之上的。此处指出一下:严格上string类型不属于容器,但是它支持很多与容器类型的操作,也支持迭代器。迭代器提供了对对象的间接访问,可以从一个元素移动到下一个元素,(在C++11里,对于对象的>/</++/--/==/!=等运算符的定义要注意理解。)迭代器某种意义上有点像更宽泛意义上的指针,在行为上也有点像,它有无效和有效之分,有效的迭代器指向某个元素或者指向尾后元素。
(1)获取迭代器:迭代器类型
有迭代器的容器类型都有拥有返回迭代器的成员。我们主要是使用迭代器,有时候并不关心迭代器的类型,所以可以由编译器决定,也可以显示指定。
auto it_b = v.begin(), it_end = v.end(); // 由编译器决定
auto cit_b = v.cbegin(), cit_end = v.cbegin(); // 返回常量迭代器
vector<int>::iterator it; //显示定义
vector<int>::const_iterator cit;//显示定义,常量,能够读取但是不修改
(2)检查容器是否为空
这里有一个特殊的迭代器,称为“尾后迭代器”,某种程度上类似于一个边界条件,(end iterator),用于构成迭代器范围。通常使用容器第一步需要判断容器是否为空,容器为空时,begin和end函数返回的是同一个迭代器,均为尾后迭代器。或者迭代器遍历容器完毕结束的条件,就是到达尾后迭代器。
if (v.begin() != v.end()){}
(3)迭代器的基本操作:
- *iter:解引用,返回迭代器所指向元素的引用;如果解引用所得到的对象是类,通过箭头运算符将解引用和成员访问结合到一起,iter->mem;
- ++iter,--iter:迭代器从一个元素移动下一个元素;几乎所有迭代器都支持++操作,++操作相对来说更通用,在反向迭代器时要注意理解。(注意:所有标准库容器都支持++递增运算的迭代器,一些容器的迭代器提供了额外的操作,比如vector和string,array,deque的迭代器运算相对丰富)(forward_list迭代器不支持--递减运算符操作)
- ==,!=:判断两个迭代器是否指向同一元素(包括尾后迭代器)
(4)迭代器范围:
迭代器范围的概念是标准库的基础。一个迭代器范围由一对迭代器表示,这里的迭代器区间是左闭合区间[begin,end),从begin开始,但是不包含end。如果begin与end相等,那么范围为空,如果begin和end不等,那么范围至少包含一个元素。
(5)迭代器的种类
除了为容器提供的用于访问数据提供的迭代器之外,标准库还提供了其他几种迭代器。
插入迭代器:绑定在容器上,可以向容器内插入数据;它接受一个数据,生成一个迭代器,当通过insert iterator进行赋值的时候,迭代器调用容器操作来向容器指定位置插入一个元素。
流迭代器;绑定在输入或输出流上,用来遍历关联的的IO流;
反向迭代器:迭代器向后移动,虽然也使用++运算符,但是是向后移动的;(C++ primer 图10.2 )
移动迭代器:顾名思义,用于移动元素。
2.顺序容器
顺序容器中,元素的排放顺序是与其加入容器时的位置相对应的,这种顺序不依赖于元素的值,与元素加入容器时的位置县桂英。
关联容器中,元素的位置由相关联的关键字值决定的。也就是所关联容器中元素的位置与关键字关联。
(1)顺序容器的种类:顺序容器都提供了快速顺序访问元素的能力。
- vector:可变大小数组,支持快速随机访问,可快速增长,但是插入或删除元素可能很慢。
- deque:双端队列,支持快速随机访问,在首尾插入、删除速度很快。
- list:双向链表,双向顺序访问/遍历(链表不支持元素的随机访问),list在任何位置的插入和删除速度都很快,很方便。
- forward_list:单向链表,单向顺序访问。
- array:固定大小数组,支持随机快速访问,不能删除或填加元素。
- string:字符串容器。
关于顺序容器的使用有一些总结:
a. 除了array固定大小,其他顺序容器都提供高效、灵活的内存管理,可以添加、删除、扩张和收缩容器。
b. string 和vector等容器将元素保存在连续的内存空间中,即其元素的内存是连续存储的,可以支持下标操作。
c. 通常使用vector是很好的选择,除非你有更好的理由选择其他容器。
d. 如果在读取时需要插入元素,但是随后需要使用随机访问元素,根据情况可以选择sort重排vector,更通用情况是输入阶段使用list,输入完成后,将list中的内容放入到一个vector中。
(2)容器库的操作
容器型的操作可以看成包括3种类型:一种是通用的,一种各自针对顺序容器、关联容器、无序容器等,还有一种是针对特殊的容器类型。
容器通用操作包括:类型别名,构造函数(默认,拷贝,迭代器范围构造c(b,e),列表初始化),赋值和swap,大小,添加和删除(insert,emplace,erase,clear),关系运算符,获取迭代器.
(3)顺序容器的操作
(4)vector对象是如何增长的:关于内存空间分配的一些讨论;
(5)string库的额外操作
3.关联容器
关联容器中的元素是根据关键字来保存和访问的,顺序中的容器元素按照它们在容器中的位置来顺序保存和访问。
关联容器支持的是高效的关键字查找和访问。、
(1)综述
关联容器的类型主要有map和set两种,根据有序无序,允许重复和不允许重复关键字分为8种关联容器。按照关键字有序保存元素的容器定义在map和set头文件中,无序容器保存在unordered_map和unordered_set中,有序容器实际上按顺序保存元素,通过比较运算符来组织元素。无序容器使用哈希(hash)函数和关键字类型==来组织元素。
举例:set——有序存储的不允许重复关键字的集合; unordered_multi_set——无序存储的通过哈希函数组织、允许关键字重复的集合。
对于有序容器而言,传入的关键字类型,需要定义元素的比较方法,标准库通过使用关键字的类型<运算符比较两个关键字,来组织元素。
(2)map
map是关键字—值对的集合,关键字起到索引的作用,可以被称为“关联数组”,因为它可以通过下标运算符来访问值,只不过下标运算符中传入的是关键字。在文本处理上,字典是一个map一个很好的使用例子。
a. map初始化:
当定义一个map时,需要指明关键字和值的类型,关联容器都定义了一个默认构造函数,创建一个指定类型的空容器。
如果需要对map或者set等容器进行初始化,可以使用列表初始化;将关联容器初始化为另一同类型容器的拷贝,或者使用一个值范围来进行初始化;(迭代器范围);其中需要注意一下multimap或者multiset与map和set的不同。
//map 值初始化
map<string, string>authors = { { "jocye", "" },
{ "Austen", ""},
{ "Dicken", ""} }; //ivec是一个包含重复元素的vector
set<int> iset(ivec.cbegin(), ivec.cend()); //不包含重复元素
multiset<int> miset(ivec.cbegin(), ivec.cend());//包含重复元素
b.map的使用:
//统计出现次数——下标运算符
map<string, size_t> word_count;
string word;
while (cin >> word)
++word_count[word];
for (const auto &w : word_count)
cout << w.first << "occurs" << w.second
<< ((w.second > ) ? "times" : "time") << endl;
这里用到了下标运算符[],下标运算符在map中的特点是,会根据传入的对象去寻找对应的key-value对,如果元素不在map中,会自动创建一个新元素,初始化value为0‘;
map中的元素为pair类型,pair的first成员保存关键字,second成员保存对应的值。
(3)set
set是关键字的简单集合,set支持的是高效的关键字查询操作,检查一个关键字是否在set中,set是最有用的,常见一个场景,定义set保存要忽略的元素,或者检查屏蔽等。
a. 定义一个set的时候,应当指明关键字类型,其初始化可参考上文map的初始化问题。
b.set的使用:
//使用set忽略想忽略的单词
map<string, size_t> word_count;
set<string> exclude = { "the", "but", "and", "or", "an" };//列表初始化
string word;
while (cin >> word)
{
if (exclude.find(word) == exclude.end())
++word_count[string];
}
这里是set一个最常见的用法,set.find(obj),调用返回一个迭代器,如果给定关键字在set中,就指向该关键字,如果不在,find返回尾后迭代器。
(4)关联容器迭代器
map<string, int> word_count;
auto map_it = word_count.begin(); //获取map迭代器
//*map_it是pair类型;可以通过map_it->first;map_it->second 访问键值对
// 注意:map_it->first 关键字是const 不可改变 set<int> myset = { , , , , , , , };
set<int>::iterator set_it = myset.begin();
//显式定义迭代器 不管是begin还是cbegin返回的都是const_iterator
(5)关联容器相关操作
- 遍历操作:递增迭代器遍历
auto map_it = word_count.cbegin();
while (map_it != word_count.cend())
{
cout << i;
cout<<(map_it->first)<<"occurs"<< (map_it->second) << endl;
++map_it;
}
- 添加元素
//set 插入操作:不允许重复元素
vector<int> vec = { , , , , , , , };
set<int> set2;
set2.insert(vec.cbegin(), vec.cend()); //set2 有4个元素 重复元素不插入 接受一对迭代器
set2.insert({ , , , , , , , }); //再插入4个元素 接受一个初始化器列表 //map 向map添加元素:所添加的元素类型是pair
word_count.insert({ "word", }); //{}花括号构造初始化一个pair
word_count.insert(make_pair("word", ));//make_pair显示构造
word_count.insert(pair<string, size_t>("word", ));
insert函数会返回值,对于不包含重复关键字的容器,添加单一元素的insert会返回一个pair,pair的的第一个元素是一个迭代器,指向给定关键字的元素,第二个元素second,是一个bool值,如果关键字已经在容器中,bool值为false,insert不做任何事。如果关键字不在,元素插入容器中,bool值为true。对于允许重复关键字的容器,接受单一元素的容器的insert会返回一个指向新元素的迭代器。
- 删除元素
c.erase(k);//k是一个关键字,返回一个size_type的值,删除元素的数量;
c.erase(it);//it是一个迭代器,删除迭代器it指向的元素
c.erase(b, e);//一对迭代器所表示的范围元素
- 访问元素
访问元素是很重要的一个方法,因为关联容器的一个意义所在就是提供了高效得对元素的访问。主要有find和count两个方法,find返回迭代器,count可以计算出元素出现的次数。
对于map和unordered_map(不允许重复)的类型中,下标运算符提供了最简单的访问和提取元素值的方法,但是下标运算符会将未在map中的元素添加到map中。因此如果不想改变map,所以应该使用find方法。
set<int> myset = { , , , , , , , };
myset.find(); //返回一个迭代器 指向key == 1; 如果未找到,返回set2.end();
myset.count();//返回1; myset.lower_bound(); //返回一个迭代器 指向第一个关键字不小于k ==2 的元素
myset.upper_bound(); //返回一个迭代器 指向第一个关键字大于k ==2 的元素 注意是大于;左闭合区间
myset.equal_range(); //返回一个pair,pair两个元素均是迭代器 表示关键字==k的元素范围----常用于multiset/multimap; // map
multimap<string, string> author;
string search_item = "Alain";
for (auto beg = author.lower_bound(search_item),
end = author.upper_bound(search_item);
beg != end; ++beg)
cout << beg->second; // 如果上限和下限返回相同迭代器,则给定关键字不在容器中
(6)无序容器
无序容器使用hash function和关键字类型==运算符来组织元素,无序容器在存储组织上为一组桶,每个桶保存零个或者多个元素,无序容器使用哈希函数将元素将元素映射到桶。
访问元素的步骤:a、计算元素的哈希值,指出这个元素是放在哪一个桶中(具有相同哈希值的元素保存在相同的桶中)b、桶中存在多个元素时,通过顺序搜索这些元素来查找我们想要的那个。
哈希技术能够获得更好的平均性能。无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
4.泛型算法
标准库容器提供了大量的泛型算法,这些算法独立于特定容器,这些算法是运行于迭代器之上的,不会去做容器的操作。这一概念一定要牢记在心理,也是泛型算法之所以通用的关键所在。
大多是算法都定义在头文件algorithm中,还有numeric中定义了数值的泛型算法。
泛型算法的知识还是相当多的,同时对迭代器的理解也要更深刻,这里我只列举一些基础和浅显的实例,更多知识应当参阅C++primer中。
此外还要注意一点,泛型算法很大一部分跟顺序容器的联系比较紧密,我们通常不对关联容器使用泛型算法,因为对于关联容器,关键字通常是const的,所以不能将关联容器传递给修改或者重排容器的算法。关联容器虽然可以使用读取元素的算法,比如说泛型算法中的find方法,但是其效率远远比不上关联容器自己定义的专用find方法,这一点要注意一下。
对于泛型算法而言,主要可以分为三类:读取元素、改变元素或者重排元素;
除了少数例外,标准库算法都是对一个范围内的元素进行操作,也就是所说的“输入范围”,该输入范围通常使用要处理的第一个元素和最后一个元素之后位置的两个迭代器表示。(再度表明左闭合区间)
(1)标准库find方法
vector<int> vec = { , , , , , };
int val = ;
//find算法:参数表(迭代器1,迭代器2,要查找的值val)
auto result = find(vec.cbegin(), vec.cend(), val);
//如果找到,返回该元素的迭代器;如果找不到,返回vec.cend();
cout << "the value" << val
<< (result == vec.cend() ? "is not present" :"is present") << endl; //find可以用在数组中查找值
int ia[] = { , , , , , };
int *ib = ia;
int ii = ;
int *result = find(begin(ia), end(ia), ii); //传入位置指针
auto result2 = find(ia, ia + , ii); // 范围
(2)accumulate:求和,接收三个参数,定义在头文件numeric中;
//accumulate 的第三个参数类型决定函数使用哪个加法运算符 第三个参数是初值
string sum = accumulate(v.cbegin(), v.cend(), string(""));
(3)重排容器元素的算法:sort unique;
void elim(vector<string> &words)
{
sort(words.begin(), words.end());//重排
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
C++ Primer 阅读笔记:迭代器和容器 小结的更多相关文章
- <C++Primer>第四版 阅读笔记 第二部分 “容器和算法”
泛型算法中,所谓"泛型(generic)"指的是两个方面:这些算法可作用于各种不同的容器类型,而这些容器又可以容纳多种不同类型的元素. 第九章 顺序容器 顺序容器的元素排列次序与元 ...
- C++PRIMER 阅读笔记 第三章
本章主要介绍 string vector 和 bitset, 不能贪多,现在本文主要介绍 string 与 vector 头文件中最好不要使用namespace std, 因为头文件会直接被预处理器放 ...
- C++ Primer 第四版阅读笔记
阅读笔记 初始化 变量定义指定了变量的类型和标识符,也可以为对象提供初始值.定义时指定了初始值的对象被称为是 已初始化的.C++ 支持两种初始化变量的形式:复制初始化和 直接初始化.复制初始化语法用等 ...
- C++ Primer 学习笔记_44_STL实践与分析(18)--再谈迭代器【下】
STL实践与分析 --再谈迭代器[下] 三.反向迭代器[续:习题] //P355 习题11.19 int main() { vector<int> iVec; for (vector< ...
- 《Thinking In Java》阅读笔记
<Thinking In Java>阅读笔记 前四章:对象导论. 一切都是对象. 操作符. 控制执行流程 public在一个文件中只能有一个,可以是一个类class或者一个接口interf ...
- Mybatis3.3——源码阅读笔记
目录 Mybatis--Source阅读笔记 兵马未动,日志先行 异常 缓存 回收机制适配器 回收机制优化缓存 事务缓存 调试型缓存--日志缓存 解析 类型处理器 IO VFS Resource Re ...
- 《Java编程思想》阅读笔记二
Java编程思想 这是一个通过对<Java编程思想>(Think in java)进行阅读同时对java内容查漏补缺的系列.一些基础的知识不会被罗列出来,这里只会列出一些程序员经常会忽略或 ...
- 《STL源代码剖析》---stl_deque.h阅读笔记(2)
看完,<STL源代码剖析>---stl_deque.h阅读笔记(1)后.再看代码: G++ 2.91.57,cygnus\cygwin-b20\include\g++\stl_deque. ...
- 《uml大战需求分析》阅读笔记05
<uml大战需求分析>阅读笔记05 这次我主要阅读了这本书的第九十章,通过看这章的知识了解了不少的知识开发某系统的重要前提是:这个系统有谁在用?这些人通过这个系统能做什么事? 一般搞清楚这 ...
随机推荐
- NLP系列-中文分词(基于统计)
上文已经介绍了基于词典的中文分词,现在让我们来看一下基于统计的中文分词. 统计分词: 统计分词的主要思想是把每个词看做是由字组成的,如果相连的字在不同文本中出现的次数越多,就证明这段相连的字很有可能就 ...
- python基础训练营04-函数
任务四 函数的关键字 函数的定义 函数参数与作用域 函数返回值 一.函数的关键字: def 二.函数的定义: 在Python中,定义一个函数要使用def语句,依次写出函数名.括号.括号中的参数和冒号 ...
- 《学习OpenCV》课后习题解答7
题目:(P105) 创建一个结构,结构中包含一个整数,一个CvPoint和一个 CvRect:称结构体为"my_struct". a. 写两个函数:void Write_my_st ...
- awk,rsync,重启,maxdepth一层目录,登录,开机自启动
有100个日志文件,每个文件大约1G,每条日志都以 “H:i:s” 的时间格式开头,如: 05:02:04 xxx yyy zzz 因为是日志文件,所以肯定以时间为顺序的,现在可以确定的是,在某个文件 ...
- 深入研究java.lang.Runtime类(转)
一.概述 Runtime类封装了运行时的环境.每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接. 一般不能实例化一个Runtime对象, ...
- 【bzoj2124】等差子序列 STL-bitset
题目描述 给一个1到N的排列{Ai},询问是否存在1<=p1<p2<p3<p4<p5<…<pLen<=N (Len>=3),使得Ap1,Ap2,A ...
- 关于全球唯一标识符GUID
在C#中的语法: Console.WriteLine(System.Guid.NewGuid()); Console.ReadKey(); System.Guid.NewGuid().ToString ...
- hbase(0.94) get、scan源码分析
简介 本文是需要用到hbase timestamp性质时研究源码所写.内容有一定侧重.且个人理解不算深入,如有错误请不吝指出. 如何看源码 hbase依赖很重,没有独立的client包.所以目前如果在 ...
- ACM International Collegiate Programming Contest, Egyptian Collegiate Programming Contest (ECPC 2015)
A.Arcade Game(康拓展开) 题意: 给出一个每个数位都不同的数n,进行一场游戏.每次游戏将n个数的每个数位重组.如果重组后的数比原来的数大则继续游戏,否则算输.如果重组后的数是最大的数则算 ...
- underscore的bind和bindAll方法
bind方法和bindAll方法都是用来设定函数的this值的,区别是调用方式不同. var xiaoming = { say:function(){ console.log('I am xiaomi ...