Bootstrap

C++STL库的各种容器

C++中的STL指的是标准模板库(Standard Template Library ),本文将从标准容器近容器迭代器函数对象泛型算法五个方面介绍STL的基本内容。

1 标准容器

标准容器包括:顺序容器,容器适配器,关联容器。

  • 顺序容器vectordequelistarrayforward_list。后两个是C++11标准支持的。
  • 容器适配器stackqueuepriority_queue
  • 关联容器:无序关联容器和有序关联容器
无序关联容器有序关联容器
unordered_setset
unordered_multisetmultiset
unordered_mapmap
unordered_multimapmultimap

1.1 vector

vector是向量容器,在使用前需要引入头文件。

#include <vector>
vector<int> vec;

vector底层的数据结构是动态开辟的数组,每次以原来大小2倍(1.5倍)的数量进行扩容。

增加元素

vec.push_back(20); // 末尾添加元素,`O(1)`,可能会导致容器扩容。
vec.insert(it, 20); // it迭代器指向的位置添加一个元素20,`O(n)`,可能会导致容器扩容。

删除元素

vec.pop_back(); // 末尾删除元素,O(1)
vec.erase(it); // 删除it迭代器指向的元素,O(n)

查询

operator[] // 下标的随机访问
iterator   // 迭代器进行遍历
find
for_each   // 通过iterator实现

注意:对容器进行连续插入或者删除操作(insert/erase),一定要更新迭代器,否则第一次insert或者erase完成,迭代器就失效了。

常用方法

size()
empty()
reserve(20) // vector预留空间的,只给容器底层开辟指定大小的空间,并不会添加新的元素
resize(20)  // 容器扩容使用,不仅给容器底层开辟指定大小的空间,还会添加新的元素
swap        // 两个容器进行元素交换

1.2 deque

deque是双端队列容器,使用前需要引入头文件。

#include <deque>
deque<int> deq;

底层数据结构是动态开辟的二维数组,一维数组以2开始,以2倍的方式进行扩容,每次扩容后,原来第二维的数组,从第一维数组的下标 o l d s i z e / 2 oldsize / 2 oldsize/2开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加。

#define MAP_SIZE 2
#define QUE_SIZE 4096/sizeof(T)

增加元素

deq.push_back(20);  // 从末尾添加元素,O(1)
deq.push_front(20); // 从首部添加元素,O(1)
deq.insert(it, 20); // it指向的位置添加元素,O(n)

删除元素

deq.pop_back();  // 从末尾删除元素,O(1)
deq.pop_front(); // 从首部删除元素,O(1)
deq.erase(it);   // 从it指向的位置删除元素,O(n)

查询

iterator(连续的inserterase一定要考虑迭代器失效的问题)

1.3 list

list是列表容器,使用需要添加头文件。

#include <list>
list<int> lst;

底层数据结构:双向的循环链表

增加元素

lst.push_back(20);  // 从末尾添加元素,O(1)
lst.push_front(20); // 从首部添加元素,O(1)
lst.insert(it, 20); // it指向的位置添加元素,O(1)。但是在链表中进行insert的时候,先要进行一个query查询操作,这个效率比较慢

删除元素

lst.pop_back();  // 从末尾删除元素,O(1)
lst.pop_front(); // 从首部删除元素,O(1)
lst.erase(it);   // 从it指向的位置删除元素,O(1)

查询搜索

iterator(连续的inserterase一定要考虑迭代器失效的问题)

1.4 vector,deque和list三者的区别?

以下是一个总结 vectordequelist 区别的表格:

