C++——STL

底层数据结构和使用场景

vector:数组
deque:数组+多个数组
list:双向链表
map、multimap、set、multiset:红黑树
unordered_map 等:哈希表

使用场景:
1)vector的使用场景:频繁查找,而不频繁插入删除的,因为频繁插入删除会造成内存的不断移动和扩容。

2)deque的使用场景:支持两端插入删除

3)list的使用场景:支持频繁的不确实位置元素的移除插入。

4)map的使用场景:频繁查找key-value形式的数据且需要按key排序的场景

5)set的使用场景:频繁查找单个value且需要按value排序的场景

4)unordered_map的使用场景:频繁查找key-value形式的数据且不需要按key排序的场景

5)unordered_set的使用场景:频繁查找单个value且不需要按value排序的场景

序列式容器

vector

STL 众多容器中,vector 是最常用的容器之一,其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。 vector 容器可以看做是一个动态数组,array容器则是静态数组、

通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:

//_Alloc 表示内存分配器
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
    ...
protected:
    pointer _Myfirst;
    pointer _Mylast;
    pointer _Myend;
};

下图演示了以上这 3 个迭代器分别指向的位置
在这里插入图片描述
通过灵活运用这 3 个迭代器,vector 容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能,比如:

template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
public:
    iterator begin() {return _Myfirst;}
    iterator end() {return _Mylast;}
    size_type size() const {return size_type(end() - begin());}
    size_type capacity() const {return size_type(_Myend - begin());}
    bool empty() const {return begin() == end();}
    reference operator[] (size_type n) {return *(begin() + n);}
    reference front() { return *begin();}
    reference back() {return *(end()-1);}
    ...
};

扩容

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  • 完全弃用现有的内存空间,重新申请更大的内存空间;
  • 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  • 最后将旧的内存空间释放。

这也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。

另外、由于扩容操作是很耗资源的,为了避免过多的扩容,一开始应该将容器设置为一个合适的大小。

扩容大小
不同的编译器申请更多内存空间的量是不同的。

相关的函数:

成员方法功能
size()返回当前 vector 容器中已经存有多少个元素
capacity()返回当前 vector 容器总共可以容纳多少个元素
resize(n)强制容器必须存储 n 个元素 ,n比size小则析构删除,大则添加扩容默认值
reserve(n)强制 容器至少存储为 n个元素。小则算了,大则扩容

插入元素

插入某一指定位置: insert vs emplace
insert 采用拷贝构造函数
emplace采用移动构造函数,少了一次复制和析构,优先使用
但是emplace一次只能插入一个元素,而insert可以插入多个

插入末尾: emplace_back() 和 push_back() 的区别
push_back采用拷贝构造函数
emplace_back采用移动构造函数,少了一次复制和析构,优先使用

删除元素

删除 vector 容器元素的几种方式对比:

函数功能
pop_back()删除 vector 容器中最后一个元素,该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
erase(pos)删除 vector 容器中 pos 迭代器指定位置处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)会减 1,但容量(capacity)不会发生改变。
erase(beg,end)删除 vector 容器中位于迭代器 [beg,end)指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)会减小,但容量(capacity)不会发生改变。
clear()删除 vector 容器中所有的元素,使其变成空的 vector 容器。该函数会改变 vector 的大小(变为 0),但不是改变其容量。
remove()删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量,所以不建议使用。

注意:借助这些成员方法删除元素后,容器的容量并不会因此而改变

如果要删除具体的某个数值,不建议用remove,因为remove不是真的删除而是覆盖,内存得不到释放,所以还是建议用erase进行删除

循环中删除元素

只删除一个元素

vector<int> num;

for(vector<int>::iterator iter=num.begin();iter!=num.end();iter++){        //从vector中删除指定的某一个元素 
    if(*iter==k){
        num.erase(iter);
        break;
    }
}

删除指定的多个重复元素

for (vector<int>::iterator iter = num.begin(); iter != num.end();) {
        if (*iter == k) {
            iter = num.erase(iter); //erase函数的返回指向当前被删除元素的下一个元素的迭代器
    	}else{
    		iter++;   
    	}
}

deque

deque 是 double-ended queue 的缩写,又称双端队列容器。
有vector为什么还要deque呢?
因为deque支持两端都能快速安插和删除元素,这些操作可以在分期摊还的常数时间(amortized constant time)内完成。所以当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器。

deque容器的存储结构
和 vector 容器采用连续的线性空间不同,deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于内存的不同区域。

为了管理这些连续空间,deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。也就是说,map 数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间,如下图所示:
在这里插入图片描述
通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”的效果。
当 deque 容器需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的开头或结尾添加指向该空间的指针,由此该空间就串接到了 deque 容器的头部或尾部。

