Three Parts of STL
 约 938 个字  147 行代码  预计阅读时间 5 分钟
 
- Containers(容器)
- class templates, common data structures
 
- Algorithms(算法)
- Functions that operate on range of elements
 
- Iterators(迭代器)
- Generlization of pointers, access elements in a uniform manner
 
Containers
- Sequence Containers
- array(static), vector(dynamic)
- deque(double-ended queue)
- forward_list(singly-linked), list(doubly-linked)
 
- Associative
- set(collection of unique keys)
- map(collection of key-value pairs)
- multiset, multimap
 
- Unordered Associative
- hashed by keys
- unordered_set, unordered_map
- unordered_multiset, unordered_multimap
 
- Adaptors
- stack, queue, priority_queue
 
Using the vector
| C++ | 
|---|
|  | #include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> evens{2,4,6,8,10};
    evens.push_back(20);    //在尾端加一个元素
    evens.push_back(22);
    evens.insert(evens.begin()+4, 5, 10);    //在第4个元素前插入5个10
    for(int i = 0; i < evens.size(); i++)
    {
        cout << evens[i] << " ";
    }
    cout << endl;
}
 | 
得到如下输出:
| Text Only | 
|---|
|  | 2 4 6 8 10 10 10 10 10 20 22 
 | 
对于 vector 的遍历,我们还可以用每个容器自带的迭代器来实现:
| C++ | 
|---|
|  | for(vector<int>::iterator it = evens.begin(); it < evens.end(); ++it)
{
    cout << *it << " "; // 迭代器需要解引用
}
cout << endl;
 | 
每个容器的迭代器都遵循上边的接口,从 begin() 到 end(),只是在不同的容器中,迭代器的实现方式不同。
迭代器有什么用
你可能想说,对于遍历一个容器来说,我直接用下标不是更方便吗?为什么要用迭代器呢?
确实,但是迭代器的存在是为了让算法实现的时候比较通用,并且支持我们写一些语法糖。
 
Basic operations for vector
- Constructor and destructor
- Element access
- at(),- operator[],- front(),- back()
 
- Iterators
- begin(),- end(),- cbegin(),- cend()
 
- Capacity
- size(),- empty(),- reserve(),- capacity()
 
- Modifiers
- push_back(),- pop_back(),- insert(),- erase(),- swap(),- clear()
 
Using the list
| C++ | 
|---|
|  | #include <iostream>
#include <string>
#include <list>
using namespace std;
int main()
{
    list<string> s;
    s.push_back("hello");
    s.push_back("world");
    s.push_front("goodbye ");
    list<string>::iterator p;
    for(p = s.begin(); p != s.end(); p++)
    {
        cout << *p << " ";
    }
    cout << endl;
}
 | 
得到如下输出:
注意:这里使用迭代器遍历的时候,我们的终止条件是 p != s.end(),而不是 p < s.end(),因为 list 的迭代器不支持比较大小,只支持比较是否相等。
具体原因是 vector 的迭代器能力更强,它是支持 random access 即随机访问的,它的底层是一个数组,访问某个位置的元素只需要 O(1) 的时间复杂度。而 list 访问某个位置需要从头一个个遍历,时间复杂度是 O(n),所以 list 的迭代器只支持 == 和 != 操作。
所以为了统一,我们在遍历的时候都用 != 来判断是否到达了容器的末尾。
Using the map
底层是红黑树,关联式容器。
- Look up by key, and retrieve avalue.
- Example: for a phone book, map<string, string>
| C++ | 
|---|
|  | #include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
    map<string, float> price;
    price["apple"] = 1.99;
    price["banana"] = 0.99;
    for( const auto& pair : price)
    {
        cout << pair.first << " : " << pair.second << " ";
    }
    cout << endl;
    string item;
    double total = 0;
    while(cin >> item)
        total += price[item];
    cout << "Total: " << total << endl;
}
 | 
我们可以通过 pair.first 和 pair.second 来访问 map 中的 key 和 value。
随后我们输入
| Text Only | 
|---|
|  | apple
banana
apple
ctrl+d
 | 
得到如下输出:
| Text Only | 
|---|
|  | apple : 1.99 banana : 0.99
Total: 3.97
 | 
但是我们的程序有个问题:如果我们在终端中输入一个不存在的 key,比如 orange,那么程序自动在 map 中插入了一个 key 为 orange 的 value 为 0 的元素。
我们可以试一试:
| C++ | 
|---|
|  | #include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
    map<string, float> price;
    price["apple"] = 1.99;
    price["banana"] = 0.99;
    for( const auto& pair : price)
    {
        cout << "{" << pair.first << " : " << pair.second << "}" << " ";
    }
    cout << endl;
    string item;
    double total = 0;
    while(cin >> item)
        total += price[item];
    cout << "Total: " << total << endl;
    for( const auto& pair : price)
    {
        cout << "{" << pair.first << " : " << pair.second << "}" << " ";
    }
    cout << endl;
}
 | 