特性vectordequelist
内部实现动态数组一组连续内存块组成的双端队列双向链表
内存分配连续内存块分段的连续内存块每个元素独立节点,内存不连续
随机访问时间常数时间 O(1)常数时间 O(1)(略慢于 vector线性时间 O(n)
插入/删除时间尾部:常数时间 O(1)
中间/头部:线性时间 O(n)
头尾:常数时间 O(1)
中间:线性时间 O(n)
常数时间 O(1)(找到位置后)
特点高效随机访问
适合尾部插入/删除
头尾双端高效操作
适合双端插入/删除
高效任意位置插入/删除
不适合随机访问
适用场景需要高效随机访问
主要在尾部操作
需要头尾高效插入/删除操作需要高效任意位置插入/删除

1.5 容器适配器

什么是容器适配器?

适配器没有自己的数据结构,它是另外一个容器的封装,它的方法全部由依赖的底层容器进行实现的,也没有实现自己的迭代器。

template<typename T, typename Container=deque<T>>
class stack
{
public:
    void push(const T &val) { con.push_back(val); }
    void pop() { con.pop_back(); }
    T top() const { return con.back(); }
private:
    Container con;
};

容器适配器常用的方法

以下是一个总结容器适配器常用方法的表格:

适配器方法描述
stackpush(const T& value)将元素压入栈顶
push(T&& value)将元素移动到栈顶
void pop()移除栈顶元素
T& top()返回栈顶元素的引用
const T& top() const返回栈顶元素的常量引用
bool empty() const检查栈是否为空
size_t size() const返回栈中的元素个数
queuevoid push(const T& value)将元素插入队列末尾
void push(T&& value)将元素移动到队列末尾
void pop()移除队列首元素
T& front()返回队列首元素的引用
const T& front() const返回队列首元素的常量引用
T& back()返回队列尾元素的引用
const T& back() const返回队列尾元素的常量引用
bool empty() const检查队列是否为空
size_t size() const返回队列中的元素个数
priority_queuevoid push(const T& value)将元素插入优先队列
void push(T&& value)将元素移动到优先队列
void pop()移除优先队列中的最大元素(默认情况下)
const T& top() const返回优先队列中的最大元素(默认情况下)
bool empty() const检查优先队列是否为空
size_t size() const返回优先队列中的元素个数

stack和queue为什么依赖deque?

stackqueue 默认依赖 deque 作为底层容器的原因主要与 deque 的数据结构特性和性能优势有关。以下是详细的原因解释:

  1. deque(双端队列)的特性
    deque(双端队列)是一种可以在两端(前端和后端)高效地进行插入和删除操作的序列容器。它的设计使得在两端进行操作的时间复杂度为常数时间(O(1)),这与栈和队列的操作需求非常契合。

  2. stack(栈)依赖 deque 的原因

    • 后进先出(LIFO)特性stack 的主要操作是 push(在栈顶插入元素)和 pop(移除栈顶元素),这两个操作在 deque 的末端执行,性能为常数时间。

    • 高效的内存管理deque 由多个块组成,扩展时不需要移动已有的元素,只需分配新的块,这样的内存管理方式更高效,避免了 vector 的大规模内存复制。

    • 灵活的内存分配deque 能够动态地分配和释放内存,而无需像 vector 那样一次性分配大块连续内存,这使得 stack 在使用 deque 作为底层容器时更加高效和灵活。

  3. queue(队列)依赖 deque 的原因

    • 先进先出(FIFO)特性queue 的主要操作是 push(在队列尾插入元素)和 pop(移除队列首元素),这两个操作分别在 deque 的两端执行,性能为常数时间。

    • 双端操作的效率deque 在两端的插入和删除操作都具有常数时间复杂度,确保了 queue 在操作上的高效性。

    • 灵活的内存管理:与 stack 类似,deque 的内存管理方式使得 queue 能够在不进行大规模内存复制的情况下,灵活地扩展和收缩。

  4. vector 的比较

    • 插入和删除操作的效率vector 仅在末尾插入和删除操作是常数时间,而在其他位置进行插入和删除操作是线性时间(O(n))。这对于 queue 来说,在队列首部进行删除操作的效率较低。

    • 内存重分配vector 需要在扩展时重新分配并复制整个数组,这在频繁插入和删除操作时性能较差。

    • 连续内存块的需求vector 需要连续的内存块,这在大规模数据处理时可能会导致内存碎片化问题,而 deque 由于其分块存储结构,更能有效地利用内存。

因此,stackqueue 默认选择 deque 作为底层容器,是因为 deque 的数据结构特性和内存管理方式更适合它们的操作需求,提供了更高的性能和灵活性。当然,用户也可以根据具体需求选择其他底层容器,例如 vectorlist

priority_queue为什么依赖vector?

priority_queue 默认使用 vector 作为底层容器,原因主要在于 vector 的特性非常适合实现堆结构,这是 priority_queue 的基础数据结构。

  1. 堆结构的特性

    • priority_queue 是基于堆(heap)实现的,通常是最大堆或最小堆。堆是一种完全二叉树,通常使用数组或动态数组来实现。
  2. vector 适合堆实现的原因

    • 连续内存块vector 在内存中是连续存储的,这使得可以非常高效地使用数组下标来访问堆中的元素。堆的父子节点关系可以通过简单的算术运算(索引计算)来确定,利用 vector 的连续内存,可以高效地实现这些操作。

    • 动态扩展vector 提供了动态扩展的能力,可以在不需要手动管理内存的情况下增长容量,这对于需要动态调整大小的 priority_queue 非常重要。

    • 随机访问效率vector 支持常数时间的随机访问,这对于堆的操作(如插入和删除)非常关键,因为堆操作需要频繁访问和调整数组中的元素位置。

  3. 堆操作的具体实现

    • 插入操作(push):在堆中插入元素时,通常将新元素添加到数组末尾,然后通过上滤(sift-up)操作调整堆结构。vector 的末尾插入操作是常数时间,这非常高效。

    • 删除操作(pop):删除堆顶元素(即最大或最小元素)时,通常将末尾元素移到堆顶,然后通过下滤(sift-down)操作调整堆结构。vector 末尾删除操作是常数时间,再结合下滤操作,可以高效地完成堆顶元素的删除。

  4. deque list的比较

    • deque:虽然 deque 支持在两端进行高效插入和删除,但由于其分块存储结构,不适合堆的索引访问模式,无法高效地进行堆操作。

    • listlist(链表)不支持常数时间的随机访问,无法通过索引直接访问元素,这使得实现堆操作(如上滤和下滤)变得低效和复杂。

priority_queue 默认使用 vector 作为底层容器,因为 vector 提供了连续内存、动态扩展和常数时间随机访问的特性,这些都非常适合实现高效的堆操作。相比之下,dequelist 的特性在实现堆时并不具备同样的优势。

1.6 关联容器

以下是关联容器和无序关联容器的总结,包含底层数据结构和常用的增删查方法:

容器类型底层数据结构插入元素删除元素查找元素
std::map红黑树myMap.insert({key, value})myMap.erase(key)myMap.find(key)
myMap[key] = valuemyMap.erase(it)
std::multimap红黑树myMultimap.insert({key, value})myMultimap.erase(key)myMultimap.equal_range(key)
std::set红黑树mySet.insert(value)mySet.erase(value)mySet.find(value)
mySet.erase(it)
std::multiset红黑树myMultiset.insert(value)myMultiset.erase(value)myMultiset.equal_range(value)
std::unordered_map哈希表myUnorderedMap.insert({key, value})myUnorderedMap.erase(key)myUnorderedMap.find(key)
myUnorderedMap[key] = valuemyUnorderedMap.erase(it)
std::unordered_multimap哈希表myUnorderedMultimap.insert({key, value})myUnorderedMultimap.erase(key)myUnorderedMultimap.equal_range(key)
std::unordered_set哈希表myUnorderedSet.insert(value)myUnorderedSet.erase(value)myUnorderedSet.find(value)
myUnorderedSet.erase(it)
std::unordered_multiset哈希表myUnorderedMultiset.insert(value)myUnorderedMultiset.erase(value)myUnorderedMultiset.equal_range(value)

2 近容器

数组、字符串和位集(bitset)是三种基本的近容器,它们在数据存储和操作中具有不同的特性和用途。下面从底层数据结构和常用的增删查方法两个方面来介绍这三种近容器。

2.1 数组(Array)

特点

  • 底层数据结构: 连续的内存块。
  • 特点: 固定大小,元素类型相同,支持随机访问,索引从0开始。
常用方法
  • 初始化

    int arr[5] = {1, 2, 3, 4, 5};  // 静态数组
    std::array<int, 5> arr = {1, 2, 3, 4, 5};  // C++11标准库数组
    
  • 插入元素

    • 数组大小固定,不能直接插入元素,只能通过重新分配数组来插入。
  • 删除元素

    • 数组大小固定,不能直接删除元素,只能通过重新分配数组来删除。
  • 查找元素

    int value = arr[2];  // 访问索引为2的元素
    
  • 遍历数组

    for (int i = 0; i < 5; ++i) {
      std::cout << arr[i] << std::endl;
    }
    

2.2 字符串(String)

特点
  • 底层数据结构: 动态数组,通常是字符数组。
  • 特点: 动态大小,可变长度,支持各种字符串操作。
常用方法
  • 初始化

    std::string str = "Hello, world!";
    
  • 插入元素

    str.insert(5, ",");
    
  • 删除元素

    str.erase(5, 1);
    
  • 查找元素

    size_t pos = str.find("world");  // 返回子字符串"world"的起始位置
    
  • 遍历字符串

  for (char c : str) {
    std::cout << c << std::endl;
  }

2.3 位集(Bitset)

特点
  • 底层数据结构: 固定大小的位数组。
  • 特点: 用于高效存储和操作二进制位,适用于位操作场景。
常用方法
  • 初始化

    std::bitset<8> bset("10101010");  // 从字符串初始化
    std::bitset<8> bset(42);  // 从整数初始化
    
  • 设置位

    bset.set(2);  // 将索引为2的位设为1
    bset.set();   // 将所有位设为1
    
  • 清除位

    bset.reset(2);  // 将索引为2的位设为0
    bset.reset();   // 将所有位设为0
    
  • 翻转位

    bset.flip(2);  // 翻转索引为2的位
    bset.flip();   // 翻转所有位
    
  • 查找位

    bool bit = bset.test(2);  // 返回索引为2的位的值
    
  • 遍历位集

    for (size_t i = 0; i < bset.size(); ++i) {
      std::cout << bset[i] << std::endl;
    }
    

2.4 总结

近容器类型底层数据结构特点常用操作
数组连续的内存块固定大小,随机访问初始化、遍历、索引访问
字符串动态数组动态大小,各种字符串操作初始化、插入、删除、查找、遍历
位集位数组固定大小,高效位操作初始化、设置位、清除位、翻转位、查找位、遍历

这些近容器在不同的应用场景中提供了基础的存储和操作功能。数组适用于需要固定大小和快速随机访问的场景;字符串适用于需要处理文本和动态长度的场景;位集适用于需要高效位操作的场景。

3 迭代器

迭代器是一种通用的工具,用于遍历容器中的元素。在C++标准库中,迭代器提供了一种一致的方式来访问容器中的元素,使得算法可以独立于具体的容器实现。以下是迭代器的几种常见类型:

3.1 iterator和const_iterator

iterator

iterator是可读写的迭代器,允许修改其指向的元素。

示例

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    *it *= 2;  // 修改元素值
}

