STL 模板

C++ 的标准模板库 (Standard Template Library, 简称 STL)。STL 是 C++ 标准库的一个重要组成部分,它提供了一系列通用、高效的数据结构和算法,极大地提高了 C++ 的编程效率和代码质量。

你可以把 STL 看作一个精心设计的“工具箱”,里面包含了各种常用的数据结构(比如动态数组、链表、树、哈希表)和操作这些数据结构的算法(比如排序、查找、计数)。而且这些工具是“通用”的,你可以用它们来处理不同的数据类型,甚至你自己定义的复杂对象。

在算法竞赛中,STL 也是解决问题的利器,不过这个“工具箱”的内容非常多,我们在本章节中只是介绍其中一些简单的内容。

模板

STL 其中的 T 是指的模板(Templates),理解模板是理解 STL 的基础,因为 STL 的所有组件(容器、算法、迭代器、函数对象)都是用模板实现的。

什么是模板

假设你需要编写一个函数来交换两个变量的值。如果变量是 int 类型,你可能会这样写:

void swap_int(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

如果变量是 double 类型呢?你需要再写一个几乎完全相同的函数:

void swap_double(double& a, double& b) {
    double temp = a;
    a = b;
    b = temp;
}

实际也还可能交换两个 string 类型变量的值,也有可能要交换的这两个变量是你自定义的结构体类型。你需要为每种类型都写一个重复的函数!这显示太繁琐了,代码也冗余。

有没有办法写一种“通用”的函数,独立于具体的数据类型?

模板就是 C++ 用来实现这种需求的工具,基于模板的编程我们通常称为泛型编程(Generic Programming),这里的泛其实就可以理解为“通用”。

函数模板

函数模板用于创建可以处理多种数据类型的函数。

定义函数模板

使用 template <typename T>来定义函数模板。T 是一个模板参数,代表一个待定的数据类型。

对于前面的交换两个变量的值的函数,我们可以这样写:

#include <bits/stdc++.h>
using namespace std;

// 定义一个函数模板,可以交换任意类型 T 的变量
template <typename T> // T 是一个类型参数
void swap_template(T& a, T& b) {
    T temp = a; // 使用类型参数 T
    a = b;
    b = temp;
}

int main() {
    // 这里是使用函数模板的地方
    return 0;
}
  • template <typename T>:声明这是一个模板,T 是一个类型参数。typename 和 class 在这里是等价的。
  • 在函数签名和函数体中,可以使用 T 来代表待定的数据类型。

使用(实例化)函数模板

当你调用一个函数模板时,编译器会根据你传入的实参的类型,自动推断出模板参数 T 的具体类型,然后生成(实例化)一个针对该类型的函数版本。

#include <bits/stdc++.h>
using namespace std;

template <typename T>
void swap_new(T& a, T& b) {
  T temp = a;
  a = b;
  b = temp;
}

int main() {
  int a1 = 10, a2 = 20;
  swap_new(a1, a2); // 编译器推断 T 为 int,生成 swap_new<int>
  cout << "a1:" << a1 << ", " << "a2:" << a2 << endl;
  
  // 你也可以显式指定模板参数的类型
  // swap_new<int>(a1, a2); // 显式实例化

  double b1 = 1.1, b2 = 2.2;
  swap_new(b1, b2); // 编译器推断 T 为 double,生成 swap_new<double>
  cout << "b1:" << b1 << ", " << "b2:" << b2 << endl;

  string s1 = "hello", s2 = "world";
  swap_new(s1, s2); // 编译器推断 T 为 string,生成 swap_new<string>
  cout << "s1:" << s1 << ", " << "s2:" << s2 << endl;

  return 0;
}

注意: 编译器只有在实际使用(调用)函数模板时才会进行实例化。如果你只定义了模板但从未使用它,编译器不会为它生成任何代码。

除了单个模板参数,C++ 还支持多个模板参数。另外,模板参数除了可以是类型,还可以是其它非类型(如整数、枚举值、指针等),这里就不一一细讲了,感兴趣的可以查阅相关资料。

类模板

类模板用于创建可以处理多种数据类型的类。STL 中的容器(如 vectorlistmap)都是类模板。

定义类模板

使用 template <typename T> 或 template <class T> 来定义类模板。

#include <bits/stdc++.h>
using namespace std;

// 定义一个简单的栈类模板
template <typename T> // T 是一个类型参数
class Stack {
private:
  vector<T> elements; // 使用类型参数 T 定义成员变量的类型
public:
  void push(T value) { // 使用类型参数 T 定义参数类型
    elements.push_back(value);
  }
  void pop() {
    if (!elements.empty()) {
        elements.pop_back();
    }
  }
  T top() { // 使用类型参数 T 定义返回类型
    return elements.back();
  }
  bool empty() const {
    return elements.empty();
  }
};

int main() {
  // 这里是使用类模板的地方
  return 0;
}
  • template <typename T>:声明这是一个模板,T 是一个类型参数。
  • 在类定义中,可以使用 T 来代表待定的数据类型。

使用(实例化)类模板

使用类模板时,必须显式指定模板参数的具体类型。编译器会根据指定的类型生成(实例化)一个针对该类型的类版本。

#include <bits/bits/stdc++.h>
using namespace std;

template <typename T>
class Stack {
private:
  vector<T> elements;
public:
  void push(T value) { elements.push_back(value); }
  void pop() { if (!elements.empty()) elements.pop_back(); }
  T top() { return elements.back(); }
  bool empty() const { return elements.empty(); }
};

int main() {
  // 实例化一个存储 int 的 Stack 类
  Stack<int> intStack;
  intStack.push(10);
  intStack.push(20);
  cout << "Int stack top: " << intStack.top() << endl; // 输出 20

  // 实例化一个存储 string 的 Stack 类
  Stack<string> stringStack;
  stringStack.push("hello");
  stringStack.push("world");
  cout << "String stack top: " << stringStack.top() << endl; // 输出 world

  return 0;
}
  • Stack<int>:显式指定模板参数 T 为 int,编译器会生成一个 Stack<int> 类。
  • Stack<string>:显式指定模板参数 T 为 string,编译器会生成一个 Stack<string> 类。

注意: 与函数模板不同,类模板的模板参数通常不能由编译器自动推断,需要显式指定(除了 C++17 引入的类模板参数推导)。

算法模板库常见函数

我们来看几个 STL 算法模板库 <algorithm> 中非常基础且常用的函数:minmaxswapsort

min 函数

min 函数用于找出两个或多个值中的最小值

示例

#include <bits/stdc++.h>
using namespace std;

bool cmp(const string& s1, const string& s2) {
  return s1.size() < s2.size();
}

int main() {
  int a = 10, b = 20;
  cout << min(a, b) << endl; // 输出 10

  double d1 = 3.14, d2 = 2.71;
  cout << min(d1, d2) << endl; // 输出 2.71

  string s1 = "apple", s2 = "banana";
  // 字符串按字典序比较
  cout << min(s1, s2) << endl; // 输出 apple

  // 使用自定义比较函数 (例如,按字符串长度比较)
  cout << min(s1, s2, cmp) << endl; // 输出 apple (长度 5 < 6)

  // 比较多个值 (C++11)
  cout << min({5, 2, 8, 1, 9}) << endl; // 输出 1

  return 0;
}

max 函数

max 函数用于找出两个或多个值中的最大值

示例

#include <bits/stdc++.h>
using namespace std;

bool cmp(const string& s1, const string& s2) {
  return s1.size() < s2.size();
}

int main() {
  int a = 10, b = 20;
  cout << max(a, b) << endl; // 输出 10

  double d1 = 3.14, d2 = 2.71;
  cout << max(d1, d2) << endl; // 输出 2.71

  string s1 = "apple", s2 = "banana";
  // 字符串按字典序比较
  cout << max(s1, s2) << endl; // 输出 apple

  // 使用自定义比较函数 (例如,按字符串长度比较)
  cout << max(s1, s2, cmp) << endl; // 输出 banana (长度 5 < 6)

  // 比较多个值 (C++11)
  cout << max({5, 2, 8, 1, 9}) << endl; // 输出 1

  return 0;
}

swap 函数

sort 函数

好的,我们来详细介绍一下 C++ STL 模板库中的几个核心容器:栈 (stack)队列 (queue)链表 (list)向量 (vector)。它们是 STL 中最常用的数据结构,每种容器都有其特定的特点和适用场景。


常见容器

容器是 STL 中用来存储和管理数据的类模板。它们提供了组织数据的方式以及访问和操作数据的方法。所有 STL 容器都遵循一些共同的接口约定(比如都有 begin(), end(), size(), empty() 等成员函数),但具体实现和性能特点各不相同。

容器都是类模板,这意味着在使用时需要指定它们存储的数据类型,例如 vector<int> 表示存储整数的向量,list<string> 表示存储字符串的链表。

向量 (vector)

vector 是一个动态数组。它在内存中是连续存储的,类似于普通数组,但它的大小是可变的。当需要存储更多元素时,vector 会自动重新分配更大的内存空间,并将现有元素复制过去。

vector 定义

#include <vector> // 要使用 vector,需要引入相应的头文件

// 定义一个 vector,名字是 v,未初始化
vector<int> v;

// 定义一个未初始化的 vector,然后将 vector 长度调整为 5
vector<int> v;
v.resize(5);

// 定义长度为 5 的 vector,
vector<int> v(5);

// 定义并初始化,初始化后,vector 长度为 3
vector<int> v = {1, 2, 3};

vector 初始化

// 方法1 
vector<int> v= {1, 2, 4};

// 方法2
// 定义一个长度为 10 的 vector,并将里面所有元素的值设为 1
vector<int> v(10, 1);

// 方法3
vector<int> v(10);
fill(v.begin(), v.end(), 1) // 将 vector 全部填充 1

基本上,使用 array 去完成的操作,在 vector 中都可以完成,而且使用 vector 会更灵活一些。

添加和删除元素

vector<int> v;
v.push_back(5); // 在 v 尾部增加元素 5
v.emplace_back(8); // 在 v 尾部增加元素 8
v.pop_back(); // 删除 v 最的一个元素

这里的 emplace_back() 是 C++ 11 标准新增加的,如果要兼容之前的版本,就使用 push_back(),也比较好记。11 标准以后,推荐使用 emplace_back()

emplace_back()push_back() 的主要区别在于底层实现的机制不同。push_back() 在向容器尾部添加元素时,首先会创建这个元素,然后再将创建出的元素拷贝或移动到容器;而 emplace_back 则是直接在容器尾部创建这个元素,省去了拷贝和移动的过程。emplace_back() 执行效率更高一些。

vector 遍历

既然 Vector 可以看作是一种动态数组,你可以像数组那样利用循环去遍历 Vector。

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

// vector 遍历
int main() {
    vector<int> v = {2, 1, -3, 8, -15};
    
    for (int i = 0; i < v.size(); i++) {
      cout << v[i] << " ";
    }
    
    return 0;
}

这里的 v.size() 是用来获取 Vector 的长度(也就是元素的个数),其它的操作与遍历数组是完全一样的。

也可以使用 v.length() 去获取 Vector 的长度,与 v.size() 是同样的效果,v.size() 是为了兼容 STL 容器的操作在 C++ 11 后加入的。

与数组一样,也可以直接使用 Ranged for 来遍历 Vector 中的所有元素。

for (const int& i : v) {
  cout << i << " ";
}

vector 适用场景

  • 需要频繁在末尾添加或删除元素。
  • 需要快速随机访问元素。
  • 元素数量不确定,需要动态调整大小。
  • 作为其他容器适配器(如 stack, queue)的底层容器。

链表 (list)

list 是一个双向链表。每个元素都包含指向前一个元素和后一个元素的指针。元素在内存中不是连续存储的。

特点

  • 非连续存储: 元素分散在内存中。
  • 快速插入/删除: 在链表的任意位置插入或删除元素的时间复杂度是 O(1)(前提是已经有了指向该位置的迭代器)。
  • 不支持随机访问: 不能通过下标直接访问元素,需要从头或尾遍历,访问第 n 个元素的时间复杂度是 O(n)。
  • 迭代器支持: 提供双向迭代器。
  • 额外的内存开销: 每个元素都需要额外的空间来存储前后指针。

常用成员函数

  • list<T> l: 默认构造一个空 list。
  • l.push_back(value): 在末尾添加元素。
  • l.push_front(value): 在开头添加元素。
  • l.pop_back(): 删除末尾元素。
  • l.pop_front(): 删除开头元素。
  • l.size(): 返回元素个数 (O(n) 或 O(1) 取决于实现,C++11 后通常是 O(1))。
  • l.empty(): 判断是否为空。
  • l.begin(): 返回指向第一个元素的迭代器。
  • l.end(): 返回指向最后一个元素之后位置的迭代器。
  • l.insert(pos, value): 在指定位置 pos 前插入一个元素。
  • l.erase(pos): 删除指定位置 pos 的元素。
  • l.clear(): 清空所有元素。
  • l.sort(): 对链表进行排序 (链表自己的成员函数,因为不支持随机访问)。

适用场景

  • 需要频繁在链表的中间位置进行插入或删除操作。
  • 不需要快速随机访问元素。

示例

#include <bits/stdc++.h>
using namespace std;

int main() {
  list<string> words; // 声明一个 string 类型的 list

  words.push_back("world");
  words.push_front("hello"); // 在开头添加

  cout << "List elements: ";
  for (const string& word : words) {
      cout << word << " ";
  }
  cout << endl; // 输出: List elements: hello world

  auto it = words.begin();
  advance(it, 1); // 将迭代器向前移动 1 位,指向 "world"

  words.insert(it, "beautiful"); // 在 "world" 前插入 "beautiful"
  cout << "After insert: ";
  for (const string& word : words) {
      cout << word << " ";
  }
  cout << endl; // 输出: After insert: hello beautiful world

  it = words.begin();
  advance(it, 1); // 将迭代器向前移动 1 位,指向 "beautiful"
  words.erase(it); // 删除 "beautiful"
  cout << "After erase: ";
  for (const string& word : words) {
      cout << word << " ";
  }
  cout << endl; // 输出: After erase: hello world

  return 0;
}

栈 (stack)

stack 是一个容器适配器。它不是一个独立的容器,而是基于现有的底层容器(默认是 deque,也可以是 vectorlist)提供了一个后进先出 (LIFO) 的接口。

你可以想象一个栈就是一摞盘子,你只能在最上面放盘子或拿盘子。

特点

  • LIFO (Last-In, First-Out): 最后进入的元素最先出来。
  • 接口受限: 只提供 push (入栈)、pop (出栈)、top (查看栈顶元素)、empty (判断是否为空)、size (获取大小) 等操作。不能访问栈底或中间的元素。
  • 底层容器: 默认使用 deque 作为底层容器,也可以指定为 vectorlist

常用成员函数

  • stack<T> s: 默认构造一个空栈 (底层使用 deque)。
  • stack<T, vector<T>> s_vec: 构造一个底层使用 vector 的栈。
  • s.push(value): 将元素压入栈顶。
  • s.pop(): 移除栈顶元素。
  • s.top(): 返回栈顶元素的引用 (不移除)。
  • s.empty(): 判断栈是否为空。
  • s.size(): 返回栈中元素个数。

适用场景

  • 需要实现后进先出逻辑的场景,例如:
    • 函数调用栈。
    • 表达式求值(中缀转后缀)。
    • 括号匹配。
    • 深度优先搜索 (DFS) 的非递归实现。

示例

#include <bits/stdc++.h>
using namespace std;

int main() {
    stack<int> myStack; // 声明一个 int 类型的栈 (底层默认 deque)

    myStack.push(10); // 入栈
    myStack.push(20);
    myStack.push(30);

    cout << "Stack elements (LIFO): ";
    while (!myStack.empty()) {
        cout << myStack.top() << " "; // 查看栈顶
        myStack.pop(); // 出栈
    }
    cout << endl; // 输出: Stack elements (LIFO): 30 20 10
    return 0;
}

队列 (queue)

queue 也是一个容器适配器。它基于现有的底层容器(默认是 deque,也可以是 list)提供了一个先进先出 (FIFO) 的接口。

你可以想象一个队列就是排队买票的人群,第一个排队的人最先买到票离开。

特点

  • FIFO (First-In, First-Out): 最先进入的元素最先出来。
  • 接口受限: 只提供 push (入队)、pop (出队)、front (查看队头元素)、back (查看队尾元素)、empty (判断是否为空)、size (获取大小) 等操作。不能访问队列中间的元素。
  • 底层容器: 默认使用 deque 作为底层容器,也可以指定为 list

常用成员函数

  • queue<T> q: 默认构造一个空队列 (底层使用 deque)。
  • queue<T, list<T>> q_list: 构造一个底层使用 list 的队列。
  • q.push(value): 将元素添加到队尾。
  • q.pop(): 移除队头元素。
  • q.front(): 返回队头元素的引用 (不移除)。
  • q.back(): 返回队尾元素的引用 (不移除)。
  • q.empty(): 判断队列是否为空。
  • q.size(): 返回队列中元素个数。

适用场景

  • 需要实现先进先出逻辑的场景,例如:
    • 任务调度。
    • 消息队列。
    • 广度优先搜索 (BFS)。

示例

#include <bits/stdc++.h>
using namespace std;

int main() {
    queue<string> myQueue; // 声明一个 string 类型的队列 (底层默认 deque)

    myQueue.push("first"); // 入队
    myQueue.push("second");
    myQueue.push("third");

    cout << "Queue elements (FIFO): ";
    while (!myQueue.empty()) {
        cout << myQueue.front() << " "; // 查看队头
        myQueue.pop(); // 出队
    }
    cout << endl; // 输出: Queue elements (FIFO): first second third
    return 0;
}

如何选择合适的容器?

  • 如果需要快速随机访问,并且主要在末尾进行添加/删除,选择 vector
  • 如果需要频繁在任意位置进行添加/删除,并且不需要快速随机访问,选择 list
  • 如果只需要实现后进先出的逻辑,选择 stack
  • 如果只需要实现先进先出的逻辑,选择 queue

理解这些容器的底层实现和性能特点,是高效使用 STL 的关键。在解决问题时,根据数据的访问模式和操作需求,选择最合适的容器。

总结

本章介绍了 C++ STL 中的一些基础内容,掌握基础知识,可以帮助我们写出更简洁、高效的代码,在算法竞赛和实际开发中都能发挥重要作用。