如果 map 数组满了怎么办?
很简单,再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。

deque 容器的分段存储结构,提高了在序列两端添加或删除元素的效率,但也使该容器迭代器的底层实现变得更复杂,下面看deque容器迭代器的底层实现。

deque容器迭代器的底层实现
由于 deque 容器底层将序列中的元素分别存储到了不同段的连续空间中,因此要想实现迭代器的功能,必须先解决如下 2 个问题:

  1. 迭代器在遍历 deque 容器时,必须能够确认各个连续空间在 map 数组中的位置;
  2. 迭代器在遍历某个具体的连续空间时,必须能够判断自己是否已经处于空间的边缘位置。如果是,则一旦前进或者后退,就需要跳跃到上一个或者下一个连续空间中。

为了实现遍历 deque 容器的功能,deque 迭代器定义了如下的结构:

template<class T,...>
struct __deque_iterator{
    ...
    T* cur;
    T* first;
    T* last;
    map_pointer node;//map_pointer 等价于 T**
}

可以看到,迭代器内部包含 4 个指针,它们各自的作用为:

  • cur:指向当前正在遍历的元素;
  • first:指向当前连续空间的首地址
  • last:指向当前连续空间的末尾地址
  • node:它是一个二级指针,用于指向 map 数组中存储的指向当前连续空间的指针。

借助这 4 个指针,deque 迭代器对随机访问迭代器支持的各种运算符进行了重载,能够对 deque 分段连续空间中存储的元素进行遍历。例如:

//当迭代器处于当前连续空间边缘的位置时,如果继续遍历,就需要跳跃到其它的连续空间中,该函数可用来实现此功能
void set_node(map_pointer new_node){
    node = new_node;//记录新的连续空间在 map 数组中的位置
    first = *new_node; //更新 first 指针
    //更新 last 指针,difference_type(buffer_size())表示每段连续空间的长度
    last = first + difference_type(buffer_size());
}
//重载 * 运算符
reference operator*() const{return *cur;}
pointer operator->() const{return &(operator *());}
//重载前置 ++ 运算符
self & operator++(){
    ++cur;
    //处理 cur 处于连续空间边缘的特殊情况
    if(cur == last){
        //调用该函数,将迭代器跳跃到下一个连续空间中
        set_node(node+1);
        //对 cur 重新赋值
        cur = first;
    }
    return *this;
}
//重置前置 -- 运算符
self& operator--(){
    //如果 cur 位于连续空间边缘,则先将迭代器跳跃到前一个连续空间中
    if(cur == first){
        set_node(node-1);
        cur == last;
    }
    --cur;
    return *this;
}

deque容器的底层实现
了解了 deque 容器底层存储序列的结构,以及 deque 容器迭代器的内部结构之后,接下来看看 deque 容器究竟是如何实现的。

deque 容器除了维护先前讲过的 map 数组,还需要维护 start、finish 这 2 个 deque 迭代器。以下为 deque 容器的定义:

//_Alloc为内存分配器
template<class _Ty, class _Alloc = allocator<_Ty>>
class deque{
    ...
protected:
    iterator start;
    iterator finish;
    map_pointer map;
...
}

其中,start 迭代器记录着 map 数组中首个连续空间的信息,finish 迭代器记录着 map 数组中最后一个连续空间的信息。
另外需要注意的是,和普通 deque 迭代器不同,start 迭代器中的 cur 指针指向的是连续空间中首个元素;而 finish 迭代器中的 cur 指针指向的是连续空间最后一个元素的下一个位置。

因此,deque 容器的底层实现如图 2 所示。
在这里插入图片描述
借助 start 和 finish,以及 deque 迭代器中重载的诸多运算符,就可以实现 deque 容器提供的大部分成员函数,比如:

//begin() 成员函数
iterator begin() {return start;}
//end() 成员函数
iterator end() { return finish;}
//front() 成员函数
reference front(){return *start;}
//back() 成员函数
reference back(){
    iterator tmp = finish;
    --tmp;
    return *tmp;
}
//size() 成员函数
size_type size() const{return finish - start;}//deque迭代器重载了 - 运算符
//enpty() 成员函数
bool empty() const{return finish == start;}

list

list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。

优势:可以在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))
劣势:不支持随机访问,需要从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置(时间复杂度为O(n))

list容器的底层实现
list 容器实际上就是一个带有头节点的双向循环链表
在此基础上,通过借助 node 头节点,就可以实现 list 容器中的所有成员函数,比如:

//begin()成员函数
__list_iterator<T> begin(){return (*node).next;}
//end()成员函数
__list_iterator<T> end(){return node;}
//empty()成员函数
bool empty() const{return (*node).next == node;}
//front()成员函数
T& front() {return *begin();}
//back()成员函数
T& back() {return *(--end();)}
//...

