STL详解

STL详解

一、问题背景------解决什么问题

1.1 C++早期编程的困境

在STL出现之前,C++程序员面临着重重的编程挑战。想象一下,你需要实现一个动态数组,你可能需要手动管理内存分配与释放,考虑边界情况,处理各种异常。每当需要使用另一种数据结构,比如链表或者红黑树,你又需要从头开始编写一套全新的代码。这种低效的编程方式严重阻碍了C++的普及与发展。

具体来说,早期C++程序员面临以下几个核心问题:

代码重复问题:每实现一种数据结构,都需要重新编写内存管理、遍历、搜索等基础功能。比如你实现了一个int类型的动态数组,当需要存储double类型时,你发现大部分代码可以复用,但数据类型不同。于是你可能需要复制粘贴整个实现,然后逐个修改类型。这不仅容易出错,而且维护成本极高。

数据结构与算法耦合:传统的实现中,数据结构与算法紧密耦合在一起。例如,你实现了一个排序函数,它只能对特定的数组类型进行排序。如果你想对链表进行排序,就需要重新实现一个完全不同的排序算法。这导致了大量相似但又不完全相同的代码存在。

内存管理混乱:每个数据结构都自己管理内存,没有统一的标准。指针操作、内存泄漏、重复释放等问题层出不穷。一个复杂的程序可能有十几种不同的内存管理方式,这给调试和维护带来了极大的困难。

缺乏统一接口:不同的数据结构提供不同的操作接口。数组使用下标访问,链表使用指针遍历,树的访问方式又各不相同。这使得编写通用算法变得极为困难,你需要为每种数据结构单独编写算法。

1.2 泛型编程的需求

为了解决上述问题,泛型编程的概念应运而生。泛型编程的核心思想是:编写与类型无关的代码,使得同一段代码可以作用于不同的数据类型。

泛型编程追求的目标是:

  • 代码复用:一次编写,多处使用
  • 类型安全:在编译期进行类型检查
  • 性能优化:在编译期进行代码生成,避免运行时开销

STL正是泛型编程在C++中的完美实践。它提供了一套完整的、通用的、高效的标准库,让程序员能够专注于业务逻辑,而不是底层实现。

1.3 STL的诞生

STL(Standard Template Library,标准模板库)最初由HP的Alexander Stepanov和Meng Lee于1994年开发,后来被纳入C++标准库(STL成为C++标准库的一部分)。

STL的诞生标志着C++进入了一个新的时代。它不仅解决了代码复用的问题,更重要的是,它建立了一套完整的泛型编程范式,影响了后续众多编程语言和库的设计。


二、设计思路------设计师如何思考的

2.1 泛型编程思想

STL的设计核心是泛型编程。设计师思考的第一个问题是:如何让代码与类型解耦?

答案就是模板。通过模板,程序员可以编写通用的代码,编译器会根据具体的类型参数生成对应的代码。这就像是一个模具,可以生产出各种尺寸的产品。

cpp 复制代码
// 泛型函数:可以对任何支持比较的类型进行排序
template<typename T>
void sort(std::vector<T>& v) {
    // 排序算法实现
}

设计师的第二个思考是:如何让数据结构与算法解耦?

答案是迭代器。迭代器提供了一种统一的方式来遍历各种数据结构。算法只需要与迭代器交互,而不需要关心底层数据结构的实现细节。

cpp 复制代码
// 算法只与迭代器交互
template<typename Iterator>
void sort(Iterator first, Iterator last) {
    // 排序算法实现
}

2.2 STL六大组件

STL的设计采用了组件化的思想,整个库由六大核心组件构成:

容器(Containers):各种数据结构的实现,包括序列式容器(vector、list、deque)和关联式容器(map、set、multimap、multiset)。

算法(Algorithms):各种通用算法的实现,包括排序、搜索、复制、变换等。STL提供了超过100个算法函数。

迭代器(Iterators):遍历容器元素的对象,是算法与容器之间的桥梁。迭代器相当于智能指针,封装了对容器内部结构的访问。

空间配置器(Allocators):负责内存的分配与释放。空间配置器是STL最底层的基础设施,所有的容器都通过它来管理内存。

适配器(Adapters):对现有组件进行包装,提供不同的接口。包括容器适配器(stack、queue、priority_queue)和函数适配器(bind、mem_fn等)。

仿函数(Functors):可以像函数一样调用的对象。仿函数用于算法的自定义行为,比如自定义比较函数。

这六大组件之间的关系可以这样理解:容器 提供数据存储,迭代器 提供数据访问,算法 对数据进行操作,空间配置器 管理内存,适配器 改变接口,仿函数提供自定义行为。

2.3 迭代器模式

设计师思考的第三个问题是:如何统一不同容器的访问方式?

答案是迭代器模式 。迭代器模式的核心思想是:提供一种方法顺序访问集合中的元素,而不暴露集合的内部表示

迭代器相当于一个智能指针,它封装了对容器内部结构的访问。不同的容器提供不同类型的迭代器,但它们都有统一的接口:

cpp 复制代码
// 迭代器的基本操作
iterator++;        // 移动到下一个元素
*iterator;         // 获取当前元素
iterator1 == iterator2;  // 比较两个迭代器
iterator1 != iterator2;

通过迭代器,算法可以实现完全的通用性:

cpp 复制代码
// 这个函数可以对任何容器的任意区间进行排序
template<typename RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
    // 使用迭代器进行排序
}

2.4 空间配置器

设计师思考的第四个问题是:如何统一内存管理?

答案是空间配置器。空间配置器负责内存的分配与释放,所有的容器都通过它来管理内存。这使得内存管理变得可控,也便于优化。

STL的空间配置器采用两级配置策略:

  • 当请求的内存大于128字节时,使用一级配置器,直接调用malloc/free
  • 当请求的内存小于等于128字节时,使用二级配置器,使用内存池进行管理

这种设计的好处是:对于小块内存,避免了频繁调用malloc带来的开销,同时也减少了内存碎片。

2.5 算法与数据结构的分离

STL最重要的设计思想是算法与数据结构的彻底分离

在传统的编程中,数据结构与算法是紧密耦合的。比如你实现了一个排序算法,它与特定的数组结构绑定在一起。在STL中,算法完全独立于数据结构,它只与迭代器交互。

这种设计带来了极大的灵活性:

  • 同一个算法可以作用于不同的容器
  • 同一个容器可以使用不同的算法
  • 可以轻松地组合出复杂的功能
cpp 复制代码
// 排序算法可以作用于vector、deque、数组等
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(v.begin(), v.end());  // 对vector排序

std::deque<int> d = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(d.begin(), d.end());  // 对deque排序

int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(arr, arr + 8);        // 对数组排序

三、具体实现------从原理到代码实现

3.1 容器实现原理

3.1.1 vector的实现

vector是最常用的序列容器,它提供了动态数组的功能。

底层结构:vector的底层是一个连续的内存空间,类似于原生数组,但可以动态增长。当当前容量不足时,vector会分配更大的内存空间(通常是当前容量的1.5倍或2倍),然后将原有元素复制到新空间,并释放旧空间。

