• C++中常用的std标准容器


        从c++11标准以来,c++中std定义的几种容器的效率非常高,优化的非常好,完全没有必要自己去定义类似的数据结构。了解使用它们,可以满足90%的日常编程需要。该篇文章基于c++11标准,从用户角度来介绍常用的顺序容器与并联容器(如果想从内部了解它们是怎么实现的,推荐看看《std源码剖析》这本书)。它们包括:

    顺序容器:

    • vector
    • string (它不是类模板)
    • list
    • forward_list
    • deque
    • queue
    • priority_queue
    • stack

    有序关联容器:

    • map
    • multimap
    • set
    • multiset

     无序关联容器:

      unordered_map

      unordered_multimap

      unordered_set

      unordered_multiset

    力推网站: https://en.cppreference.com/w/cpp/container, 里面介绍的绝对很全的,绝对比本篇文章好太多太多。

    顺序容器

    1. vector容器

    a. vector的定义与初始化

    // T 表示实例化类模板时使用的类型
    
    vector<T> v1                 // 默认初始化, 此时v1为空。
    vector<T> v1(v2)              // 执行的copy初始化,此时v1与v2的内容相同
    vector<T> v1 = v2           // 与上面相同,都会执行copy构造函数
    vector<T> v1(n)              // 此时v1的size大小为n ,它里面的值是根据T的类型进行默认初始化的
    vector<T> v1(n, a)           // v1的初始化为n个值为a的元素
    vector<T> v1{a, b, c}       // 列表初始化,v1内现在的元素就是a, b, c (这是c++11标准新入的)
    vector<T> v1 = {a, b, c}    // 与上面相同

    列表初始化是什么?

    对于上面的几种初始化方法,最常用的有三种, 1. 默认初始化,这里vector为空;2.copy初始化,这时用另一个vector初始化该vector 3. 列表初始化,为vector 初始化一些初始值。   几乎或很少在初始化vector的时候去设定它的size大小,因为vector的push_bask是非常高效的,甚至比提前设置它的大小更高效(见c++primer 页)

    b. vecotr常使用的操作 

    1. 属性操作

    v1.size()      //v1内已经存放的元素的数目
    v1.capacity()    // v1现有的在存储容量(不再一次进行扩张内存空间的前提下)
    v1.empty()     // 判断v1是否为空
    v1.max_size()    // 返回vector可以存放的最大元素个数,一般这个数很大,因为vector可以不断调整容量大小。
    v1.shrink_to_fit()  // 该函数会把v1的capacity()的大小压缩到size()大小,即释放多余的内存空间。

    2. 访问操作:访问操作都会返回引用,通过它,我们可以修改vector中的值。

    v1[n]        // 通过下标进行访问vector中的元素的引用 (下标一定要存在 ,否则未定义,软件直接崩了)
    v1.at(n)       // 与上面类似,返回下标为n的元素的引用,不同的是,如果下标不存在,它会抛出out_of_range的异常。它是安全的,建议使用它。
    v1.front()      // 返回vector中头部的元素的引用(使用时,一定要进行非空判断)
    v1.back()      // 返回vector中尾部的元素 引用(使用时,一定要进行非空判断)

    3. 添加操作:

    v1.push_back(a)        //在迭代器的尾部添加一个元素
    v1.push_front(a)        // vector不支持这个操作
    v1.insert(iter,  a)        // 将元素a 插入到迭代器指定的位置的前面,返回新插入元素的迭代器(在c++11标准之前的版本,返回void)
    v1.insert(iter, iter1, iter2)       //把迭代器[iterator1, iterator2]对应的元素插入到迭代器iterator之前的位置,返回新插入的第一个元素的迭代器(在c++11标准之前的版本, 返回空)。

        在c++11标准中,引入了emplac_front()、 emplace()、emplace_back(), 它们分别与push_front()、insert()、 push_back()相对应,用法与完成的动作作完全相同,但是实现不一样。 push_front()、insert()各push_back()是对元素使用copy操作来完成的,而emplac_front()、 emplace()和emplace_back()是对元素使用构造来完成的,后者的效率更高,避免了不必要的操作。因此,在以后更后推荐使用它们。

        

    4. 删除操作:

    v1.erase(iterator)     // 删除人人迭代器指定的元素,返回被删除元素之后的元素的迭代器。(效率很低,最好别用)
    v1.pop_front()       //vector不支持这个操作
    v1.pop_back()      //删除vector尾部的元素 , 返回void类型 (使用前,一定要记得非空判断)
    v1.clear()         //清空所有元素

     5. 替换操作:

    v1.assign({初始化列表})    // 它相当于赋值操作,
    v1.assign(n, T)        // 此操作与初始化时的操作类似,用个n T类型的元素对v1进行赋值
    v1.assign(iter1, iter2)     // 使用迭代器[iter1, iter2]区间内的元素进行赋值(该迭代器别指向自身就可以),另外,只要迭代器指的元素类型相同即可(存放元素的容器不同,例如:可以用list容器内的值对vector容器进行assign操作,而用 "=" 绝对做不到的。
    v1.swap(v2)      // 交换v1与v2中的元素。  swap操作速度很快,因为它是通过改变v1与v2两个容器内的数据结构(可能是类似指针之类的与v1和v2的绑定)完成的,不会对容器内的每一个元素进行交换。 这样做,不仅速度快,并且指向原容器的迭代器、引用以及指针等仍然有效,因为原始的数据没有变。在c++ primer 中建议大家使用非成员版本的swap()函数,它在范型编程中很重要。

    c. 小结:

        1. vector容器最重要的特性是: 它在一段连续的内存空间中存储元素, 可以在常量时间内对vector容器进行随机访问,并且可以很高效的在vector的尾部进行添加与删除操作,在vector中间或头部添加与删除元素的效率很低。
        2. 只要对vector进行增加与删除元素的操作,都会使迭代器、指针、引用失效(可能有时候它们仍然有效,不过是随机的,绝对不能作这样假设)。所以当使用vector的迭代器、引用和指针时,一定要杜绝对他们进行增加与删除元素的操作
        3.  对于vector的迭代器,它除了可以进行  ++iter   与   --iter   的操作之外 ,还可以进行算术运算,例如: iter + n 、 ::difference_type a = iter1 - iter2 //它的返回类型为 ::difference_type,例如vector<int>::difference_type (另一个也支持迭代器算术运算的容器为string)
        4. 待补充!

     2. string容器

         string与vector类似,但是string不是一种类模板,而就是一种类型,因为它专门用于存放字符的(存放的元素类型已经明确),所以没有设计为类模板。它的所有特性与vector相同,包括存储在连续的空间/快速随机访问/高效在尾部插入与删除/低效在中间插入与删除等, string的迭代器也支持算术运算。  实际上,就可以把string类型看作为vector<char>类型, vector的所有特性都适合与string类型。当然,因为string类型比vector模板更特例化一些,因此它肯定具有一些自己特有而vector没有的特性,下面总结一下。

     

    在陈述之前,首先说明:

    1. 在string中(有一些也适用于C风格的字符串),我们可以使用一组迭代器/单个迭代器(从此迭代器开始到字符串末)/位置+长度表示范围/单个位置(从此位置到字符串末)来表示字符串中的范围, 这样的参数记作range.

    2.  可以使用列表初始化的字符串/使用字符串+range的组合形式表示的子字符串 / 字面值常量(如“china”)来表示字符串。  这里的字符串包括string类型的字符串和C风格的char* 字符串。  字符串使用字符args 表示。

    正因为pos和args的样式可以随意组合,所以string的操作函数的参数是多种的,因此它的重载函数数目很多,由于对于insert(pos, args)/append(args)/erase(pos,args)/replace(pos, args)等操作。

     

    a.   string的初始化

    相对于vector类型来说, string 增加一个使用字面值类型进行初始化,即:

    1 string a("xiaoming")
    2 string a = "xiaoming"

    b. string中包含的专有的操作(相对于vector来说)

    1.  string的添加与替换

    在string中,增加了append()与 replace()函数

    str.append(args)    // 在尾部添加一个字符或一个字符

    str.replace(pos, args)    // 在尾部添加一个字符或一个字符 ,它的重载函数很多,共16个。

    2. string的访问子字符串:

    str.substr(_pos, n)  //该函数可以获得原字符串中的部分字符, 从pos开始的n个字符,当_pos超过范围时,会抛出out_of_range的异常。

    3. str的搜索操作:

    str.find(args)  //查找args 第一次出现的位置

    str.rfind(args)  //查找args最后一次出现的位置

    str.find_first_of(args)   //搜索的是字符, 第一个是args里的字符的位置

    str.find_last_of(args)   // 搜索的是字符, 最后一个是args里的字符的位置

    str.find_first_not_of()  // 搜索的是字符,第一个不是args里的字符的位置

    str.find_last_not_of()  // 搜索的是字符, 最后一个不是args里的字符的位置

    4. str的大小操作:

    str.length()   // 该函数与str.size()函数完成一样,只是名字不同而已罢了。只所以这样搞的原因,可能开发人员感觉length更适合字串符,size更适合容器吧。

    c字符串的转换函数

    1. 由数值转换为字符串:

    to_string(val): 

    2. 由字符串转换为数值:(要转换的string的第一个非空白符必须是数值中可能出现的字符,处理直到不可能转换为数值的字符为止,以下内容来自:c++primer)

    stoi(str, pos, base)    // 字符串转换为整型,其中str表示字符串,  pos用于表示第一个非数值字符的下标(意思就是我给函数传入一个地址,它会对它进行赋第一个非数值字符的位置), base表数值的基数,默认为10,即10进制数。
    stol(str, pos, base)    // 转换为long
    stoul(str, pos, base)    // 转换为 unsigned long
    stoll(str, pos, base)    // 转换为 long long
    stoull(str, pos, base)   // 转换为unsigned long long
    stof(str, pos)      // 转换为float
    stod(str, pos,)     // 转换为double
    stold(str, pos,)     // 转换为long double

    d 对字符的操作(在cctype头文件中,并不属于string头文件的范围,但是关系很紧密的)

    以下内容来自:c++ primer 第五版p82, 只写出部分常用来的(字母:alpha, 数字:number或digit)

    isalnum(c)  // 当为字母或数字时为真

    isalpha(c)  // 当为字母时为真

    isdigit(c)  // 当为数字时真

    islower(c)  // 当为小写字母时为真

    issupper(c)  // 当为大写字母时为真

    isspace(c)  // 当为空格时为真

    tolower(c)  // 转换为小写字母, 当本身为小写字母时,原样输出

    toupper(c)  // 转换为大写字母, 当本身为大写字母时,原样输出

    3. list 容器

        与vector和string相比,list内部的实现为一个双向链表,它的元素不是存储在连续的内存空间中,而是非连续的,这就决定了它不能在常量时间内完成对元素的随机访问,只能从头到尾的遍历一遍。  因为它是用双向链表实现的,所以,它的一大特性就是它的迭代器永远不会变为无效(除非这段空间不存在了),即无论增加、删除操作,都不会破坏迭代器。

    大多数对vector的操作也适合于list,由于底层实现不同,有也差异:

    list与vector的差别:

    1. list支持push_front()、pop_front()操作

    2. list不支持vector中的随机访问操作,即使用v1.at( )和v1[ ] 操作。

    3. list的删除与增加元素的操作不会破坏迭代器,而 vector与string 会使迭代器失效。

    4. list 内部增加了一个sort()的方法,用于实现排序,不过呢,反正我感觉基本不用它,直接用<algorithm>里的范型sort()更好啊啊。

     5. list增加了一个类似insert()的函数,为splice( ) :该函数可以实现在常数时间内把一个list  插入到另一个list内,与insert()的区别在于insert是进行copy, 而splice()直接操作的链表的指针指向。它有好几个重载函数。

     6. list的去重复函数: unique(); 该函数的作用是去除连续重复的元素,参数即可以为空,也可以传入一个二元谓词,用于确定相等的比较算法。  因为unique()函数可能去除连续重复的元素,因此,很依赖配合上sort()函数使用啊。

    7. list的合并函数merge():  该函数就是合并两个list, 它在合并过程中会在两个链表之间进行来回的比较,如果原来的两个list是有顺序的,合并之后的结果也是有序的,如果合并之前是无序的,合并之后也是无序的。反正吧,这个比较就这样。

    4. forward_list容器

     forward_list的实现是使用单向链表(list为双向链表),  在操作单向链表的时候,为了对一个元素进行删除与添加,都需要访问到该元素的前趋节点,因此呢,forward_list的会有insert.after()emplase.after()/erase.after()等操作, 另外forward_list也没有size()操作,原因在于为了尽可能让forward_list与手写的单向链表的效率相同。说实话呢,forward_list操作起来有点反人类,用起来有点不方便,我个人比较买习惯使用list,但是list相对forward_list的内存空间花费更多。

    以后什么时候用它的时候,再来介绍。

    5. deque容器(double-end queue, 双端队列)

    6.

     

    有序关联容器

        关联容器与顺序容器最大的区别在于关联容器没有下标,都过键值或 值本身进行索引。有序关联容器内部通过红黑树实现的,当搜索一个元素时,具有O(logn)的平均复杂度,而无序的关联容器在底层是通过散列表(哈希函数映射)实现的,当搜索一个元素时,通常O(1)的平均复杂度,最坏为O(logn), 下面介绍它们。

    1. map 容器

    在介绍map之前,必须先介绍pair 类型。

     pair类型:

    1. pair类型定义在头文件utility中。
    2. pair类型为一个结构体类型的模板,(在c++中结构体与类,除了默认的访问符不同,没有其它任何区别)
    3. pair 有两个public的数据成员,分别为first与second.
    4. pair的初始化与大多数结构体或类的初始化相同:

    • pair<int, string> sb //初始化一个默认值的pair对象sb, 它的first是默认初始化的(0,内置类型默认初始化大多数应该是未定义的啊,它这是为0), second也是采用默认初始化(空字符串)
    • pair<int, string> sb(1, "japan"); //很常见的初始化方法
    • pair<int, string> sb = (1, "japan");
    • pair<int, string> sb{1,"japan"} //c++11中的列表初始化方法
    • pair<int, string> sb = {1, "japan"}
    • 可以调用make_pair()模板函数,返回一个pair对象:


        1. map是用于存放键-值对的容器,它使用pair的first数据成员表示键(key),second数据成员表示对应的值(value),所以呢,map是存放pair类型对象的容器。在map中,key都是固定的,一旦使用就不可以改变,而value是可以改变的, 因此会把pair类型的first数据成员的类型声明为const。

        2. map的特性之一是:按value的大小进行有序存放(unordered_map是无序的), 因此,构造mqp容器时,要求它的key类型必须能够比较大小,当使用自定义的类类型时,
    应该把重载的 operator< 运算符传递给map, 例如:

    1 // 添加相关代码
    2 
    3 
    4 
    5 ..

        3.在map中:

    • ::value_type表示"键-值 对"类型
    • ::key_type表示键类型,vlue类型
    • ::mapped_type 表示值的类型

    例如: map<int, string>, 则 map<int, string>::value_type 与pair<int, string>等价, map<int, string>::key_type与int等价, map<int, string>::mapped_type与string等价;

        4. map的访问操作:

    • map同样支持使用迭代器,它会返回指向 pair类型的对象 的迭代器
    • map 使用[]运算符 通过key来访问对应的 value ,如果访问的key不存在,则会自动添加一个对应的pair 对象,其中它的value采用默认值。因此,当通过key来访问map时,
    • map不能是const类型。
    • map 使用at()成员函数 通过key来访问对应的value, 如果访问的key不存在,则会抛出一个out_of_range的异常;

        5. map的添加与删除操作:

    • insert()或emplace()操作: 当向map中插入不存在的元素(指key值不同)时,可以插入成功,当插入一个已经存在key值的pair对象时,ma不会作任何改变。因此,当对map进行插入操作时,需要知道有没有插入成功。insert()与emplace()函数的 返回值也是一个pair类型,first为一个迭代器,指向插入时的键值对应的pair对象(可能是新插入的,也可能是已经存在的), second是一个bool类型,它表示是否插入成功(例如:当map中已经存在待插入的值时,为false)
    • erase()操作:它有三个版本,前两个版本与顺序容器相同,使用迭代器指定一个位置或一对迭代器指定一个范围,这时返回值为一个迭代器,指向删除之后的下一个元素;第三个版本的erase()很不错,我很喜欢,它的参数为key值,删除对应key值的pair()对象, 返回值为成功删除的个数(可能为0或1,在multimap中可能为n)

        6.查找操作

    • find(key): 查找一个特定key值的pair对象,如果找到就返回对应的迭代器,如果找不到,就返回.end()迭代器。
    • count(key):统计在map容器中特征key值的pair对象的个数.(在multimap与multiset中很有用的)
    • equal_range(key) // 返回一个pair类型,first表示low_bound, second表示upper_bound;
    • lower_bound(key) //返回迭代器,对应第一个大于等于key的元素
    • upper_bound(key) //返回迭代器,对应第一个大于key的元素 (说明:其实,最后这四个函数,在multimap与multiset中是非常有用的) 

    2. multimap容器:

        与map容器相比,区别在于multimap允许键值重复,即一个键值可能对应多个value。所以呢,相应的操作会有一些变化,例如:multimap不可以像map中使用key 作为索引(使用operator[]和at()成员函数)进行访问元素(因为对应的value可能是多个),multimap的插入操作一定会成功等,除此之外,它们的性相同, 不多介绍。

    3. set容器:

        set容器与map容器的唯一区别在于:存放的元素类型不同: map存储的是键-值对,即pair类型,而set中只存放键值。正因为如此,所以:

    • 1. set只有::value_type与key_type类型,没有::mapped_type类型;
    • 2. set不需要索引访问操作(通过operator[]和at()函数)

        除此之外, set与map也没有什么其它区别了。

    4. multiset容器:

        multiset容器相对于set容器,允许它容器内部的元素重复。没有其它区别了。

    无序关联容器:

    1. unordered_map容器:

    d

    2. unordered_multimap容器:

    d

    3. unordered_set容器:

    d

    4. unordered_multiset容器:

    d

  • 相关阅读:
    [轉]Discuz!NT 数据库读写分离方案
    [轉]最流行的PHP MVC框架
    [轉]关于MS SQL的执行计划
    [轉]Oracle读写分离架构
    [轉]資料庫讀寫分離
    [轉]SQL Server 老是死锁,或提示进程已牺牲,怎麽解决
    [轉]让IIS支持解析json
    [轉]jquery.validate全攻略
    [轉]Flex判断对象的类型
    [轉]mssql 数据表修复方法
  • 原文地址:https://www.cnblogs.com/yinheyi/p/7504041.html
Copyright © 2020-2023  润新知