const_iterator

const_iterator是只读迭代器,不允许修改其指向的元素。

示例

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it) {
    std::cout << *it << " ";  // 只读访问
}

3.2 reverse_iterator和 const_reverse_iterator

reverse_iterator

reverse_iterator是反向迭代器,从容器的末尾向起始位置遍历。

示例

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit) {
    std::cout << *rit << " ";  // 反向遍历
}
const_reverse_iterator

const_reverse_iterator是只读反向迭代器,从容器的末尾向起始位置遍历,且不允许修改元素。

示例

std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::const_reverse_iterator rit = vec.crbegin(); rit != vec.crend(); ++rit) {
    std::cout << *rit << " ";  // 只读反向遍历
}

3.3 总结

迭代器类型作用示例
iterator可读写正向迭代器std::vector<int>::iterator it = vec.begin();
const_iterator只读正向迭代器std::vector<int>::const_iterator it = vec.cbegin();
reverse_iterator可读写反向迭代器std::vector<int>::reverse_iterator rit = vec.rbegin();
const_reverse_iterator只读反向迭代器std::vector<int>::const_reverse_iterator rit = vec.crbegin();

迭代器使得操作容器变得更为灵活和统一,无论是正向遍历、反向遍历、修改元素还是只读访问,都可以通过相应的迭代器类型来实现。

