C++标准模板库——容器

之前章节介绍的分支、循环、函数、递归……等概念都是命令式编程语言通用的思想方法。时常有同学问:我们到底学的是 C 还是 C++?可以说之前大家写的程序基本都是 C 风格的 (除了 cin cout string 等少数内容)。这一章介绍的标准模板库 (Standard Template Library, STL),是 C++ 区别于 C 的重要内容之一。STL 提供的内容将程序员从复杂的底层算法和数据结构中解放出来,使得程序员写程序更加得心应手。

标准模板库由四个部分构成:

  • 算法 (Algorithm)
  • 容器 (Container)
  • 函数 (Function)
  • 迭代器 (Iterator)

这一章主要讲解容器。

大家最近学习了“抽象数据结构“,STL的容器可以理解为C++库为大家实现好了一批数据结构,你只要读懂这些容器对外暴露的接口的功能说明,合理地使用这些接口,就可以在不知道底层实现的情况下享受这些数据结构的福利。我们在这里介绍几个最常用的STL容器和相关操作。

注:STL容器的用法极其丰富,这里只是浮光掠影简单介绍,大家可以在 cppreference 上查询详细的方法列表。

vector

vector 的中文名是向量,但这个容器和数学中的向量几乎毫无关系,vector 其实更像一个可以自由变换长度,完成一系列操作的“动态数组”。当你不能确定数组长度,或者需要在任意位置插入/删除元素时,vector 将会成为你的一大助力。下面我们通过例子展示 vector 的常见使用方法:

  • 初始化

    // 通用格式: vector<类型> 名字(最大容量,初始值)
    #include <vector>  // 大家常用的 <bits/stdc++.h> 已经包括了该头文件
    
    int main ()
    {
        vector<double> v_double(0);  // 一个空的,存储的变量为 double 类型的 vector
        vector<int> v_int(10, 0);    // 一个长度为 10 的 vector,初始所有元素为 0
    }
    
  • 元素访问

    // vector 和数组一样支持用下标访问,下标从 0 开始
    vector<int> v_int(10, 1);
    cout << v_int[2];  // 输出: 0
    cout << v_int[10]; // 下标越界,未定义行为!
    
  • 迭代器

    虽然迭代器的概念有点复杂,但为了更好地使用容器,我们还是简单地介绍一下。迭代器可以理解为指向容器中某个元素的“指针”,如果你不知道什么是指针,你可以把它想象成一个“小箭头”。

    vector 中最常用的几个和迭代器相关的函数有:

    • begin():返回一个指向第一个元素的迭代器。
    • end():返回一个指向最后一个元素的“下一个位置”的迭代器。
    • rbegin():返回指向最后一个元素的迭代器。

    迭代器可以通过简单的 “+1/-1” 操作向右/左移动。我们通过迭代器可以顺序访问数组中的所有元素:

    // 假设当前 vector<int> v 中有 5 个元素,分别是 1, 2,3, 4, 5
    for (vector<int>::iterator iter = v.begin(); iter != v.end(); iter++)
        cout << *iter << ' ';   // 通过 *iterator 的方式来获取”小箭头“指向的元素的值
    // 输出结果: 1 2 3 4 5
    for (vector<int>::iterator iter = v.rbegin(); iter != v.begin(); iter--)
        cout << *iter ** ' ';
    // 输出结果: 5 4 3 2,请体会 end() 与 begin() 的不同之处!
    
  • 元素的添加和删除

    vector<int> v_int(3, 1);        // vector 内容:[1, 1, 1]
    // push_back(x) 方法用于在末尾添加元素 x
    v_int.push_back(2)              // vector 内容:[1, 1, 1, 2]
    v_int.push_back(3)              // vector 内容:[1, 1, 1, 2, 3]
    // insert(iter, x) 方法用于在迭代器 iter 指向的位置前插入元素 x
    v_int.insert(v_int.begin(), 3)  // vector 内容:[3, 1, 1, 1, 2, 3]
    // pop_back() 方法用于删除最后一个元素
    v_int.pop_back()                // vector 内容:[3, 1, 1, 1, 2]
    // erase(iter) 方法用于删除迭代器 iter 指向的元素
    v_int.erase(v_int.begin() + 1)  // vector 内容:[3, 1, 1, 2]
                                    // 注:删除了第二个元素
    // clear() 函数用于清空 vector
    v_int.clear()                   // vector 内容:[]
    
  • 相关参数

    // 假设当前 vector<int> v 中有 5 个元素,分别是 1, 2,3, 4, 5
    cout << int(v.size());     // 输出:5
    bool isEmpty = v.empty()   // isEmpty = false
    