然后输入
得到如下输出:
| Text Only | 
|---|
|  | {apple : 1.99} {banana : 0.99}
Total: 0
{Jerry,0}{Tom,0}{apple,1.99}{banana,0.99}
 | 
所以我们需要在使用 map 的时候,先判断 key 是否存在,再进行操作。
| C++ | 
|---|
|  | while(cin >> item)
{
    if(price.contains(item))
        total += price[item];
    else
        cout << "Item not found!" << endl;
}
 | 
这样就不会出现上述问题了。
Question
这个功能存在的意义是什么呢?为什么要自动添加一个元素呢?
考虑如下代码:
| C++ | 
|---|
|  | #include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
    map<string, int> word_map;
    for(const auto& word : {"we", "are", "not", "humans", "we", "are", "robots", "!!"})
    {
        word_map[word]++;
    }
    for(const auto& word : word_map)
    {
        cout << "{" << word.first << " : " << word.second << "}" << " ";
    }
}
 | 
如上是一个统计词频的功能,我们不需要先把单词加入 map, 程序可以自动统计单词的出现次数,简化工作量。
 
Using the stack
| C++ | 
|---|
|  | #include <iostream>
#include <stack>
#include <string>
using namespace std;
bool is_balanced(const string & test)
{
    stack<char> st;
    for(char c : test)
    {
        if (c == '(' || c == '{' || c == '[')   //  先push左半边括号
        {
            st.push(c);
        }
        else if (c == ')' || c == '}' || c == ']')  // 如果是右半边,就pop栈顶的元素,检查是否匹配
        {
            if(st.empty()) 
                return false;
            char top = st.top();
            st.pop();
            if ((c == '}' && top != '{' ) || (c == ')' && top != '(') || (c == ']' && top != '['))
                return false;
        }
    }
    return st.empty();  // 如果最后栈为空,说明所有括号都匹配
}
int main()
{
    string test1 = "a(b{c[d]e}f)g";
    string test2 = "x(y{z[)})";
    cout << "Test 1:" << (is_balanced(test1) ? "Balanced" : "Unbalanced") << endl;
    cout << "Test 2:" << (is_balanced(test2) ? "Balanced" : "Unbalanced") << endl;
}
 | 
如上是我们使用 stack 来判断一个字符串中的括号是否匹配。
自己实现一个栈
| C++ | 
|---|
|  | template <typename T>
class Stack
{
public:
    virtual ~Stack() = default; // 虚析构函数
    virtual T& top();
    virtual bool empty() const = 0;
    virtual size_t size() const = 0;
    virtual void push(const T& value) = 0;
    virtual void pop() = 0;
};
template <typename T, typename Container = deque<T>>
class c_stack : public Stack<T>
{
public:
    T& top() override
    {
        return c.back();
    }
    bool empty() const override
    {
        return c.empty();
    }
    size_t size() const override
    {
        return c.size();
    }
    void push(const T& value) override
    {
        c.push_back(value);
    }
    void pop() override
    {
        c.pop_back();
    }
private:
    Container<T> c;
}
 | 
然后我们就可以用 c_stack 来实现作为一个栈的功能。
在我们有线性表的情况下,实现一个栈是很容易的。
 
这个就叫 Adaptor,它是一个适配器,用来适配不同的容器,使得它们有相同的接口。
Note
在标准库中,stack 默认使用 deque 作为底层容器,但是我们可以通过模板参数来指定底层容器。
 
Algorithms
- for_each,- find,- count
- copy,- fill,- transform,- replace,- rotate
- sort,- partial_sort,- nth_element
- set_difference,- set_union,- set_intersection
- min_element,- max_element,- accumulate,- partial_sum
| C++ | 
|---|
|  | #include <iostream>
#include <vector>
#include <iterator>
using namespace std;
int main()
{
    vector<int> v={1, 2, 3, 5};
    vector<int> u;
    reverse(v.begin(), v.end());    // 反转
    copy(v.begin(), v.end(), back_inserter(u));   // 复制到另一个容器
    copy(u.begin(), u.end(), ostream_iterator<int>(cout, ", "));    // 复制到标准输出流
    cout << endl;
}
 | 
得到如下输出:
但是这样输出并不是很美观,我们只想要逗号在中间输出,我们可以自定义一个输出迭代器,这个在日后会涉及。
Iterators
- connect containers and algorithms
- Talk about it later
- after templates and operator overloading
 
Typedefs
我们在平时编程的时候可能会遇到如 map<Name, list<PhoneNum>> phonebook 等名字很长的情况,这时候我们可以直接用 typedef 来简化:
| C++ | 
|---|
|  | typedef map<Name, list<PhoneNum>> PB;
PB phonebook;
 | 
在 c++11 中我们可以用 auto, using 来代替 typedef:
| C++ | 
|---|
|  | using PB = map<Name, list<PhoneNum>>;
 |