• vector学习


    之前看了一遍容器和迭代器,但听了学长说的,感觉我的理解有点偏差。打算来学习学习容器的具体用法,敲一敲。

    先来学习顺序容器vector的用法。


    类似于C风格的数组;元素内存空间连续,每个元素有自己的槽。在vector中可建立索引。可以在任何位置添加或删除新元素,需要线性时间。在尾部执行操作时,实际运行时间为摊还常量。(那么什么是摊还常量呢?)。随机访问单个元素的复杂度为常量时间。

    概述

    在vector头文件中被定义为一个带有2个类型参数的类模板:一个参数为要保存的元素类型,另一个参数为分配器(allocator)类型。

    template <class T, class Allocator = allocator<T>> class vector;

    Allocator参数指定内存分配器对象的类型,客户可设置内存分配器。(这章介绍的都是默认模板参数的内存分配器)

    固定长度的vector

    vector最简单的用法莫过于把它当做C风格数组的替代品(长度固定)。vector提供了一个可以指定元素数量的构造函数,还提供了重载的operator[]来访问和修改元素。

    同时C++标准指出,通过operator[]来访问vector边界之外的元素,是UB。

    除了使用operator[]运算符之外,还能用at(),front(),back()访问元素。

    at() = operator[],不过at()会检查边界,并在越界时抛出out_of_range。

    front()和back()分别返回第一个元素和最后一个元素的引用。

    下面敲一个来“标准化”考试分数的程序,经过标准化后,最高分100,其他所有分数据此调整。它创建了一个带有10个double值的vector,然后读入10个值,将每个值除以最高分,再乘以100,最后打印出新值。

    vector<double> doubleVector(10);
    
    double max = -numeric_limits<double>::infinity();
    
    for(size_t i =0; i < doubleVector.size();i++){
        cout << "Enter score " << i+1 << ": ";
        cin >> doubleVector[i];
        if(doubleVector[i]>max){
            max = doubleVector[i];
        }
    }
    
    max /= 100.0;
    for(auto& element: doubleVector){
        element /= max;
        cout << element << " ";
    }
    

    这里要注意的几点是:(1).第一个for循环使用size()方法来确定容器元素的个数。

    (2).基于区间的for循环中用auto&而不是auto,因为这里要用引用,才能在每次迭代中修改元素。

    动态长度的vector

    vector真正好用之处在于它的动态增长。如前面的处理分数程序,如果要再加上一项任意数量的要求,那么则有:

    vector<double> doubleVector;
    
    double max = -numeric_limits<double>::infinity();
    
    for(size_t i = 1; true; i++){
    	double temp;
        cout << "Enter score " << i << "(-1 to stop)";
        cin >> temp;
        if(temp == -1) break;
        doubleVector.push_back(temp);
        if(temp > max) max = temp;
    }
    
    max /= 100.0;
    for(auto& element: doubleVector){
        element/= max;
        cout << element << " ";
    }
    

    这创建了一个不包含元素的空vector,然后每读取一个值,通过push_back()方法添加到vector,push_back()能为新元素分配空间。基于区间的for循环不需要做修改。

    详解

    构造函数和析函数

    默认的构造函数的创建一个不包含元素的vector

    vector<int> intVector;

    但也可以指定元素个数,并指定元素的值,如:vector<int> intVector(10,100)这个便指定了10个元素,初始化值为100

    如果没有提供默认值,则对新对象进行0初始化。0初始化通过默认构造函数创建对象,将基本的整数类型初始化为0,浮点数0.0,指针类型nullptr。

    还可以创建内建类的vector,如下所示:vector<string> stringVector(10,"Hello");

    用户自定义的类也可:

    class Element{
        public:
        	Element(){}
        	virtual ~Element() = default;
    };
    ...
    vector<Element> elementVector;
    

    还可以用初始化列表:vector<int> intVector({1,2,3,4,5,6});

    initializer_list还可以用于统一初始化:

    vector<int> intVector1 = {1,2,3,4,5,6};

    vector<int> intVector2{1,2,3,4,5,6};

    还可以在堆上分配:auto elementVector = make_unique<vector<Element>>(10);

    复制和赋值

    存储对象,其析构函数调用每个对象的析构函数。vector类的复制构造函数和赋值运算符对vector中的元素执行深赋值。所以,我们应该通过引用或者const引用向函数和方法传递vector来提高效率。

    除了普通的复制和赋值运算符,还有assign()方法,删除所有元素,并添加任意树目的新元素。此方法适用于vector的重用。下面举个例子:intVector包含10个默认值为0的元素,然后通过assign()删除所有的10个元素,并以5个值为100的元素替代:

    vector<int> intVector(10);
    //other code
    intVector.assign(5, 100);
    

    而且assign还能接收initializer_list。现在intVector有4个具有给定值的元素:intVector.assign({1,2,3,4});

    vector还提供了swap()方法,可以交换两个vector的内容,并且具有常量时间复杂度。举例:

    vector<int> vectorOne(10);
    vector<int> vectorTwo(5,100);
    vectorOne.swap(vectorTwo);
    

    比较

    vector提供了:==,!=,<,>,<=,>=这6个重载的比较运算符。如果两个vector的元素数量相等,且对应元素也相等,那么2个vector相等。其比较用字典顺序(和字符串比较差不多)

    下面是一个比较元素类型为int的两个vector的程序:

    vector<int> vectorOne(10);
    vector<int> vectorTwo(10);
    
    if(vectorOnve == vectorTwo){
        cout << "equal!" << endl;
    }else{
        cout << "not equal!"<< endl;
    }
    
    vectorOne[3] = 50;
    if(vectorOne < vectorTwo){
        cout << "vectorOne is less than vectorTwo" << endl;
    }else{
        cout <<"vectorTwo is not less than vectorTwo" << endl;
    }
    

    vector迭代器

    先用迭代器将前面那个区间循环替代掉:

    for(vector<double>::iterator iter = begin(doubleVector);
       	iter != end(doubleVector); ++iter){
        *iter /= max;
        cout << *iter << " ";
    }
    

    这里: vector<double>::iterator iter = begin(doubleVector);。每个容器都定义了一种名为iterator的类型,来表示该容器类型的迭代器。begin()返回引用第一个元素的迭代器。

    然后判断iter是否遇到了元素序列的尾部。++iter则是递增迭代器,以引用vector下一个元素。

    循环体里面包含的两句。第一行通过*解除引用iter,从而获得iter引用的元素,然后给这个元素赋值。第二行再次解除引用,将元素流式输出到cout

    可以通过auto来简化迭代器初始化:

    for(auto iter = begin(doubleVector);
       	iter != end(doubleVector); ++iter){
        ...
    }
    

    访问对象元素中的字段

    如果迭代器是对象,那么可以通过->来调用对象的方法或者访问对象成员。如下列程序,建立了包含10个字符串的vector,然后遍历所有字符串,给每个字符串追加一个新的字符串:

    vector<string> stringVector(10,"hello");
    for(auto it = begin(stringVector);it != end(stringVector);++it){
        it->append(" there");
    }
    

    或者用基于区间的循环:

    vector<string> stringVector(10,"hello");
    for(auto& str : stringVector){
        str.append(" there");
    }
    

    const_iterator

    正如const一样,对const对象调用begin和end,或者cbegin和cend,将会得到const_iterator。这是只读的,不能通过const_iterator修改元素。iterator始终可以转换为const_iterator。因此底下的行为是安全的:vector<type>::const_iterator it = begin(myVector);

    然而,const_iterator不能转换为iterator.

    在使用auto推断时,应该用cbegin和cend来返回const迭代器。基于区间也可以在区间元素前加const来强制使用const_iterator

    迭代器的安全性

    通常情况下,迭代器的安全性和指针接近:不安全。比如:

    vector<int> intVector;
    auto iter = end(intVector);
    *iter = 10;
    

    end()对空容器使用,返回的迭代器越过了尾部,试图解除引用会产生UB。(不会有检查的行为。)

    如果使用了不匹配的迭代器,则可能出现其他UB行为:

    vector<int> vectorOne(10);
    vector<int> vectorTwo(10);
    
    for(auto iter = begin(vectorTwo); iter != end(vectorOne); ++iter){
        //Loop body
    }
    

    其他迭代器操作

    vector迭代器是随机访问的,因此可以自由前移、后移、跳跃。下列代码将第5个元素的值改为4:

    vector<int> intVector(10);
    auto it = begin(intVector);
    it += 5;
    --it;
    *it = 4;
    

    迭代器?索引?

    前面写的功能更加用普通的索引也可以完成。那么为什么要用迭代器呢?

    • 使用迭代器可以在任意位置 插入、删除元素或者元素序列。
    • 迭代器可使用标准库算法。
    • 通过迭代器顺序访问元素,通常比索引效率高(vector没有,但是list,map,set中效率提高)。

    在vector中存储引用

    需包含fuctional头文件,在容器中存储std::reference_wrapper。std::ref()和cref()用于创建非const和const reference_wrapper实例。

    string str1 = "Hello";
    string str2 = "World";
    
    vector<reference_wrapper<string>> vec{ ref(str1) };
    vec.push_back(ref(str2));
    
    vec[1].get() += "!";
    
    cout << str1 <<" "<< str2 << endl;
    

    添加和删除元素

    前面push_back可以向vector追加元素。还可以用pop_back()来删除元素。(它不会返回删除的元素,要返回得先调用back()来获得元素。)

    通过insert()可以在任意位置插入元素,这个方法在迭代器指定位置添加一个或多个元素。它有5种重载形式:

    • 插入单个元素
    • 插入单个元素的n份副本
    • 从某个迭代器范围插入元素。迭代器范围是半开区间,所以只包含初始迭代器,不包含尾部迭代器。
    • 使用移动语义,将给定的元素转移到vector中,插入一个元素。
    • 向vector中插入一列元素,这列元素是通过initializer_list指定。

    通过erase()在vector中删除元素,clear()删除所有元素。erase有2个形式:1.单个迭代器,删除单个元素。2.2个迭代器,删除范围元素。

    如果要删除满足条件的多个元素,以之前的想法是写个循环遍历所有元素。但这个方法有平方复杂度。可以使用删除-擦除惯用法(remove-erase-idiom),线性复杂度。

    • insert(const_iterator pos, const T& x):将x插入pos位置
    • insert(const_iterator pos, size_type n, const T& x):将x 值在位置pos插入n次。
    • insert(const_iterator pos, InputIterator first,InputIterator last):将[first,last)范围内的元素插入pos
    template<typename T>
    void printVector( const vector<T>& v){
        for(auto& element : v) {
            cout << element <<" ";
            cout << endl;
        }
    }
    
    vector<int> vectorOne = { 1, 2, 3, 4, 5};
    vector<int> vectorTwo;
    
    vectorOne.insert(cbegin(vectorOne)+3, 4);
    
    for(int i = 6; i<=10; i++){
        vectorTwo.push_back(i);
    }
    
    printVector(vectorOne);
    printVector(vectorTwo);
    
    //Add all the elements from vectorTwo to the end of vectorOne
    vectorOne.insert(cend(vectorOne),cbegin(vectorTwo),cend(vectorTwo));
    printVector(vectorOne);
    
    //now erase the numbers 2 through 5 in vectorOne
    vectorOne.erase(cbegin(vectorOne)+1,cbegin(vectorOne)+5);
    printVector(vectorOne);
    
    //clear vectorTwo entirely
    vectorTwo.clear();
    
    //add 10 coples of the value 100
    vectorTwo.insert(cbegin(vectorTwo), 10, 100);
    
    //decide we only want 9 elements
    vectorTwo.pop_back();
    printVector(vectorTwo);
    

    移动语义

    所有的标准库容器都包含移动构造函数和移动赋值函数,从而实现移动语义。这样的一大好处是可以通过传值的方式从函数返回标准库容器,而不会降低性能。

    vector<int> createVectorOfSize(size_t size)
    {
        vector<int> vec(size);
    	int contents = 0;
    	for(auto& i : vec){
        	i = contents++;
    	}
        return vec;
    }
    ...
    vector<int> myVector;
    myVector = createVectorOfSize(123);
    

    如果没有移动语义,那么每次都进行防复制的话,性能会有很大影响。

    push操作在某些情况下也会通过移动语义提升性能。例如,假如有一个类型为字符串的 vector,如下所示:

    vector<string> vec

    向这个vector添加元素,如下所示:

    string myElement(5,'a') //construct the string"aaaaa"
    vec.push_back(myElement);
    

    由于myElement不是临时对象,所以pushback会生成其副本,然后存入vector。vector类还定义了push_back(const T& val)的移动版本。如果用move(),则可以避免这种复制:vec.push_back(move(myElement));。这之后,myElement处于有效但不确定状态,不应该再使用myElement。

    也可以这样:vec.push_back(string(5,'a'));,string生成一个临时string对象,然后push_back将它直接move过去,避免了复制。

    emplace操作

    放置到位。

    emplace_back()不会复制或移动任何数据,只是分配空间,然后就地构建对象。如:vec.emplace_back(5,'a')

    emplace以可变参数模板的形式接收可变数目的参数。

    从C++17开始,emplace_back()返回已插入元素的引用。在17之前,它的返回类型是void。

    还有个emplace()方法,可以在指定位置就地构建对象,并返回所插入元素的迭代器。

    算法复杂度和迭代器失效

    在vector中插入或删除元素,会导致后面的所有元素向后移动,或向前移动。因此,它们都才用线性复杂度。此外,因为移动的原因,该点和其后的所有迭代器在操作完后都失效了。迭代器不会自己移动。

    vector内部的重分配可能导致引用vector中元素的所有迭代器失效。!

    内存分配方案

    vector会自动分配内存来保存插如的元素。vector要求元素必须放在连续的内存,由于不可能请求在当前内存块的尾部追加内存,因此vector在申请更多内存时,要在另一个位置分配一块新的更大的内存块,然后将所有元素复制/移动到新的内存块。这很耗时,所以在执行重分配时,会分配比所需内存更多的内存,以尽量避免复制转移过程。

    按道理,因为抽象原则,使用者不用考虑vector内部的内存分配方案,但是不然。

    1).效率。vector分配方案能保证元素插入采用摊还常量时间复杂度:也就是说大部分操作都采用摊还常量,但是也有线性时间(需要重新分配内存),如果关注运行效率。那么可以控制vector执行内存重分配的时机。

    2).迭代器失效。重分配会使引用vector内元素的所有迭代器失效。因此,vector接口允许查询或控制vector的重分配。如果不显式地控制重分配,那么应该假定每次插入都会导致重分配已经所有迭代器失效。

    大小和容量

    vector提供了两个可获得大小信息的方法:size()和capacity()

    前者返回元素的个数,后者则返回重分配之前可以保存的元素个数。因此在重分配之前还能插入的元素个数为capacity()-size()

    C++17中引入了非成员的std::size()和std::empty()全局函数。这些与用于获取迭代器的非成员函数类似(begin(),end()等)。非成员函数的size和empyt可以用于所有容器,也可以用于静态分配的C风格数组(不通过指针访问),以及initializer。

    vector<int> vec{ 1,2,3 };
    cout << size(vec) << endl;
    cout << empty(vec) << endl;
    

    预留容器

    如果不关心效率和迭代器失效,那就不需要人为控制内存分配。但如果希望尽可能高效,或者确保迭代器不失效,就可以强制预先分配足够的空间,来保存所有元素。

    另外一种方法是调用reserve(),负责分配保存指定数目元素的足够空间。

    另外一种预分配空间的方法是在构造函数中,或者通过resize()或assign()方法,指定vector要保存的元素数目。会创建指定大小的vector。

    直接访问数据

    在内存中连续存储数据,可以用data()方法获取指向这块内存的指针。

    C++17引入了非成员的data()来获取数据的指针。它可以用于array、vector容器、字符串、静态分配的C风格数组(不通过指针访问)和initializer_lists:

    vector<int> vec{1,2,3};
    int* data1 = vec.data();
    int* data2 = data(vec);
    
  • 相关阅读:
    Linux直接在通过终端打开图片文件
    【暑假】[实用数据结构]UVa11995 I Can Guess the Data Structure!
    【暑假】[实用数据结构]动态范围查询问题
    【暑假】[实用数据结构]范围最小值问题(RMQ)
    【暑假】[实用数据结构]动态连续和查询问题
    【暑假】[基本数据结构]基本的数据结构知识点总结梳理
    【暑假】[基本数据结构]根据in_order与post_order构树
    【暑假】[基本数据结构]根据BFS与DFS确定树
    【暑假】[网络流]网络流知识总结
    [HDOJ2546] 饭卡 (01背包)
  • 原文地址:https://www.cnblogs.com/ranbom/p/12845213.html
Copyright © 2020-2023  润新知