【语言】C++ STL库介绍

247 阅读9分钟

容器分为序列容器和关联容器。
序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
关联容器,元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。

1. 序列容器

序列容器主要包含以下几种:
1. array<T,N>
2. vector
3. deque
4. list
5. forward_list

1.1 array<T,N>(数组容器)

该容器可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值。

#include<iostream>
#include<array>
using namespace std;

int main() {
   array<int, 10> myArr = {1,2,3,4,5};//固定大小为10
   cout << "元素序列:";
   for (auto i = 0; i < 10; ++i) {
   	cout << myArr[i] << " ";
   }
   cout << endl;
   cout << "myArr.at(3) = " << myArr.at(3) << endl;//任意访问
   cout << "myArr[3] = " << myArr[3] << endl;//任意访问
	cout << "myArr.front() = " << myArr.front() << endl;//获取第一个元素
   cout << "myArr.back() =  " << myArr.back() << endl;//获取最后一个元素
   cout << "myArr.data() = " << myArr.data() << endl;//获取第一个元素的指针
   cout << "*myArr.data() = " << *myArr.data() << endl;//获取第一个元素的指针指向的元素
   return 0;
}

1.2 vector(向量容器)

用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差,因为它需要移动元素(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数)。

int main(int argc, const char * argv[]) {
    
    //定义一个vector容器
    vector<int> v1;
    
    //插入元素(尾部插入)
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);
    
    //迭代器遍历打印
    for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    
    //修改头部元素的值(front()返回是引用,可以当左值)
    v1.front() = 44;
    
    //输出头部元素
    cout<< "头部元素:" << v1.front() << endl;
    
    //修改尾部的值(back()返回是引用,可以当左值)
    v1.back() = 99;
    
    //输出尾部元素
    cout << "尾部元素" << v1.back() <<endl;
    
    //删除元素(从尾部删除)
    v1.pop_back();
    
    //迭代器遍历打印
    for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    
    v1.push_back(2);
    v1.push_back(2);
    vector<int>::iterator it = v1.begin();
    while (it != v1.end()) {
        if (*it == 2) {
            it = v1.erase(it);   //删除后,迭代器指针会执行下一个位置并返回。
        }else{
            it++;
        }
    }
    
    return 0;
}

1.3 deque< T >(双端队列容器)

和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;