内存布局

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     分配的内存空间                          │
├─────────────┬─────────────────────────────────┬────────────┤
│   start_    │         已存储的元素            │ end_of_    │
│   (起始指针)│   [0] [1] [2] ... [size-1]      │ storage_   │
├─────────────┴─────────────────────────────────┴────────────┤
│              ↑ finish_ (指向最后一个元素的下一个位置)       │
└─────────────────────────────────────────────────────────────┘

核心实现

cpp 复制代码
template<typename T, typename Alloc = std::allocator<T>>
class vector {
private:
    T* start_;           // 指向起始位置
    T* finish_;          // 指向当前最后一个元素的下一个位置
    T* end_of_storage_;  // 指向存储空间的末尾
    Alloc alloc_;        // 空间配置器

public:
    // 迭代器类型
    typedef T* iterator;
    typedef const T* const_iterator;
    typedef std::reverse_iterator<iterator> reverse_iterator;
    typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

    // 大小与容量
    size_t size() const { return finish_ - start_; }
    size_t capacity() const { return end_of_storage_ - start_; }
    bool empty() const { return start_ == finish_; }

    // 访问元素
    T& operator[](size_t n) { return *(start_ + n); }
    const T& operator[](size_t n) const { return *(start_ + n); }

    // 带边界检查的访问
    T& at(size_t n) {
        if (n >= size()) throw std::out_of_range("vector::at");
        return *(start_ + n);
    }

    // 首尾元素访问
    T& front() { return *start_; }
    T& back() { return *(finish_ - 1); }

    // 添加元素
    void push_back(const T& value) {
        if (finish_ == end_of_storage_) {
            // 容量不足,需要扩容
            size_t old_capacity = capacity();
            size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
            reserve(new_capacity);
        }
        // 在末尾构造元素(使用placement new)
        alloc_.construct(finish_, value);
        ++finish_;
    }

    // 移动版本的push_back,避免拷贝
    void push_back(T&& value) {
        if (finish_ == end_of_storage_) {
            size_t old_capacity = capacity();
            size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
            reserve(new_capacity);
        }
        alloc_.construct(finish_, std::move(value));
        ++finish_;
    }

    // emplace_back:直接在末尾构造元素
    template<typename... Args>
    void emplace_back(Args&&... args) {
        if (finish_ == end_of_storage_) {
            size_t old_capacity = capacity();
            size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
            reserve(new_capacity);
        }
        alloc_.construct(finish_, std::forward<Args>(args)...);
        ++finish_;
    }

    // 扩容函数
    void reserve(size_t new_cap) {
        if (new_cap > capacity()) {
            size_t old_size = size();
            // 分配新内存
            T* new_start = alloc_.allocate(new_cap);

            // 将旧元素移动到新空间(使用uninitialized_move)
            T* new_finish = std::uninitialized_move(start_, finish_, new_start);

            // 销毁旧元素并释放旧内存
            destroy(start_, finish_);
            alloc_.deallocate(start_, capacity());

            // 更新指针
            start_ = new_start;
            finish_ = new_finish;
            end_of_storage_ = start_ + new_cap;
        }
    }

    // 销毁元素
    void destroy(T* first, T* last) {
        for (; first != last; ++first) {
            alloc_.destroy(first);
        }
    }

    // 删除最后一个元素
    void pop_back() {
        --finish_;
        alloc_.destroy(finish_);
    }

    // 清空容器
    void clear() {
        destroy(start_, finish_);
        finish_ = start_;
    }

    // 迭代器
    iterator begin() { return start_; }
    iterator end() { return finish_; }
    const_iterator begin() const { return start_; }
    const_iterator end() const { return finish_; }
};

迭代器类型:vector的迭代器就是原生指针,因为vector的内存是连续的。

cpp 复制代码
typedef T* iterator;
typedef const T* const_iterator;

这意味着vector的迭代器支持随机访问,可以在常数时间内跳转到任意位置。

扩容策略详解

vector的扩容策略是影响性能的关键因素。常见的扩容倍数有1.5倍和2倍两种:

  • 2倍扩容:简单直观,但可能导致较多的内存浪费
  • 1.5倍扩容:更节省内存,但实现稍复杂(需要处理内存对齐)

GCC使用的是2倍扩容策略,MSVC也使用2倍,而某些实现使用1.5倍。

cpp 复制代码
// 扩容时的内存分配过程
void expand() {
    size_t old_cap = capacity();
    size_t new_cap = old_cap == 0 ? 1 : old_cap * 2;  // 2倍扩容

    // 1. 分配新内存
    T* new_data = alloc_.allocate(new_cap);

    // 2. 移动元素(C++11使用move,效率更高)
    for (size_t i = 0; i < size(); ++i) {
        new (new_data + i) T(std::move(old_data[i]));
        old_data[i].~T();  // 销毁旧对象
    }

    // 3. 释放旧内存
    alloc_.deallocate(old_data, old_cap);

    // 4. 更新指针
    data_ = new_data;
    capacity_ = new_cap;
}

emplace_back vs push_back

cpp 复制代码
// push_back:先构造对象,然后拷贝/移动到容器中
std::vector<std::string> v;
std::string s = "hello";
v.push_back(s);           // 拷贝
v.push_back(std::string("world"));  // 移动

// emplace_back:直接在容器内存中构造对象
v.emplace_back("hello");           // 直接构造
v.emplace_back(5, 'a');            // 相当于 string(5, 'a')
v.emplace_back(s);                 // 拷贝
v.emplace_back(std::move(s));      // 移动

emplace_back避免了临时对象的创建,对于复杂类型(如std::string、std::vector)可以显著提升性能。

3.1.2 list的实现

list是一个双向链表,提供了常数时间的插入和删除操作。

底层结构:list的每个节点都是独立的,通过指针连接在一起。每个节点包含数据域和两个指针域(指向前一个节点和后一个节点)。

内存布局

kotlin 复制代码
┌──────────────────────────────────────────────────────────────────┐
│                         list结构                                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│    ┌──────┐     ┌──────┐     ┌──────┐     ┌──────┐              │
│    │ node │◄───►│ node │◄───►│ node │◄───►│ node │              │
│    │(head)│     │ data │     │ data │     │ data │              │
│    └──────┘     └──────┘     └──────┘     └──────┘              │
│       ▲                                                            │
│       │                                                            │
│    ┌──────────────────────────────────────────┐                  │
│    │  哨兵节点(不存储数据,仅作为链表的边界)   │                  │
│    └──────────────────────────────────────────┘                  │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

节点结构

cpp 复制代码
template<typename T>
struct __list_node {
    __list_node* prev;  // 指向前一个节点
    __list_node* next;  // 指向下一个节点
    T data;             // 存储的数据
};

核心实现

cpp 复制代码
template<typename T, typename Alloc = std::allocator<T>>
class list {
private:
    typedef __list_node<T> list_node;
    list_node* node_;  // 哨兵节点(头节点)