list容器迭代器的底层实现
和 array、vector 这些容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器的指针,并且该指针还可以借助重载的 *、++、–、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。

因此,list 容器迭代器的实现代码如下:
可以看到,迭代器的移动就是通过操作节点的指针实现的。

template<tyepname T,...>
struct __list_iterator{
    __list_node<T>* node;
    //...
    //重载 == 运算符
    bool operator==(const __list_iterator& x){return node == x.node;}
    //重载 != 运算符
    bool operator!=(const __list_iterator& x){return node != x.node;}
    //重载 * 运算符,返回引用类型
    T* operator *() const {return *(node).myval;}
    //重载前置 ++ 运算符
    __list_iterator<T>& operator ++(){
        node = (*node).next;
        return *this;
    }
    //重载后置 ++ 运算符
    __list_iterator<T>& operator ++(int){
        __list_iterator<T> tmp = *this;
        ++(*this);
        return tmp;
    }
    //重载前置 -- 运算符
    __list_iterator<T>& operator--(){
        node = (*node).prev;
        return *this;
    }
    //重载后置 -- 运算符
    __list_iterator<T> operator--(int){
        __list_iterator<T> tmp = *this;
        --(*this);
        return tmp;
    }
    //...
}

forward_list

forward_list 是 C++ 11 新添加的一类容器, forward_list 使用单链表,list 使用双向链表

forward_list提高的功能没有list 容器强大,但是优点在于单链表耗用的内存空间更少

关联式容器

map

map 容器存储的都是 pair 对象,也就是用 pair 类模板创建的键值对,形如pair<const K, T>。可以看出键值既不能重复也不能被修改。

map 容器的模板定义如下:

template < class Key,                                     // 指定键(key)的类型
           class T,                                       // 指定值(value)的类型
           class Compare = less<Key>,                     // 指定排序规则
           class Alloc = allocator<pair<const Key,T> >    // 指定分配器对象的类型
           > class map;

排序规则:
map容器会自动根据各键值对的键的大小,按照既定的规则进行排序。默认升序排序。也可以自定义排序规则。

operator[]和insert()效率对比

  • 添加——insert() 效率更高
    因为用 operator[ ] 添加新键值对元素的流程是:先构造一个有默认值的键值对,然后再为其 value 赋值,而insert是直接构造一个要添加的键值对元素。
  • 更新 ——operator[ ] 效率更高
    insert() 方法在进行更新操作之前,需要有一个 pair 类型元素做参数。这意味着,该方法要多构造一个 pair 对象,并且事后还要析构此 pair 对象。
    而operator[ ] 就不需要使用 pair 对象,自然不需要构造并析构任何 pair 对象

map容器3种插入键值对的方法效率对比:
结论:emplace() 和 emplace_hint() 的执行效率比 insert() 高
原因:使用 insert() 向 map 容器中插入键值对的过程是,先创建该键值对,然后再将该键值对复制或者移动到 map 容器中的指定位置;而使用 emplace() 或 emplace_hint() 插入键值对的过程是,直接在 map 容器中的指定位置构造该键值对。
证明:

#include <iostream>
#include <map>  //map
#include <string> //string
using namespace std;
class testDemo
{
public:
    testDemo(int num) :num(num) {
        std::cout << "调用构造函数" << endl;
    }
    testDemo(const testDemo& other) :num(other.num) {
        std::cout << "调用拷贝构造函数" << endl;
    }
    testDemo(testDemo&& other) :num(other.num) {
        std::cout << "调用移动构造函数" << endl;
    }
private:
    int num;
};
int main()
{
    //创建空 map 容器
    std::map<std::string, testDemo>mymap;
    cout << "insert():" << endl;
    mymap.insert({ "http://c.biancheng.net/stl/", testDemo(1) });
   
    cout << "emplace():" << endl;
    mymap.emplace( "http://c.biancheng.net/stl/:", 1);
    cout << "emplace_hint():" << endl;
    mymap.emplace_hint(mymap.begin(), "http://c.biancheng.net/stl/", 1);
    return 0;
}

程序输出结果为:


insert():
调用构造函数
调用移动构造函数
调用移动构造函数
emplace():
调用构造函数
emplace_hint():
调用构造函数

可以看到,执行insert()调用了一次构造函数和2次移动构造函数。
下面分析具体调用原理:mymap.insert();这行代码底层的执行过程,可以分解为以下 3 步:

//构造类对象
testDemo val = testDemo(1); //调用 1 次构造函数
//构造键值对
auto pai = make_pair("http://c.biancheng.net/stl/", val); //调用 1 次移动构造函数
//完成插入操作
mymap.insert(pai); //调用 1 次移动构造函数