4 函数对象(类似C的函数指针)

函数对象(functor)是在C++中一种通过重载operator()来实现类似函数调用行为的对象。函数对象可以存储状态并在函数调用操作符中使用这些状态。标准库中定义了许多有用的函数对象,包括std::greaterstd::less,它们在排序和比较操作中非常有用。

4.1 函数对象(Functor)

定义和示例

函数对象是一个类或结构体,该类或结构体重载了operator()

示例

#include <iostream>

// 定义一个函数对象
struct Add {
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Add add;
    int result = add(2, 3);  // 使用函数对象进行调用
    std::cout << "Result: " << result << std::endl;
    return 0;
}

在这个例子中,Add是一个函数对象类,重载了operator(),使得它可以像普通函数一样调用。

4.2 std::greater和std::less

std::greater

std::greater是一个函数对象,用于比较两个值并返回第一个值是否大于第二个值。

定义

template <class T>
struct greater {
    constexpr bool operator()(const T& lhs, const T& rhs) const {
        return lhs > rhs;
    }
};

使用示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 4, 2, 8, 5, 7};
    
    // 使用std::greater进行排序
    std::sort(vec.begin(), vec.end(), std::greater<int>());
    
    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

在这个例子中,std::greater<int>()用于将向量vec按降序排序。