    // 分配节点
    list_node* create_node(const T& value) {
        list_node* p = allocator_.allocate(1);
        allocator_.construct(p, value);
        return p;
    }

    // 销毁节点
    void destroy_node(list_node* p) {
        allocator_.destroy(p);
        allocator_.deallocate(p, 1);
    }

public:
    // 迭代器实现
    template<typename T>
    struct list_iterator {
        list_node* node;

        // 解引用操作
        T& operator*() { return node->data; }
        T* operator->() { return &(node->data); }

        // 前向遍历
        list_iterator& operator++() {
            node = node->next;
            return *this;
        }

        list_iterator operator++(int) {
            list_iterator tmp = *this;
            node = node->next;
            return tmp;
        }

        // 后向遍历
        list_iterator& operator--() {
            node = node->prev;
            return *this;
        }

        list_iterator operator--(int) {
            list_iterator tmp = *this;
            node = node->prev;
            return tmp;
        }

        // 比较操作
        bool operator==(const list_iterator& other) const {
            return node == other.node;
        }
        bool operator!=(const list_iterator& other) const {
            return node != other.node;
        }
    };

    // 构造函数:创建哨兵节点
    list() {
        node_ = create_node(T());  // 创建哨兵节点
        node_->next = node_;
        node_->prev = node_;
    }

    // 在指定位置插入节点
    iterator insert(iterator position, const T& value) {
        list_node* new_node = create_node(value);
        list_node* cur = position.node;
        list_node* prev = cur->prev;

        // 链接新节点
        new_node->next = cur;
        new_node->prev = prev;
        prev->next = new_node;
        cur->prev = new_node;

        return iterator(new_node);
    }

    // 删除指定位置的节点
    iterator erase(iterator position) {
        list_node* cur = position.node;
        list_node* prev = cur->prev;
        list_node* next = cur->next;

        prev->next = next;
        next->prev = prev;
        destroy_node(cur);

        return iterator(next);
    }

    // 在链表末尾添加元素
    void push_back(const T& value) {
        insert(end(), value);
    }

    // 在链表头部添加元素
    void push_front(const T& value) {
        insert(begin(), value);
    }

    // 删除最后一个元素
    void pop_back() {
        erase(--end());
    }

    // 删除第一个元素
    void pop_front() {
        erase(begin());
    }

    // 链表排序(归并排序)
    void sort() {
        if (node_->next == node_->next->next) return;  // 0或1个元素

        list<T> carry;
        list<T> counter[64];
        int fill = 0;

        while (!empty()) {
            carry.splice(carry.begin(), *this, begin());
            int i = 0;
            while (i < fill && !counter[i].empty()) {
                counter[i].merge(carry);
                carry.swap(counter[i++]);
            }
            carry.swap(counter[i]);
            if (i == fill) ++fill;
        }

        for (int i = 1; i < fill; ++i) {
            counter[i].merge(counter[i-1]);
        }
        swap(counter[fill-1]);
    }

    // 归并操作(用于排序)
    void merge(list& other) {
        iterator first1 = begin(), last1 = end();
        iterator first2 = other.begin(), last2 = other.end();

        while (first1 != last1 && first2 != last2) {
            if (*first2 < *first1) {
                iterator next = first2;
                transfer(first1, first2, ++next);
                first2 = next;
            } else {
                ++first1;
            }
        }
        if (first2 != last2) {
            transfer(last1, first2, last2);
        }
    }

    // 将[first, last)区间的内容移动到position之前
    void transfer(iterator position, iterator first, iterator last) {
        if (position == last) return;
        list_node* p = position.node;
        list_node* f = first.node;
        list_node* l = last.node->prev;

        // 断开原链表
        f->prev->next = last.node;
        first.node->prev = last.node->prev;

        // 链接到新位置
        l->next = p;
        f->prev = p->prev;
        p->prev->next = f;
        p->prev = l;
    }
};

迭代器类型:list的迭代器是双向迭代器,不支持随机访问。++和--操作是常数时间的,但+和-操作不支持。

特点

  • 插入和删除操作是常数时间
  • 不支持随机访问,遍历效率较低
  • 不会导致迭代器失效(除了被删除的节点)
  • 占用额外的内存存储指针(每个节点需要额外的prev和next指针)

list vs vector

特性 list vector
内存布局 分散的节点,通过指针连接 连续的内存块
随机访问 O(n) O(1)
头部插入/删除 O(1) O(n)
尾部插入/删除 O(1) O(1) amortized
中间插入/删除 O(1) O(n)
缓存命中率
内存开销 每节点额外16字节(64位) 无额外开销
3.1.3 deque的实现

deque(双端队列)是一个可以在两端进行插入和删除的序列容器。

底层结构:deque采用多段连续的内存块来存储数据,每段内存块称为一个"缓冲区"。所有缓冲区连在一起,形成一个逻辑上的连续空间。

内存布局

arduino 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                           deque结构                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   map(指针数组):                                                   │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐                 │
│   │ *   │ *   │ *   │ *   │ *   │ *   │ *   │ *   │                 │
│   └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘                 │
│      │     │     │     │     │     │     │                         │
│      ▼     ▼     ▼     ▼     ▼     ▼     ▼                         │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐                       │
│   │ buf │ buf │ buf │ buf │ buf │ buf │ buf │  (多个缓冲区)         │
│   └─────┴─────┴─────┴─────┴─────┴─────┴─────┘                       │
│                                                                      │
│   start_cur ─────────────────────────────────────► 第一个元素       │
│   finish_cur ───────────────────────────────────► 最后一个元素后    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

核心实现

cpp 复制代码
template<typename T, typename Alloc = std::allocator<T>>
class deque {
private:
    // 指向缓冲区指针数组的指针
    T** map_;
    size_t map_size_;      // map数组的大小
    size_t buffer_size_;   // 每个缓冲区的大小

    // 迭代器位置
    T* start_cur_;          // 指向第一个元素
    T* finish_cur_;         // 指向最后一个元素的下一个位置

    // 计算元素所在的缓冲区
    size_t map_index(T* p) const {
        size_t n = (p - *map_) / buffer_size_;
        return n;
    }

    // 重新分配map
    void reallocate_map(size_t new_size, bool add_at_front);

public:
    // 迭代器实现
    template<typename T>
    struct deque_iterator {
        T* cur;      // 当前元素
        T* first;   // 当前缓冲区的起始
        T* last;    // 当前缓冲区的结束
        T** node;   // 指向map中对应指针的指针

        // 跳转到下一个缓冲区
        void set_node(T** new_node) {
            node = new_node;
            first = *new_node;
            last = first + buffer_size();
        }

        // 前向移动
        deque_iterator& operator++() {
            ++cur;
            if (cur == last) {
                set_node(node + 1);
                cur = first;
            }
            return *this;
        }

        // 后向移动
        deque_iterator& operator--() {
            if (cur == first) {
                set_node(node - 1);
                cur = last;
            }
            --cur;
            return *this;
        }