而完成同样的插入操作,emplace() 和 emplace_hint() 方法都只调用了 1 次构造函数,这足以证明,这 2 个方法是在 map 容器内部直接构造的键值对。
因此,在实现向 map 容器中插入键值对时,应优先考虑使用 emplace() 或者 emplace_hint()。

set

set 容器具有以下几个特性:

  • 存储的元素有序且唯一
  • 不再以键值对的方式存储数据,因为 set 容器专门用于存储键和值相等的键值对,因此该容器中真正存储的是各个键值对的值(value);
  • set 容器在存储数据时,会根据各元素值的大小对存储的元素进行排序(默认做升序排序);
  • 存储到 set 容器中的元素,虽然其类型没有明确用 const 修饰,但正常情况下不要修改;实在要修改应该先删除,后添加的方式进行修改
  • set容器迭代器
    和 map 容器不同,C++ STL 中的 set 容器类模板中未对 [] 运算符进行重载。因此,要想访问 set 容器中存储的元素,只能借助 set 容器的迭代器。

set 容器的类模板定义如下:

template < class T,                        // 键 key 和值 value 的类型
           class Compare = less<T>,        // 指定 set 容器内部的排序规则
           class Alloc = allocator<T>      // 指定分配器对象的类型
           > class set;

关联式容器的默认排序规则

关联式容器是有序的,关联式容器提供了四个可直接使用的排序规则,如下:

std::less<T>   	底层采用 < 运算符实现升序排序,各关联式容器默认采用的排序规则。
std::greater<T>	底层采用 > 运算符实现降序排序,同样适用于各个关联式容器。
std::less_equal<T>	底层采用 <= 运算符实现升序排序,多用于 multimap 和 multiset 容器。
std::greater_equal<T>	底层采用 >= 运算符实现降序排序,多用于 multimap 和 multiset 容器。

这些排序规则底层是采用函数对象的方式实现的。以 std::less 为例,其底层实现为:

template <typename T>
struct less {
    //定义新的排序规则
    bool operator()(const T &_lhs, const T &_rhs) const {
        return _lhs < _rhs;
    }
}

关联式容器的自定义排序规则

1、使用函数对象自定义排序规则
无论关联式容器中存储的什么类型,都可以使用函数对象的方式为该容器自定义排序规则。

//定义函数对象类,也可以使用 struct 关键字创建,也可以将其定义为模板类template <typename T>
class cmp { 
public:
    //重载 () 运算符
    bool operator ()(const string &a,const string &b) {
        //按照字符串的长度,做升序排序(即存储的字符串从短到长)
        return  (a.length() < b.length());
    }
};
int main() {
    //创建 set 容器,并使用自定义的 cmp 排序规则
    std::set<string, cmp>myset{"http://c.biancheng.net/stl/",
                               "http://c.biancheng.net/python/",
                               "http://c.biancheng.net/java/"};
    //输出容器中存储的元素
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
            cout << *iter << endl;
    }
    return 0;
}

2、重载关系运算符实现自定义排序
当关联式容器中存储的数据类型为自定义的结构体变量或者类对象时,通过对现有排序规则中所用的关系运算符进行重载,也能实现自定义排序规则的目的。
举例:

//自定义类
class myString {
public:
    //定义构造函数,向 myset 容器中添加元素时会用到
    myString(string tempStr) :str(tempStr) {};
    //获取 str 私有对象,由于会被私有对象调用,因此该成员方法也必须为 const 类型
    string getStr() const;
private:
    string str;
};
string myString::getStr() const{
    return this->str;
}
//重载 < 运算符,参数必须都为 const 类型
bool operator <(const myString &stra, const myString & strb) {
    //以字符串的长度为标准比较大小
    return stra.getStr().length() < strb.getStr().length();
}
int main() {
    //创建空 set 容器,仍使用默认的 less<T> 排序规则
    std::set<myString>myset;
    //向 set 容器添加元素,这里会调用 myString 类的构造函数
    myset.emplace("http://c.biancheng.net/stl/");
    myset.emplace("http://c.biancheng.net/c/");
    myset.emplace("http://c.biancheng.net/python/");
    //
    for (auto iter = myset.begin(); iter != myset.end(); ++iter) {
        myString mystr = *iter;
        cout << mystr.getStr() << endl;
    }
    return 0;
}

在这个程序中,虽然 myset 容器表面仍采用默认的 std::less 排序规则,但由于我们对其所用的 < 运算符进行了重载,使得 myset 容器内部实则是以字符串的长度为基准,对各个 mystring 类对象进行排序。