int main(int argc, const char * argv[]) {
    
    //定义deque对象
    deque<int> d1;
    
    //尾部插入元素
    d1.push_back(10);
    d1.push_back(20);
    d1.push_back(30);
    
    //头部插入元素
    d1.push_front(1);
    d1.push_front(2);
    d1.push_front(3);
    
    //尾部删除元素
    d1.pop_back();
    
    //头部删除元素
    d1.pop_front();
    
    //修改头部和尾部的值
    d1.front() = 111;
    d1.back()  = 222;
    
    //查找元素为1的下标
    //通过distance求取下标
    deque<int>::iterator it = d1.begin();
    while (it != d1.end()) {
        if (*it == 1) {
            cout << "下标:" << distance(d1.begin(), it) << endl;
        }
        it++;
    }
    
    //遍历
    for (deque<int>::iterator it = d1.begin(); it != d1.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    
    return 0;
}

1.4 list< T >(链表容器)

是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。

int main(int argc, const char * argv[]) {
    
    //创建list对象
    list<int> l;
    
    //尾部添加元素
    for (int i = 0; i < 10; i++) {
        l.push_back(i);
    }
    
    //头部添加元素
    l.push_front(111);
    
    //遍历
    for (list<int>::iterator it = l.begin(); it != l.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
    
    //list不能随机访问
    list<int>::iterator it = l.begin();
    it++;
    it++;
    cout << *it <<endl;
//    it = it + 1;  //编译报错,不能随机访问
//    it = it + 5;  //编译报错,不能随机访问

    //删除头部元素
    l.erase(l.begin());
    
     //删除某个区间
    it = l.begin();
    it++;
    it++;
    it++;
    l.erase(l.begin(), it);
    
    //移除值为100的所有元素
    l.remove(100);
    
    return 0;
}

1.5 forward_list< T >(正向链表容器)

和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

// forward_list constructors
#include <iostream>
#include <forward_list>

int main ()
{
    // constructors used in the same order as described above:
    std::forward_list<int> first;                      // default: empty
    std::forward_list<int> second (3,77);              // fill: 3 seventy-sevens
    std::forward_list<int> third (second.begin(), second.end()); // range initialization
    std::forward_list<int> fourth (third);            // copy constructor
    std::forward_list<int> sixth = {3, 52, 25, 90};    // initializer_list constructor
    
    std::forward_list<int> mylist = { 34, 77, 16, 2 };
    std::cout << "mylist contains:";
    for ( auto it = mylist.begin(); it != mylist.end(); ++it )
    std::cout << ' ' << *it;

    std::forward_list<int> mylist = {10, 20, 30, 40, 50};
                                                // 10 20 30 40 50
    auto it = mylist.begin();                   // ^
    
    it = mylist.erase_after(it);              // 10 30 40 50
                                              //     ^
    it = mylist.erase_after(it,mylist.end()); // 10 30
                                            //         ^
    
    std::cout << "mylist contains:";
    for (int& x: mylist) std::cout << ' ' << x;
    std::cout << '\n';

    return 0;
}

2. 关联容器

关联容器主要包含以下几种:
set:排好序的集合,不允许有相同元素。
multiset:排好序的集合,允许有相同元素。
map:每个元素都分为关键字和值两部分,容器中的元素是按关键字排序的。不允许有多个元素的关键字相同。
multimap:和 map 类似,差别在于元素的关键字可以相同。

使用关联容器的目的也就在于快速查找。当一个元素被插入关联容器时,该元素会和已有的元素进行比较,最终被插入一个合适的位置。
关联容器底层实现为“红黑树”(RB-Tree),红黑树是平衡二叉树的一种,其有以下特点:
(1)所有左子树结点的值小于等于根节点的值,右子树节点的值大于根节点的值。
(2)没有一个节点深度过大。
通过上面,就可以知道,关联容器是平衡二叉树的具体应用,因为其内部是通过链表的方式组织,所以在插入的时候比vector要快,比list要慢;由于其底层是平衡二叉树,查找、插入、删除时间复杂度都应该为O(logN)。 在关联容器中查找元素和插入元素的时间复杂度都是 O(log(n))。从 begin() 到 end() 遍历整个关联容器,就是从小到大遍历整个容器。

关联容器内部的元素或关键字之间比较大小可以用<运算符,也可以用自定义的比较器。因为有序,所以在关联容器上进行查找的速度较快。

不能修改 set 或 multiset 容器中元素的值。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 set 或 multiset 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素。同理,也不能修改 map 和 multimap 容器中元素的关键字

2.1 map/multimap

map容器提供一个键值对(key/value)容器,map与multimap差别仅仅在于multimap允许一个键对应多个值。需要包含头文件#include。对于迭代器来说,可以修改实值,而不能修改key。map会根据key自动排序。
multimap 有的成员函数,map 都有。此外,map 还有成员函数 operator[]。

#include <iostream>
#include <map>  //使用map需要包含此头文件
using namespace std;
template <class T1,class T2>
ostream & operator <<(ostream & o,const pair<T1,T2> & p)
{ //将pair对象输出为 (first,second)形式
    o << "(" << p.first  << "," << p.second << ")";
    return o;
}
template<class T>
void Print(T first,T last)
{//打印区间[first,last)
    for( ; first != last; ++ first)
        cout <<  * first << " ";
    cout << endl;
}
typedef map<int,double,greater<int> > MYMAP; //此容器关键字是整型,元素按关键字从大到小排序
int main()
{
    MYMAP mp;
    mp.insert(MYMAP::value_type(15,2.7));
    pair<MYMAP::iterator,bool> p = mp.insert(make_pair(15,99.3));
    if(!p.second)
        cout << * (p.first) << " already exists" << endl; //会输出
    cout << "1) " << mp.count(15) << endl; //输出 1) 1
    mp.insert(make_pair(20,9.3));
    cout << "2) " << mp[40] << endl;//如果没有关键字为40的元素,则插入一个
    cout << "3) ";Print(mp.begin(),mp.end());//输出:3) (40,0)(20,9.3)(15,2.7)
    mp[15] = 6.28; //把关键字为15的元素值改成6.28
    mp[17] = 3.14; //插入关键字为17的元素,并将其值设为3.14
    cout << "4) ";Print(mp.begin(),mp.end());
    return 0;
}

2.2 set/multiset

set的含义是集合,它是一个有序的容器,里面的元素都是排序好的支持插入、删除、查找等操作,就像一个集合一样,所有的操作都是严格在logn时间内完成,效率非常高。set和multiset的区别是:set插入的元素不能相同,但是multiset可以相同,set默认是自动排序的,使用方法类似list。

#include <iostream>
#include <set>  //使用set须包含此文件
using namespace std;
int main()
{
    typedef set<int>::iterator IT;
    int a[5] = { 3,4,6,1,2 };
    set<int> st(a,a+5);    // st里是 1 2 3 4 6
    pair< IT,bool> result;
    result = st.insert(5); // st变成  1 2 3 4 5 6
    if(result.second)    //插入成功则输出被插入元素
        cout << * result.first  << " inserted" << endl; //输出: 5 inserted
    if(st.insert(5).second)
        cout << * result.first  << endl;
    else
        cout << * result.first << " already exists" << endl;
    //输出 5 already exists
    pair<IT,IT> bounds = st.equal_range(4);
    cout << * bounds.first << "," << * bounds.second ;  //输出:4,5
    return 0;
}