        // 随机访问
        deque_iterator& operator+=(difference_type n) {
            difference_type offset = n + (cur - first);
            if (offset >= 0 && offset < buffer_size()) {
                cur += n;
            } else {
                // 需要跨越缓冲区
                difference_type node_offset = offset / buffer_size();
                set_node(node + node_offset);
                cur = first + (offset % buffer_size());
            }
            return *this;
        }

        difference_type operator-(const deque_iterator& other) const {
            return buffer_size() * (node - other.node - 1) +
                   (cur - first) + (other.last - other.cur);
        }
    };

    // 头部插入
    void push_front(const T& value) {
        if (start_cur_ == *map_) {
            // 第一个缓冲区已满,需要扩展
            reallocate_map(map_size_, true);
        }
        --start_cur_;
        new (start_cur_) T(value);
    }

    // 尾部插入
    void push_back(const T& value) {
        if (finish_cur_ == *map_ + map_size_ - 1) {
            // 最后一个缓冲区已满,需要扩展
            reallocate_map(map_size_, false);
        }
        new (finish_cur_) T(value);
        ++finish_cur_;
    }

    // 随机访问
    T& operator[](size_t n) {
        return *(start_cur_ + n);
    }

    // 迭代器
    iterator begin() { return start_cur_; }
    iterator end() { return finish_cur_; }
};

迭代器实现详解

deque的迭代器是最复杂的,因为它需要处理跨缓冲区的情况:

cpp 复制代码
template<typename T>
struct deque_iterator {
    T* cur;      // 当前元素指针
    T* first;   // 当前缓冲区的起始位置
    T* last;    // 当前缓冲区的结束位置(不包含)
    T** node;   // 指向map数组中对应元素的指针

    // 随机访问操作符+
    deque_iterator operator+(difference_type n) const {
        deque_iterator tmp = *this;
        return tmp += n;
    }

    // += 操作符的实现
    deque_iterator& operator+=(difference_type n) {
        // 计算相对于当前缓冲区起始位置的偏移
        difference_type offset = n + (cur - first);

        // 情况1:在当前缓冲区内
        if (offset >= 0 && offset < buffer_size()) {
            cur += n;
        }
        // 情况2:向前跨越缓冲区
        else if (offset < 0) {
            difference_type node_offset = (offset + 1) / buffer_size() - 1;
            set_node(node + node_offset);
            cur = last + (offset % buffer_size());
        }
        // 情况3:向后跨越缓冲区
        else {
            difference_type node_offset = offset / buffer_size();
            set_node(node + node_offset);
            cur = first + (offset % buffer_size());
        }
        return *this;
    }

    // 跳转到另一个缓冲区
    void set_node(T** new_node) {
        node = new_node;
        first = *new_node;
        last = first + buffer_size();
    }
};

特点

  • 两端插入删除都是常数时间
  • 支持随机访问(虽然比vector稍慢)
  • 迭代器失效问题比vector更复杂(插入可能只影响部分迭代器)
3.1.4 map和set的实现

map和set是关联容器,底层使用红黑树实现。

红黑树特性(五大性质):

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 每个叶子节点(NIL)是黑色
  4. 如果一个节点是红色,则它的两个子节点都是黑色
  5. 从任一节点到其每个叶子的路径上,黑色节点的数量相同

红黑树的优势

  • 最坏情况下的时间复杂度也是O(log n)
  • 插入、删除、查找的效率稳定
  • 自动保持平衡,不需要像AVL树那样频繁旋转

红黑树结构

scss 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         红黑树结构                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│                           30 (黑)                                   │
│                          /    \                                     │
│                      (红)     (红)                                  │
│                       20      40                                    │
│                      /  \    /  \                                   │
│                   (黑)  (黑)(黑) (黑)                               │
│                   15   25  35   50                                  │
│                                                                      │
│   黑色高度:从任一节点到其每个叶子节点的路径上,                     │
│            黑色节点的数量相同                                       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

节点结构

cpp 复制代码
enum Color { RED, BLACK };

template<typename T>
struct rb_tree_node {
    rb_tree_node* parent;   // 父节点
    rb_tree_node* left;     // 左子节点
    rb_tree_node* right;    // 右子节点
    Color color;            // 节点颜色
    T value;                // 存储的值(对于map是pair<Key, Value>)
};

核心实现

cpp 复制代码
template<typename Key, typename Value, typename KeyOfValue,
         typename Compare, typename Alloc = std::allocator<T>>
class rb_tree {
private:
    typedef rb_tree_node<Value>* node_ptr;
    typedef Value* value_ptr;

    node_ptr header_;      // 头节点(哨兵节点)
    size_t node_count_;    // 节点数量
    Compare key_compare_;  // 键比较函数

    // 获取节点颜色
    Color color(node_ptr node) {
        return node ? node->color : BLACK;
    }

    // 左旋
    void rotate_left(node_ptr x) {
        node_ptr y = x->right;
        x->right = y->left;
        if (y->left) y->left->parent = x;
        y->parent = x->parent;

        if (x->parent == header_) {
            header_->parent = y;
        } else if (x == x->parent->left) {
            x->parent->left = y;
        } else {
            x->parent->right = y;
        }

        y->left = x;
        x->parent = y;
    }

    // 右旋
    void rotate_right(node_ptr x) {
        node_ptr y = x->left;
        x->left = y->right;
        if (y->right) y->right->parent = x;
        y->parent = x->parent;

        if (x->parent == header_) {
            header_->parent = y;
        } else if (x == x->parent->right) {
            x->parent->right = y;
        } else {
            x->parent->left = y;
        }

        y->right = x;
        x->parent = y;
    }

    // 插入后平衡操作
    void insert_fixup(node_ptr x) {
        while (x->parent->color == RED) {
            if (x->parent == x->parent->parent->left) {
                node_ptr uncle = x->parent->parent->right;
                // 情况1:叔叔节点是红色
                if (uncle && uncle->color == RED) {
                    x->parent->color = BLACK;
                    uncle->color = BLACK;
                    x->parent->parent->color = RED;
                    x = x->parent->parent;
                } else {
                    // 情况2:叔叔节点是黑色,当前节点是右子节点
                    if (x == x->parent->right) {
                        x = x->parent;
                        rotate_left(x);
                    }
                    // 情况3:叔叔节点是黑色,当前节点是左子节点
                    x->parent->color = BLACK;
                    x->parent->parent->color = RED;
                    rotate_right(x->parent->parent);
                }
            } else {
                // 对称情况
                node_ptr uncle = x->parent->parent->left;
                if (uncle && uncle->color == RED) {
                    x->parent->color = BLACK;
                    uncle->color = BLACK;
                    x->parent->parent->color = RED;
                    x = x->parent->parent;
                } else {
                    if (x == x->parent->left) {
                        x = x->parent;
                        rotate_right(x);
                    }
                    x->parent->color = BLACK;
                    x->parent->parent->color = RED;
                    rotate_left(x->parent->parent);
                }
            }
        }
        header_->parent->color = BLACK;  // 确保根节点是黑色
    }