另外,上面程序以全局函数的形式实现对 < 运算符的重载,还可以使用成员函数或者友元函数的形式实现。

当以成员函数的方式重载 < 运算符时,该成员函数必须声明为 const 类型,且参数也必须为 const 类型:

bool operator <(const myString & tempStr) const {
    //以字符串的长度为标准比较大小
    return this->str.length() < tempStr.str.length();
}

如果以友元函数的方式重载 < 运算符时,要求参数必须使用 const 修饰:

//类中友元函数的定义
friend bool operator <(const myString &a, const myString &b);
//类外部友元函数的具体实现
bool operator <(const myString &stra, const myString &strb) {
    //以字符串的长度为标准比较大小
    return stra.str.length() < strb.str.length();
}

至于参数的传值方式是采用按引用传递还是按值传递,都可以(建议采用按引用传递,效率更高)。

注意:当关联式容器中存储的元素类型为结构体指针变量或者类的指针对象时,只能使用函数对象的方式自定义排序规则

无序关联式容器

无序关联式容器,又称哈希容器。和关联式容器一样,此类容器存储的也是键值对元素;

无序关联式容器擅长通过指定键查找对应的值,而遍历容器中存储元素的效率不如关联式容器。

无序容器功能
unordered_map存储键值对 <key, value> 类型的元素,其中各个键值对键的值不允许重复,且该容器中存储的键值对是无序的。
unordered_multimap和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对。
unordered_set存储的全部都是键和值相等的键值对,正因为它们相等,因此只存储 value 即可。另外,存储的元素唯一且无序。
unordered_multiset和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素。

无序容器自定义哈希函数

哈希函数虽然名字叫函数,但其实是一个函数对象类。因此,如果我们想自定义个哈希函数,实际需要自定义一个函数对象类。

举个例子:

class Person {
public:
    Person(string name, int age) :name(name), age(age) {};
    string getName() const;
    int getAge() const;
private:
    string name;
    int age;
};
string Person::getName() const {
    return this->name;
}
int Person::getAge() const {
    return this->age;
}

//自定义一个哈希函数
class hash_fun {
public:
  //注意,重载 ( ) 运算符时,其参数必须为 const 类型,且该方法也必须用 const 修饰。
    int operator()(const Person &A) const { 
        return A.getAge();
    }
};

int main(){
   std::unordered_set<Person, hash_fun> myset;
}

但是,此时创建的 myset 容器还无法使用,因为该容器使用的是默认的 std::equal_to<key> 比较规则,但此规则并不适用于该容器。

无序容器自定义比较规则

和哈希函数一样,无论创建哪种无序容器,都需要为其指定一种可比较容器中各个元素是否相等的规则。

值得一提的是,默认情况下无序容器使用的 std::equal_to<key> 比较规则,其本质也是一个函数对象类,底层实现如下:

template<class T>
class equal_to
{
public:   
    bool operator()(const T& _Left, const T& _Right) const{
        return (_Left == _Right);
    }   
};

可以看到,该规则在底层实现过程中,直接用 = = 运算符比较容器中任意 2 个元素是否相等,这意味着,如果容器中存储的元素类型,支持直接用 == 运算符比较是否相等,则该容器可以使用默认的 std::equal_to<key> 比较规则;反之,就不可以使用。

显然,对于我们上面创建的 myset 容器,其内部存储的是 Person 类对象,不支持直接使用 == 运算符做比较。这种情况下,有以下 2 种方式可以解决此问题

1、在 Person 类中重载 == 运算符,这会使得 std::equal_to<key> 比较规则中使用的 == 运算符变得合法,myset 容器就可以继续使用 std::equal_to<key> 比较规则;
例如:

bool operator==(const Person &A, const Person &B) {
    return (A.getAge() == B.getAge());
}

//重载 == 运算符之后,就能以如下方式创建 myset 容器:
std::unordered_set<Person, hash_fun> myset{ {"zhangsan", 40},{"zhangsan", 40},{"lisi", 40},{"lisi", 30} };

2、以函数对象类的方式,自定义一个适用于 myset 容器的比较规则。

class mycmp {
public:
    bool operator()(const Person &A, const Person &B) const {
        return (A.getName() == B.getName()) && (A.getAge() == B.getAge());
    }
};
//在 mycmp 规则的基础上,我们可以像如下这样创建 myset 容器:
std::unordered_set<Person, hash_fun, mycmp> myset{ {"zhangsan", 40},{"zhangsan", 40},{"lisi", 40},{"lisi", 30} };

总结:

当无序容器中存储的是用结构体或类自定义类型的数据时,自定义比较规则,可以使用默认的 std::equal_to<key> 规则,但前提是必须重载 == 运算符,其他的一律只能以函数对象类的方式实现