std::less

std::less是一个函数对象,用于比较两个值并返回第一个值是否小于第二个值。

定义

template <class T>
struct less {
    constexpr bool operator()(const T& lhs, const T& rhs) const {
        return lhs < rhs;
    }
};

使用示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 4, 2, 8, 5, 7};
    
    // 使用std::less进行排序(默认排序)
    std::sort(vec.begin(), vec.end(), std::less<int>());
    
    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    return 0;
}

在这个例子中,std::less<int>()用于将向量vec按升序排序(虽然这是std::sort的默认行为,不需要明确指定std::less<int>())。

4.3 总结

名称功能使用场景
函数对象(Functor)通过重载operator()实现类似函数调用的对象需要存储状态并在调用时使用这些状态的场景
std::greater比较两个值,返回第一个值是否大于第二个值用于降序排序或需要比较两个值大小的场景
std::less比较两个值,返回第一个值是否小于第二个值用于升序排序或需要比较两个值大小的场景(std::sort默认行为)

5 泛型算法

C++标准库提供了一组强大且通用的算法,它们可以与各种容器和数据类型一起使用。这些算法通过模板机制实现,使其非常灵活和高效。以下是一些常见的泛型算法及其使用介绍:

5.1 std::sort

功能

对范围内的元素进行排序。

原型

template <class RandomIt>
void sort(RandomIt first, RandomIt last);

template <class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);

示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {5, 3, 8, 1, 4};
    std::sort(vec.begin(), vec.end());  // 默认升序排序

    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    std::sort(vec.begin(), vec.end(), std::greater<int>());  // 降序排序

    for (int n : vec) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

5.2 std::find

功能

在范围内查找等于指定值的第一个元素。

原型

template <class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value);

示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {5, 3, 8, 1, 4};
    auto it = std::find(vec.begin(), vec.end(), 3);

    if (it != vec.end()) {
        std::cout << "Found: " << *it << std::endl;
    } else {
        std::cout << "Not found" << std::endl;
    }

    return 0;
}

5.3 std::find_if

功能

在范围内查找第一个满足指定条件的元素。

原型

template <class InputIt, class UnaryPredicate>
InputIt find_if(InputIt first, InputIt last, UnaryPredicate p);

示例

#include <iostream>
#include <vector>
#include <algorithm>

bool isOdd(int n) {
    return n % 2 != 0;
}

int main() {
    std::vector<int> vec = {5, 3, 8, 1, 4};
    auto it = std::find_if(vec.begin(), vec.end(), isOdd);

    if (it != vec.end()) {
        std::cout << "Found odd number: " << *it << std::endl;
    } else {
        std::cout << "No odd number found" << std::endl;
    }

    return 0;
}

5.4 std::binary_search

功能

检查范围内是否包含等于指定值的元素。注意,范围必须已排序。

原型

template <class ForwardIt, class T>
bool binary_search(ForwardIt first, ForwardIt last, const T& value);

template <class ForwardIt, class T, class Compare>
bool binary_search(ForwardIt first, ForwardIt last, const T& value, Compare comp);

示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 3, 4, 5, 8};
    bool found = std::binary_search(vec.begin(), vec.end(), 3);

    if (found) {
        std::cout << "Found" << std::endl;
    } else {
        std::cout << "Not found" << std::endl;
    }

    return 0;
}

5.5 std::for_each

功能

对范围内的每个元素执行指定操作。

原型

template <class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);

示例

#include <iostream>
#include <vector>
#include <algorithm>

void print(int n) {
    std::cout << n << " ";
}

int main() {
    std::vector<int> vec = {5, 3, 8, 1, 4};
    std::for_each(vec.begin(), vec.end(), print);
    std::cout << std::endl;

    return 0;
}

5.6 总结

算法功能使用示例
std::sort对范围内的元素进行排序std::sort(vec.begin(), vec.end());
std::find在范围内查找等于指定值的第一个元素std::find(vec.begin(), vec.end(), 3);
std::find_if在范围内查找第一个满足指定条件的元素std::find_if(vec.begin(), vec.end(), isOdd);
std::binary_search检查范围内是否包含等于指定值的元素(范围必须已排序)std::binary_search(vec.begin(), vec.end(), 3);
std::for_each对范围内的每个元素执行指定操作std::for_each(vec.begin(), vec.end(), print);

这些泛型算法极大地简化了容器操作,使代码更简洁、更可读,同时也提高了效率。

;