    // 删除后平衡操作
    void erase_fixup(node_ptr x, node_ptr parent) {
        while (x != header_->parent && x->color == BLACK) {
            if (x == parent->left) {
                node_ptr sibling = parent->right;
                // 情况1:兄弟节点是红色
                if (sibling->color == RED) {
                    sibling->color = BLACK;
                    parent->color = RED;
                    rotate_left(parent);
                    sibling = parent->right;
                }
                // 情况2:兄弟节点是黑色,两个侄子都是黑色
                if ((!sibling->left || sibling->left->color == BLACK) &&
                    (!sibling->right || sibling->right->color == BLACK)) {
                    sibling->color = RED;
                    x = parent;
                    parent = x->parent;
                } else {
                    // 情况3:兄弟节点是黑色,右侄子黑色,左侄子红色
                    if (!sibling->right || sibling->right->color == BLACK) {
                        sibling->left->color = BLACK;
                        sibling->color = RED;
                        rotate_right(sibling);
                        sibling = parent->right;
                    }
                    // 情况4:兄弟节点是黑色,右侄子红色
                    sibling->color = parent->color;
                    parent->color = BLACK;
                    sibling->right->color = BLACK;
                    rotate_left(parent);
                    x = header_->parent;
                }
            } else {
                // 对称情况
                node_ptr sibling = parent->left;
                if (sibling->color == RED) {
                    sibling->color = BLACK;
                    parent->color = RED;
                    rotate_right(parent);
                    sibling = parent->left;
                }
                if ((!sibling->right || sibling->right->color == BLACK) &&
                    (!sibling->left || sibling->left->color == BLACK)) {
                    sibling->color = RED;
                    x = parent;
                    parent = x->parent;
                } else {
                    if (!sibling->left || sibling->left->color == BLACK) {
                        sibling->right->color = BLACK;
                        sibling->color = RED;
                        rotate_left(sibling);
                        sibling = parent->left;
                    }
                    sibling->color = parent->color;
                    parent->color = BLACK;
                    sibling->left->color = BLACK;
                    rotate_right(parent);
                    x = header_->parent;
                }
            }
        }
        x->color = BLACK;
    }

public:
    // 插入操作
    std::pair<iterator, bool> insert(const Value& value) {
        node_ptr y = header_;
        node_ptr x = header_->parent;

        while (x != nullptr) {
            y = x;
            if (key_compare_(KeyOfValue()(value), KeyOfValue()(x->value))) {
                x = x->left;
            } else if (key_compare_(KeyOfValue()(x->value), KeyOfValue()(value))) {
                x = x->right;
            } else {
                // 键已存在
                return std::pair<iterator, bool>(iterator(x), false);
            }
        }

        // 创建新节点
        node_ptr z = create_node(value);
        z->left = nullptr;
        z->right = nullptr;
        z->color = RED;

        // 插入节点
        if (y == header_ || key_compare_(KeyOfValue()(value), KeyOfValue()(y->value))) {
            y->left = z;
        } else {
            y->right = z;
        }
        z->parent = y;

        // 平衡操作
        insert_fixup(z);

        ++node_count_;
        return std::pair<iterator, bool>(iterator(z), true);
    }

    // 查找操作
    iterator find(const Key& key) {
        node_ptr x = header_->parent;
        while (x != nullptr) {
            if (key_compare_(key, KeyOfValue()(x->value))) {
                x = x->left;
            } else if (key_compare_(KeyOfValue()(x->value), key)) {
                x = x->right;
            } else {
                return iterator(x);
            }
        }
        return end();
    }

    // 删除操作
    void erase(iterator position) {
        node_ptr z = position.node;
        node_ptr y = z;
        node_ptr x;
        node_ptr parent;

        Color original_color = y->color;

        if (z->left == nullptr) {
            x = z->right;
            parent = z->parent;
            transplant(z, z->right);
        } else if (z->right == nullptr) {
            x = z->left;
            parent = z->parent;
            transplant(z, z->left);
        } else {
            y = minimum(z->right);
            original_color = y->color;
            x = y->right;
            if (y->parent == z) {
                parent = y;
            } else {
                transplant(y, y->right);
                y->right = z->right;
                y->right->parent = y;
                parent = y->parent;
                transplant(z, y);
                y->left = z->left;
                y->left->parent = y;
                y->color = z->color;
            }
        }

        if (original_color == BLACK) {
            erase_fixup(x, parent);
        }

        destroy_node(z);
        --node_count_;
    }

    // 替换节点(用于删除)
    void transplant(node_ptr u, node_ptr v) {
        if (u->parent == header_) {
            header_->parent = v;
        } else if (u == u->parent->left) {
            u->parent->left = v;
        } else {
            u->parent->right = v;
        }
        if (v) v->parent = u->parent;
    }

    // 找到最小节点
    node_ptr minimum(node_ptr node) {
        while (node->left != nullptr) {
            node = node->left;
        }
        return node;
    }
};

特点

  • 元素自动排序(根据key或value)
  • 查找、插入、删除都是对数时间复杂度
  • 迭代器是有序的(中序遍历)
  • map支持键值对,set只存储键
3.1.5 unordered_map和unordered_set的实现

unordered_map和unordered_set是哈希表实现的关联容器。

哈希表基本原理

哈希表通过哈希函数将键映射到数组的索引位置,实现常数时间复杂度的查找。

bash 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         哈希表结构                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   哈希函数: hash("apple") = 3  →  映射到索引3                        │
│                                                                      │
│   桶数组:                                                            │
│   ┌───┬───┬───┬───┬───┬───┬───┬───┐                                 │
│   │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │                                 │
│   ├───┼───┼───┼───┼───┼───┼───┼───┤                                 │
│   │   │   │   │apple│   │   │   │   │  ← 链地址法处理冲突            │
│   │   │   │   │  ↓  │   │   │   │   │                                 │
│   │   │   │   │cat  │   │   │   │   │                                 │
│   └───┴───┴───┴─────┴───┴───┴───┴───┘                                 │
│                                                                      │
│   负载因子 = 元素数量 / 桶数量                                        │
│   当负载因子超过阈值(通常为1.0)时,进行重哈希                       │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

核心实现

cpp 复制代码
template<typename Key, typename Value, typename Hash = std::hash<Key>,
         typename KeyEqual = std::equal_to<Key>,
         typename Alloc = std::allocator<std::pair<const Key, Value>>>
class unordered_map {
private:
    // 桶:存储链表或红黑树
    std::vector<std::list<std::pair<const Key, Value>>> buckets_;
    size_t bucket_count_;     // 桶的数量
    size_t size_;             // 元素数量
    float max_load_factor_;   // 最大负载因子

    Hash hash_function_;
    KeyEqual key_equal_;

    // 哈希函数
    size_t bucket_index(const Key& key) const {
        return hash_function_(key) % bucket_count_;
    }