完整代码:

#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
class Person {
public:
    Person(string name, int age) :name(name), age(age) {};
    string getName() const;
    int getAge() const;
private:
    string name;
    int age;
};
string Person::getName() const {
    return this->name;
}
int Person::getAge() const {
    return this->age;
}
//自定义哈希函数
class hash_fun {
public:
    int operator()(const Person &A) const {
        return A.getAge();
    }
};

//重载 == 运算符,myset 可以继续使用默认的 equal_to<key> 规则
bool operator==(const Person &A, const Person &B) {

    return (A.getAge() == B.getAge());
}
//完全自定义比较规则,弃用 equal_to<key>
class mycmp {
public:
    bool operator()(const Person &A, const Person &B) const {
        return (A.getName() == B.getName()) && (A.getAge() == B.getAge());
    }
};
int main()
{
    //使用自定义的 hash_fun 哈希函数,比较规则仍选择默认的 equal_to<key>,前提是必须重载 == 运算符
    std::unordered_set<Person, hash_fun> myset1{ {"zhangsan", 40},{"zhangsan", 40},{"lisi", 40},{"lisi", 30} };
    //使用自定义的 hash_fun 哈希函数,以及自定义的 mycmp 比较规则
    std::unordered_set<Person, hash_fun, mycmp> myset2{ {"zhangsan", 40},{"zhangsan", 40},{"lisi", 40},{"lisi", 30} };
   
    cout << "myset1:" << endl;
    for (auto iter = myset1.begin(); iter != myset1.end(); ++iter) {
        cout << iter->getName() << " " << iter->getAge() << endl;
    }

    cout << "myset2:" << endl;
    for (auto iter = myset2.begin(); iter != myset2.end(); ++iter) {
        cout << iter->getName() << " " << iter->getAge() << endl;
    }
    return 0;
}

容器适配器 stack、queue、priority_queue

容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。

3 种容器适配器,分别是 stack、queue、priority_queue:

  • stack< T>:是一个封装了 deque 容器的适配器类模板,默认实现的是一个后入先出(Last-In-First-Out,LIFO)的压入栈。stack 模板定义在头文件 stack 中。
  • queue< T>:是一个封装了 deque 容器的适配器类模板,默认实现的是一个先入先出(First-In-First-Out,LIFO)的队列。可以为它指定一个符合确定条件的基础容器。queue 模板定义在头文件 queue 中。
  • priority_queue< T>:是一个封装了 vector 容器的适配器类模板,默认实现的是一个会对元素排序,从而保证最大元素总在队列最前面的队列。priority_queue 模板定义在头文件 queue 中。

priority_queue实现自定义排序

两种方式实现自定义排序

1、无论 priority_queue 中存储的是基础数据类型(int、double 等),还是 string 类对象或者自定义的类对象,都可以使用函数对象的方式自定义排序规则。例如:

#include<iostream>
#include<queue>
using namespace std;
//函数对象类
template <typename T>
struct cmp{
    //重载 () 运算符
    bool operator()(T a, T b)
    {
        return a > b;
    }
};
int main()
{
    int a[] = { 4,2,3,5,6 };
    priority_queue<int,vector<int>,cmp<int> > pq(a,a+5);
    while (!pq.empty())
    {
        cout << pq.top() << " ";
        pq.pop();
    }
    return 0;
}

2、当 priority_queue 容器适配器中存储的数据类型为结构体或者类对象(包括 string 类对象)时,还可以通过重载其 > 或者 < 运算符,间接实现自定义排序规则的目的。

举个例子:

class node {
public:
    node(int x = 0, int y = 0) :x(x), y(y) {}
    int x, y;
};
//新的排序规则为:先按照 x 值排序,如果 x 相等,则按 y 的值排序
bool operator < (const node &a, const node &b) {
    if (a.x > b.x) return 1;
    else if (a.x == b.x)
        if (a.y >= b.y) return 1;
    return 0;
}

int main()
{    
    priority_queue<node> pq(a,a+5);
    return 0;
}

当然,也可以以友元函数或者成员函数的方式重载 > 或者 < 运算符。

需要注意的是,以成员函数的方式重载 > 或者 < 运算符时,该成员函数必须声明为 const 类型,且参数也必须为 const 类型,至于参数的传值方式是采用按引用传递还是按值传递,都可以(建议采用按引用传递,效率更高)。

sort

sort (first, last)
对容器或普通数组中 [first, last) 范围内的元素进行排序,默认进行升序排序。