stack

stack (栈) 是一个先进后出的容器,你可以把它想象成一个电梯:第一个进入电梯的人总是最后一个出来。下面是一些简单的用法:

stack<int> s;                   // 初始为空
// push(x) 方法向栈顶放入元素 x
s.push(1);
s.push(2);
s.push(3);                      // s的内容:  (底) [1, 2, 3] (顶)
// top() 方法用于获取栈顶元素
int currentTop = s.top();       // currentTop = 3, s: [1, 2, 3]
// pop() 方法用于弹出栈顶元素
s.pop();                        // s: [1, 2]
currentTop = s.top();           // currentTop = 2
// size() 方法用于获取 s 中元素个数
cout << int(s.size());     // 输出:2
// empty() 方法用于判断 s 是否为空
bool isEmpty = s.empty();       // isEmpty = false

queue

queue (队列) 是一个先进先出的容器,你可以把它想象成一个双开门电梯:第一个进入电梯的人第一个出来。下面是一些简单的用法:

queue<int> q;                     // 初始为空
// push(x) 方法向队列的尾部加入元素 x
q.push(1);
q.push(2);
q.push(3);                        // q的内容: (队首) [1, 2, 3] (队尾)
// front() 方法用于获取队首元素
int currentFront = q.front();     // currentFront = 1
// q.pop() 方法用于弹出队首元素
q.pop();                          // q: [2, 3]
currentFront = q.front();         // currentFront = 2
// size() 方法用于获取 q 中元素个数
int currentSize = int(q.size());  // currentSize = 2
// empty() 方法用于判断 q 是否为空
bool isEmpty = q.empty();         // isEmpty = false

set

set (集合) 顾名思义实现了一个集合应有的功能:插入、去重、判断一个元素是否存在。特别的是 set 中的元素还是按照顺序排列的 (这其实和集合定义中的无序性稍有不符)。下面是一些简单的用法:

set<int> s;                            // s: {}
// insert(x) 方法向集合插入元素 x
s.insert(2);                           // s: {2}
s.insert(1);                           // s: {1, 2}
s.insert(1);                           // s: {1, 2},重复元素不会被反复插入
s.insert(3);                           // s: {1, 2, 3}
// find(x) 方法查询 x 是否在集合中,如果在则返回指向该元素的迭代器,否则返回 s.end()
set<int>::iterator iter_1 = s.find(1);
cout << *iter;                    // 输出:1
set<int>::iterator iter_0 = s.find(0);
bool isEnd = (iter_0 == s.end());      // isEnd = true
// erase(x) 方法用于删除元素 x
s.erase(2);                            // s: {1, 3}
// 使用迭代器按顺序访问 s,会发现元素是按顺序排列的
for (set<int>::iterator iter = s.begin(); iter != s.end(); iter++)
    cout << *iter << ' ';         // 输出:1 3
// clear() 方法用于清空集合
s.clear();                             // s: {}

映射

在标题中使用 map 这个单词会被自动渲染成地图。。。因此使用了“映射”作为标题

map (映射) 和数学中的映射一样,维护了一个 key-value pair 的集合。下面是一些简单的用法:

map<string, int> m;                          // 这是一个从 string 到 int 的映射
// 映射的插入非常简单:直接使用数组赋值的语法格式即可
m["apple"] = 1;
m["banana"] = 2;
// 访问一个 key 对应的 value:直接使用数组访问的语法格式
cout << m["apple"];                          // 输出:1
cout << m["orange"];                         // 对于不存在的 key,通常会输出默认值 0
m["apple"] = 3;
cout << m["apple"];                          // 输出:3
// find(x) 方法用于查询映射中是否有 x 这个 key,如果有则返回指向该 pair 的迭代器,否则返回 m.end()
map<string, int>::iterator iter_pear = m.find("pear");
bool isEnd = (iter_pear == m.end())          // isEnd = true
map<string, int>::iterator iter_banana = m.find("banana");
// 使用 .first 获取 key,.second 获取 value
cout << *iter_banana.first << ' ' << *iter_banana.second;  // 输出: banana 2
Next