    // 重哈希
    void rehash(size_t new_bucket_count) {
        unordered_map new_map(new_bucket_count);
        for (auto& bucket : buckets_) {
            for (auto& kv : bucket) {
                new_map.insert(kv);
            }
        }
        swap(new_map);
    }

public:
    // 构造函数
    unordered_map(size_t bucket_count = 16,
                  float max_load = 1.0f,
                  const Hash& hash = Hash(),
                  const KeyEqual& equal = KeyEqual())
        : bucket_count_(bucket_count),
          size_(0),
          max_load_factor_(max_load),
          hash_function_(hash),
          key_equal_(equal) {
        buckets_.resize(bucket_count_);
    }

    // 查找
    iterator find(const Key& key) {
        size_t n = bucket_index(key);
        for (auto& kv : buckets_[n]) {
            if (key_equal_(kv.first, key)) {
                return iterator(&kv, &buckets_[n]);
            }
        }
        return end();
    }

    // 插入
    std::pair<iterator, bool> insert(const std::pair<Key, Value>& value) {
        // 检查是否需要重哈希
        if (size_ > bucket_count_ * max_load_factor_) {
            rehash(bucket_count_ * 2);
        }

        size_t n = bucket_index(value.first);

        // 检查键是否已存在
        for (auto& kv : buckets_[n]) {
            if (key_equal_(kv.first, value.first)) {
                return std::pair<iterator, bool>(iterator(&kv, &buckets_[n]), false);
            }
        }

        // 插入新元素
        buckets_[n].push_back(value);
        ++size_;

        return std::pair<iterator, bool>(
            iterator(&buckets_[n].back(), &buckets_[n]), true
        );
    }

    // emplace
    template<typename... Args>
    std::pair<iterator, bool> emplace(Args&&... args) {
        // 先构造对象
        std::pair<Key, Value> value(std::forward<Args>(args)...);
        return insert(value);
    }

    // 删除
    size_t erase(const Key& key) {
        size_t n = bucket_index(key);
        for (auto it = buckets_[n].begin(); it != buckets_[n].end(); ++it) {
            if (key_equal_(it->first, key)) {
                buckets_[n].erase(it);
                --size_;
                return 1;
            }
        }
        return 0;
    }

    // 访问元素
    Value& operator[](const Key& key) {
        auto it = find(key);
        if (it == end()) {
            auto result = insert(std::make_pair(key, Value()));
            return result.first->second;
        }
        return it->second;
    }

    // 桶数量
    size_t bucket_count() const { return bucket_count_; }
    size_t size() const { return size_; }
    float load_factor() const { return (float)size_ / bucket_count_; }
};

哈希冲突处理

STL中的unordered_map通常使用链地址法(separate chaining)处理冲突:

  1. 链表:简单实现,插入O(1),但查找需要遍历链表
  2. 红黑树:C++11以后的主流实现,查找效率更高(O(log n))

重哈希(Rehashing)

当负载因子超过最大负载因子时,需要扩展桶的数量并重新计算所有元素的位置:

cpp 复制代码
void rehash(size_t new_bucket_count) {
    // 1. 创建新的桶数组
    std::vector<std::list<std::pair<const Key, Value>>> new_buckets(new_bucket_count);

    // 2. 重新分配所有元素
    for (auto& bucket : buckets_) {
        for (auto& kv : bucket) {
            size_t new_index = hash_function_(kv.first) % new_bucket_count;
            new_buckets[new_index].push_back(std::move(kv));
        }
    }

    // 3. 替换旧桶
    buckets_.swap(new_buckets);
    bucket_count_ = new_bucket_count;
}

特点

  • 查找、插入、删除平均是常数时间
  • 元素无序(不保证迭代顺序)
  • 需要良好的哈希函数以避免冲突
  • 空间开销较大(需要额外的桶数组)

unordered_map vs map

特性 unordered_map map
底层结构 哈希表 红黑树
元素顺序 无序 有序
查找复杂度 O(1) 平均 O(log n)
插入复杂度 O(1) 平均 O(log n)
空间复杂度 O(n) O(n)
哈希函数开销
适用场景 高性能查找 需要有序遍历

3.2 迭代器底层机制

3.2.1 迭代器类型

STL定义了五种迭代器类型,根据它们支持的操作:

输入迭代器(Input Iterator):只能读取元素,只能递增。可以用于单遍扫描算法。

输出迭代器(Output Iterator):只能写入元素,只能递增。可以用于单遍输出算法。

前向迭代器(Forward Iterator):可以读写元素,只能递增。可以多次遍历同一个序列。

双向迭代器(Bidirectional Iterator):可以读写元素,可以递增和递减。可以向前和向后遍历。

随机访问迭代器(Random Access Iterator):可以读写元素,支持所有指针运算(+、-、[]、<、>等)。是功能最强大的迭代器。

各容器提供的迭代器类型

容器 迭代器类型
vector 随机访问迭代器
deque 随机访问迭代器
list 双向迭代器
map/set 双向迭代器
unordered_map/set 前向迭代器
3.2.2 迭代器特性

每个迭代器类型都有一组特性(traits),用于获取迭代器的相关信息:

cpp 复制代码
template<typename Iterator>
struct iterator_traits {
    typedef typename Iterator::iterator_category iterator_category;
    typedef typename Iterator::value_type value_type;
    typedef typename Iterator::difference_type difference_type;
    typedef typename Iterator::pointer pointer;
    typedef typename Iterator::reference reference;
};

这些特性使得算法可以根据迭代器的类型进行优化。例如,std::sort需要随机访问迭代器,因为它使用快速排序等需要随机访问的算法。

3.2.3 迭代器适配器

STL提供了多种迭代器适配器,用于改变迭代器的行为:

反向迭代器(reverse_iterator):将正向迭代器转换为反向遍历。

cpp 复制代码
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it << " ";  // 输出: 5 4 3 2 1
}

插入迭代器(insert_iterator):将迭代赋值操作转换为插入操作。

cpp 复制代码
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2;
std::copy(v1.begin(), v1.end(), std::back_inserter(v2));  // v2 = {1, 2, 3}

流迭代器(istream_iterator/ostream_iterator):将流对象包装为迭代器。

cpp 复制代码
// 从标准输入读取整数
std::istream_iterator<int> in(std::cin);
std::istream_iterator<int> end;
std::vector<int> v(in, end);

// 输出到标准输出
std::ostream_iterator<int> out(std::cout, " ");
std::copy(v.begin(), v.end(), out);

3.3 空间配置器实现

3.3.1 一级配置器

一级配置器直接使用malloc和free进行内存管理:

cpp 复制代码
template<int inst>
class malloc_alloc_template {
private:
    static void* oom_malloc(size_t);
    static void* oom_realloc(void*, size_t);

public:
    static void* allocate(size_t n) {
        void* result = malloc(n);
        if (result == nullptr)
            result = oom_malloc(n);
        return result;
    }

    static void deallocate(void* p, size_t) {
        free(p);
    }

    static void* reallocate(void* p, size_t old_sz, size_t new_sz) {
        void* result = realloc(p, new_sz);
        if (result == nullptr)
            result = oom_realloc(p, new_sz);
        return result;
    }
};
3.3.2 二级配置器

二级配置器使用内存池来管理小块内存,减少内存碎片:

cpp 复制代码
template<bool threads, int inst>
class default_alloc_template {
private:
    // 内存块大小对齐到8字节
    static size_t ROUND_UP(size_t bytes) {
        return (bytes + 7) & ~7;
    }

    // 自由链表数组,每个索引对应一种大小
    static void* free_list[16];
    static size_t FREELIST_INDEX(size_t bytes) {
        return (bytes + 7) / 8 - 1;
    }

    // 从内存池获取内存
    static void* refill(size_t n);

    // 从堆获取内存池
    static char* chunk_alloc(size_t size, int& nobjs);

public:
    static void* allocate(size_t n) {
        if (n > 128)
            return malloc_alloc::allocate(n);

        void** my_free_list = free_list + FREELIST_INDEX(n);
        void* result = *my_free_list;

        if (result == nullptr) {
            // 自由链表为空,从内存池获取
            return refill(ROUND_UP(n));
        }

        *my_free_list = (*(void**)result);
        return result;
    }

    static void deallocate(void* p, size_t n) {
        if (n > 128) {
            malloc_alloc::deallocate(p, n);
            return;
        }

        void** my_free_list = free_list + FREELIST_INDEX(n);
        *(void**)p = *my_free_list;
        *my_free_list = p;
    }
};

3.4 算法实现

3.4.1 std::sort的实现

std::sort是STL中最常用的算法之一,它的实现非常巧妙:

cpp 复制代码
template<typename RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
    if (first >= last) return;

    // 对于小区间,使用插入排序
    if (last - first < 16) {
        for (RandomAccessIterator i = first + 1; i < last; ++i) {
            auto key = *i;
            RandomAccessIterator j = i - 1;
            while (j >= first && *j > key) {
                *(j + 1) = *j;
                --j;
            }
            *(j + 1) = key;
        }
        return;
    }

    // 使用快速排序
    RandomAccessIterator pivot = partition(first, last);
    sort(first, pivot);
    sort(pivot + 1, last);
}

实际的std::sort实现更加复杂,通常使用introsort算法(快速排序+堆排序+插入排序的混合算法),以确保最坏情况下的时间复杂度也是O(n log n)。

3.4.2 std::find的实现

std::find是最简单的搜索算法之一:

cpp 复制代码
template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, const T& value) {
    while (first != last) {
        if (*first == value)
            return first;
        ++first;
    }
    return last;
}

这个实现是线性的,时间复杂度是O(n)。

3.4.3 std::lower_bound和std::upper_bound

这两个算法用于在有序区间中查找:

cpp 复制代码
// 查找第一个不小于value的位置
template<typename ForwardIterator, typename T>
ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last,
                            const T& value) {
    while (first != last) {
        auto mid = first;
        std::advance(mid, std::distance(first, last) / 2);
        if (*mid < value) {
            first = ++mid;
        } else {
            last = mid;
        }
    }
    return first;
}

// 查找第一个大于value的位置
template<typename ForwardIterator, typename T>
ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last,
                            const T& value) {
    while (first != last) {
        auto mid = first;
        std::advance(mid, std::distance(first, last) / 2);
        if (*mid <= value) {
            first = ++mid;
        } else {
            last = mid;
        }
    }
    return first;
}

这两个算法使用二分查找,时间复杂度是O(log n)。


四、常见陷阱与面试题

4.1 迭代器失效问题

迭代器失效是STL使用中最常见的问题之一。当容器发生修改操作时,迭代器可能会变得无效,继续使用会导致未定义行为。

4.1.1 vector的迭代器失效
cpp 复制代码
std::vector<int> v = {1, 2, 3, 4, 5};

// 危险:push_back可能导致迭代器失效
auto it = v.begin();
v.push_back(6);  // 可能导致内存重新分配,it失效

// 正确做法:每次push_back后重新获取迭代器
for (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 == 0) {
        it = v.erase(it);  // erase返回下一个有效的迭代器
    } else {
        ++it;
    }
}

vector迭代器失效的情况

  • 插入元素导致重新分配:所有迭代器失效
  • 插入元素未重新分配:插入点之后的迭代器失效
  • 删除元素:删除点之后的迭代器失效
4.1.2 list的迭代器失效

list的迭代器失效问题相对简单:

cpp 复制代码
std::list<int> lst = {1, 2, 3, 4, 5};

auto it = lst.begin();
lst.erase(it);  // 只有被删除的节点迭代器失效,其他迭代器仍然有效
++it;           // 正确:it现在指向第二个元素

list迭代器失效的情况

  • 删除元素:只有被删除的节点迭代器失效
  • 插入元素:不会导致任何迭代器失效
4.1.3 map/set的迭代器失效
cpp 复制代码
std::map<int, std::string> m = {{1, "a"}, {2, "b"}, {3, "c"}};

auto it = m.begin();
m.erase(it);  // 只有被删除的节点迭代器失效

// 注意:不能在使用迭代器遍历时插入相同键的元素
// 这会导致迭代器失效(因为树会重新平衡)
4.1.4 面试题:迭代器失效的场景

面试题1:下面代码有什么问题?

cpp 复制代码
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
        v.erase(it);  // 错误:erase后it失效
    }
}

答案:erase后it失效,++it的行为未定义。正确做法是使用erase的返回值更新迭代器。

面试题2:在遍历vector时删除偶数,如何实现?

cpp 复制代码
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 方法1:使用erase返回值
for (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 == 0) {
        it = v.erase(it);  // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

// 方法2:使用erase-remove惯用法
v.erase(std::remove_if(v.begin(), v.end(), 
                       [](int x) { return x % 2 == 0; }), 
        v.end());

4.2 容器选择问题

选择合适的容器是提高程序性能的关键。

4.2.1 各容器的特点对比
容器 随机访问 插入/删除(头) 插入/删除(尾) 插入/删除(中间) 有序
vector O(1) - O(1) amortized O(n)
list - O(1) O(1) O(1)
deque O(1) O(1) O(1) amortized O(n)
map - - - O(log n)
unordered_map - - - O(1) avg
4.2.2 面试题:如何选择容器?

面试题:根据以下场景,应该选择哪种容器?

  1. 存储10万个整数,需要频繁在末尾添加和删除
  2. 存储10万个整数,需要频繁在中间插入和删除
  3. 存储10万个整数,需要频繁查找
  4. 存储10万个字符串,需要按字典序遍历

答案

  1. vector:末尾操作是摊销常数时间,缓存命中率高
  2. list:中间插入删除是常数时间
  3. unordered_map:平均常数时间查找
  4. map或set:自动维护有序性

4.3 内存管理问题

4.3.1 vector的内存管理
cpp 复制代码
// 面试题:vector的capacity和size有什么区别?
std::vector<int> v;
v.reserve(100);  // capacity = 100, size = 0
v.resize(50);   // capacity = 100, size = 50

// 面试题:如何避免vector频繁扩容?
// 答案:使用reserve预分配足够的空间
std::vector<int> v;
v.reserve(1000);  // 预先分配1000个元素的空间
for (int i = 0; i < 1000; ++i) {
    v.push_back(i);  // 不会触发扩容
}
4.3.2 内存泄漏问题
cpp 复制代码
// 面试题:以下代码有什么问题?
void func() {
    std::vector<int*> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(new int(i));  // 内存泄漏
    }
    // 函数结束时,v被销毁,但指针指向的内存未被释放
}