sort() 函数受到底层实现方式的限制,只有普通数组和具备以下条件的容器,才能使用 sort() 函数:

  • 容器支持的迭代器类型必须为随机访问迭代器。这意味着,sort() 只对 array、vector、deque 这 3 个容器提供支持。
  • 如果对容器中指定区域的元素做默认升序排序,则元素类型必须支持<小于运算符,当然如果是自定义排序函数则不需要
  • sort() 函数在实现排序时,需要交换容器中元素的存储位置。如果容器中存储的是自定义的类对象,则建议在该类的内部提供移动构造函数和移动赋值运算符,以提高交换效率。

优先使用函数对象作为自定义排序

sort() 排序函数默认从小到大进行排序,也可以自定义排序规则,其定义的方式有 2 种
1、普通函数
2、函数对象

//1、以普通函数的方式实现自定义排序规则
inline bool mycomp(int i, int j) {
    return (i < j);
}
//2、以函数对象的方式实现自定义排序规则
class mycomp2 {
public:
    bool operator() (int i, int j) {
        return (i < j);
    }
};

int main() {
    std::vector<int> myvector
    //调用普通函数定义的排序规则
    std::sort(myvector.begin(), myvector.end(), mycomp);
    //调用函数对象定义的排序规则
    std::sort(myvector.begin(), myvector.end(), mycomp2());
    return 0;
}

以上两种排序规则,以函数对象的方式实现自定义排序规则比普通函数效率高

根据以往的认知,函数对象的执行效率应该不如普通函数。但事实恰恰相反,即便如上面程序那样,将普通函数定义为更高效的内联函数,其执行效率也无法和函数对象相比。

那么,是什么原因导致了它们执行效率上的差异呢?
以 mycomp2() 函数对象为例,其 mycomp2::operator() 也是一个内联函数,编译器在对 sort() 函数进行实例化时会将该函数直接展开,这也就意味着,展开后的 sort() 函数内部不包含任何函数调用,所以效率相当高。

而如果使用 mycomp 作为参数来调用 sort() 函数,情形则大不相同。
要知道,C++ 并不能真正地将一个函数作为参数传递给另一个函数,所以当我们试图将一个函数作为参数进行传递时,编译器会隐式地将它转换成一个指向该函数的指针,并将该指针传递过去。

当 sort() 函数被实例化时,编译器生成的函数声明如下所示:

std::sort(vector<int>::iterator first,
          vector<int>::iterator last,
          bool (*comp)(int, int));

可以看到,参数 comp 只是一个指向函数的指针,所以 sort() 函数内部每次调用 comp 时,编译器都会通过指针产生一个间接的函数调用,所以会有函数调用导致的开销。

容器操作可能使迭代器失效

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题

在向vector或string容器添加元素后:
如果存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。
如果未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。

1、正确的删除与修改
在这里插入图片描述
此程序删除vector中的偶数值元素,并复制每个奇数值元素。我们在调用insert和erase后都更新迭代器,因为两者都会使迭代器失效。
在调用erase后,不必递增迭代器,因为erase返回的迭代器已经指向序列中下一个元素。调用insert后,需要递增迭代器两次。记住,insert在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert后,iter指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。

2、不要保存end返回的迭代器
当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,原来end返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器,一直当作容器末尾使用。通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。

int main(){
    vector<int>v;
    auto begin=v.begin(); 
    auto end=v.end(); //在循环之前保存end()返回的迭代器,一直用作容器末尾,就会导致错误,因为end在迭代过程中的指向会发生改变
    while(begin!=end){
        //做一些增删操作
    }
}

必须在每次插入操作后重新调用end()

int main(){
    vector<int>v;
    auto begin=v.begin();
    auto end=v.end();
    while(begin!=v.end()){ //v.end()
        //做一些增删操作
    }
}

API

vector

begin()	返回指向容器中第一个元素的迭代器。
end()	返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。

rbegin()	返回指向最后一个元素的迭代器。
rend()	返回指向第一个元素所在位置前一个位置的迭代器。
cbegin()begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend()end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend()rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。

size()	返回实际元素个数。
capacity()	返回当前容量。

empty()	判断容器中是否有元素,若无元素,则返回 true;反之,返回 falseswap()	交换两个容器的所有元素。

front()	返回第一个元素的引用。
back()	返回最后一个元素的引用。
emplace_back()	在序列尾部生成一个元素————移动构造
push_back()	在序列的尾部添加一个元素————拷贝构造
pop_back()	移出序列尾部的元素。

emplace()	在指定的迭代器位置直接生成一个元素,注意只能生成一个。
insert(迭代器初始位置+i,插入的个数,插入的元素)//比如:ans.insert(ans.begin()+1,2,1)在ans的下标1前面插入2个1