// 正确做法:使用智能指针
void func() {
    std::vector<std::shared_ptr<int>> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(std::make_shared<int>(i));
    }
}

4.4 性能问题

4.4.1 迭代器类型选择
cpp 复制代码
// 面试题:以下代码的性能如何?
std::list<int> lst = {1, 2, 3, 4, 5};
// 错误:list的迭代器不支持随机访问
std::sort(lst.begin(), lst.end());

// 正确做法1:先复制到vector
std::vector<int> v(lst.begin(), lst.end());
std::sort(v.begin(), v.end());

// 正确做法2:使用list的成员函数
lst.sort();  // list自带的排序
4.4.2 算法复杂度选择
cpp 复制代码
// 面试题:以下代码有什么性能问题?
std::vector<int> v = {1, 2, 3, 4, 5};
// 错误:在vector中查找元素使用O(n)的find
auto it = std::find(v.begin(), v.end(), 100);

// 正确做法:对于有序数据使用二分查找
auto it = std::lower_bound(v.begin(), v.end(), 100);  // O(log n)

// 或者使用unordered_set进行常数时间查找
std::unordered_set<int> s(v.begin(), v.end());
auto it = s.find(100);  // O(1) avg

4.5 线程安全问题

STL容器本身不是线程安全的:

cpp 复制代码
// 面试题:以下代码有什么问题?
std::vector<int> v = {1, 2, 3};

// 线程1
std::thread t1([&]() {
    for (int i = 0; i < 1000; ++i) {
        v.push_back(i);  // 不安全
    }
});

// 线程2
std::thread t2([&]() {
    for (int i = 0; i < 1000; ++i) {
        v.push_back(i);  // 不安全
    }
});

解决方案

  • 使用互斥锁保护共享容器
  • 每个线程使用独立的容器,最后合并
  • 使用无锁数据结构(需要自行实现或使用第三方库)

4.6 常见面试题汇总

4.6.1 vector相关面试题

Q1:vector的扩容机制是什么? A:通常当size等于capacity时,vector会进行扩容。扩容比例通常是1.5倍或2倍(不同实现可能不同)。扩容时分配新内存,复制元素,释放旧内存。

Q2:vector的emplace_back和push_back有什么区别? A:push_back接受已构造的对象,会进行拷贝或移动操作。emplace_back接受构造参数,直接在容器末尾构造对象,避免了额外的拷贝或移动。

cpp 复制代码
std::vector<std::string> v;
v.push_back("hello");        // 创建一个临时string,然后拷贝/移动
v.emplace_back("hello");     // 直接在容器中构造string

Q3:为什么vector的迭代器是原生指针? A:因为vector的内存是连续的,可以通过指针算术运算实现随机访问。

4.6.2 map相关面试题

Q4:map和unordered_map的区别是什么? A:map基于红黑树实现,元素按键排序,查找、插入、删除都是O(log n)。unordered_map基于哈希表实现,元素无序,查找、插入、删除平均是O(1)。

Q5:map的operator[]和find有什么区别? A:operator[]如果键不存在,会插入一个默认构造的值,可能导致副作用。find如果键不存在,只返回end(),不会修改容器。

cpp 复制代码
std::map<int, std::string> m;
// 风险:如果键不存在,会插入一个空string
std::string s = m[100];  

// 安全:如果键不存在,不会插入
auto it = m.find(100);
if (it != m.end()) {
    std::string s = it->second;
}

Q6:为什么map的键需要支持严格弱序? A:map底层使用红黑树,需要通过比较函数对元素进行排序。严格弱序要求:非自反、非对称、传递性、等价性。

4.6.3 迭代器相关面试题

Q7:迭代器失效是什么意思? A:当容器发生修改操作(如插入、删除)时,某些迭代器可能变得无效,继续使用会导致未定义行为。

Q8:如何安全地遍历容器并删除元素? A:使用erase的返回值更新迭代器,或者使用erase-remove惯用法。

cpp 复制代码
// 方法1:使用返回值
for (auto it = v.begin(); it != v.end(); ) {
    if (should_delete(*it)) {
        it = v.erase(it);
    } else {
        ++it;
    }
}

// 方法2:erase-remove惯用法
v.erase(std::remove_if(v.begin(), v.end(), should_delete), v.end());
4.6.4 算法相关面试题

Q9:std::sort的时间复杂度是多少? A:平均O(n log n),最坏情况也是O(n log n)(使用introsort算法)。

Q10:stable_sort和sort的区别是什么? A:stable_sort保持相等元素的相对顺序,时间复杂度是O(n log n)但需要额外内存;sort不保证稳定性,通常更快。


总结

STL是C++标准库中最重要的一部分,它体现了泛型编程的核心思想。通过模板、迭代器、算法与数据结构的分离,STL提供了一套高效、灵活、可复用的工具。

理解STL的设计思路和实现原理,不仅能够帮助我们更好地使用它,还能提升我们的编程思维。掌握STL的常见陷阱和面试要点,则是C++工程师的必备技能。

在实际开发中,我们应该:

  • 根据场景选择合适的容器
  • 注意迭代器失效问题
  • 避免不必要的拷贝,使用emplace系列函数
  • 对于性能敏感的场景,注意容器特性
  • 多线程环境下注意线程安全问题

只有深入理解STL的原理,才能在工作中写出高效、正确的C++代码。

相关推荐
用户3210442819453 小时前
并发编程核心原理
面试
IT当时语_青山师__JAVA技术栈3 小时前
Java反射深度解析:运行时探查的艺术、代价与工程实践
java·后端·面试
卡次卡次13 小时前
14.1: 总结本章 Python 高性能并发:多线程+多进程核心知识点+实战指南(面试/开发双适配)
服务器·python·面试
辛苦才能4 小时前
数据结构-排序算法-堆排序(重点比赛面试经常考)
数据结构·面试·排序算法
ximu_polaris4 小时前
C++高频面试题汇总
c++·面试
中小企业实战军师刘孙亮4 小时前
中小实体如何逆势稳健发展?重塑经营逻辑是关键!佛山鼎策创局破局增长咨询
学习·面试·创业创新·制造·学习方法
人道领域4 小时前
【LeetCode刷题日记】二叉树层序遍历完全指南:从基础到LeetCode实战一篇搞定BFS模板,秒杀4道经典面试题
java·开发语言·数据结构·leetcode·面试·二叉树
QD_ANJING5 小时前
建议5月的Web前端开发都去飞书上准备面试...
前端·人工智能·面试·职场和发展·前端框架·状态模式·ai编程
研究点啥好呢5 小时前
面馆开业!客官,你的面(经)好了!
python·阿里云·docker·面试·reactjs·求职招聘·react