erase()	移出一个元素或一段元素。
e.g:v.erase(v.begin()+2);//删除第三个元素
e.g:v.erase(v.begin(),v.bengin()+3);//删除前3个元素,[ .. ),第四个元素没有删除

clear()	移出所有的元素,容器大小变为 0

map

获取元素的三种方式:
1operator[key]	map容器重载了 [] 运算符,只要知道 map 容器中某个键值对的键的值,就可以通过键直接获取对应的值,如果map不存在这个key,则转为添加,val为值初始化。
2at(key)[]一样,区别是没有key时不是转为添加而是抛出out_of_range异常
3find(key) 找到了返回迭代器,没找到返回end()位置迭代器


begin()	返回指向容器中第一个(注意,是已排好序的第一个)键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
end()	返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。

empty() 	若容器为空,则返回 true;否则 falsesize()	返回当前 map 容器中存有键值对的个数。

erase()	删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。后续章节还会对该方法做重点讲解。
swap()	交换 2 个 map 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。
clear()	清空 map 容器中所有的键值对,即使 map 容器的 size()0lower_bound(key)	返回一个指向当前 map 容器中第一个 >= key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
upper_bound(key)	返回一个指向当前 map 容器中第一个  > key的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。

count(key)	在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1

set

begin()	返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
end()	返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。

empty()	若容器为空,则返回 true;否则 falsesize()	返回当前 set 容器中存有元素的个数。

emplace()	在当前 set 容器中的指定位置直接构造新元素。其效果和 insert() 一样,但效率更高。
erase()	删除 set 容器中存储的元素。
swap()	交换 2 个 set 容器中存储的所有元素。这意味着,操作的 2 个 set 容器的类型必须相同。
clear()	清空 set 容器中所有的元素,即令 set 容器的 size()0count(val)	在当前 set 容器中,查找值为 val 的元素的个数,并返回。注意,由于 set 容器中各元素的值是唯一的,因此该函数的返回值最大为 1

unordered_map

operator[key]
该模板类中重载了 [] 运算符,其功能是可以向访问数组中元素那样,只要给定某个键值对的键 key,就可以获取该键对应的值。
注意,如果当前容器中没有以 key 为键的键值对,则其会使用该键向当前容器中插入一个新键值对。

begin()	返回指向容器中第一个键值对的正向迭代器。
end() 	返回指向容器中最后一个键值对之后位置的正向迭代器。

empty()	若容器为空,则返回 true;否则 falsesize()	返回当前容器中存有键值对的个数。

count(key)	在容器中查找以 key 键的键值对的个数。
emplace()	向容器中添加新键值对,效率比 insert() 方法高。
erase()	删除指定键值对。
clear() 	清空容器,即删除容器中存储的所有键值对。
swap()	交换 2 个 unordered_map 容器存储的键值对,前提是必须保证这 2 个容器的类型完全相等。

unordered_set:

begin()	返回指向容器中第一个元素的正向迭代器。
end();	返回指向容器中最后一个元素之后位置的正向迭代器。
empty()	若容器为空,则返回 true;否则 falsesize()	返回当前容器中存有元素的个数。
count(key)	在容器中查找值为 key 的元素的个数。
emplace()	向容器中添加新元素,效率比 insert() 方法高。
erase()	删除指定元素。
clear()	清空容器,即删除容器中存储的所有元素。
swap()	交换 2 个 unordered_map 容器存储的元素,前提是必须保证这 2 个容器的类型完全相等。

stack:

empty()	当 stack 栈中没有元素时,该成员函数返回 true;反之,返回 falsesize()	返回 stack 栈中存储元素的个数。
top()	返回一个栈顶元素的引用,类型为 T&。如果栈为空,程序会报错。
push(const T& val)	先复制 val,再将 val 副本压入栈顶。这是通过调用底层容器的 push_back() 函数完成的。
push(T&& obj)	以移动元素的方式将其压入栈顶。这是通过调用底层容器的有右值引用参数的 push_back() 函数完成的。
pop()	弹出栈顶元素。

queue:

empty()	如果 queue 中没有元素的话,返回 truesize()	返回 queue 中元素的个数。
front()	返回 queue 中第一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
back()	返回 queue 中最后一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
push(const T& obj)	在 queue 的尾部添加一个元素的副本。这是通过调用底层容器的成员函数 push_back() 来完成的。
pop()	删除 queue 中的第一个元素。

priority_queue:

empty()	如果 priority_queue 为空的话,返回 true;反之,返回 falsesize()	返回 priority_queue 中存储元素的个数。
top()	返回 priority_queue 中第一个元素的引用形式。
push(const T& obj)	根据既定的排序规则,将元素 obj 的副本存储到 priority_queue 中适当的位置。
pop()	移除 priority_queue 容器适配器中第一个元素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值