Skip to content

Commit

Permalink
update:10 articles
Browse files Browse the repository at this point in the history
  • Loading branch information
alu234 committed Feb 9, 2024
1 parent 745d69d commit d33235b
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 0 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@
* [什么是完美转发?](./problems/什么是完美转发?.md)
* [C++中四种cast的转换?](./problems/C++中四种cast的转换?.md)
* [内存池是什么?在C++中如何设计一个简单的内存池?](./problems/内存池是什么?在C++中如何设计一个简单的内存池?.md)
* [STL中,map的底层是如何实现的?](./problems/STL中,map的底层是如何实现的?.md)
* [STL中,set的底层是如何实现的?](./problems/STL中,set的底层是如何实现的?.md)
* [set,mutiset,map,mutimap之间都有什么区别?](./problems/set,mutiset,map,mutimap之间都有什么区别?.md)
* [在C++的算法库中,find()和binary_search()有什么区别?](./problems/在C++的算法库中,find()和binary_search()有什么区别?.md)
* [lower_bound()和upper_bound()有什么区别?](./problems/lower_bound()和upper_bound()有什么区别?.md)
* [为什么需要allocator?他在STL中有什么作用?](./problems/为什么需要allocator?他在STL中有什么作用?.md)
* [什么是RAII原则,他在STL是怎么应用的?](./problems/什么是RAII原则,他在STL是怎么应用的?.md)
* [STL容器是线程安全的吗?](./problems/STL容器是线程安全的吗?.md)
* [什么是泛型编程,他在STL中是怎么使用的?](./problems/什么是泛型编程,他在STL中是怎么使用的?.md)
* [如何选择合适的STL容器](./problems/如何选择合适的STL容器.md)

# 数据结构与算法

Expand Down
15 changes: 15 additions & 0 deletions problems/STL中,map的底层是如何实现的?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
在C++标准模板库(STL)中,`map` 是一种关联容器,它以键值对的方式存储元素,其中每个键都是唯一的。底层实现通常使用红黑树(Red-Black Tree),这是一种自平衡的二叉搜索树。

红黑树保持了树的平衡性,即从根到所有叶子节点的最长路径不会超过最短路径的两倍。这种性质确保了`map`中的操作(如插入、删除和查找)可以在对数时间复杂度O(log n)内完成。

红黑树有以下特性:

1. 每个节点要么是红色,要么是黑色。
2. 根节点是黑色。
3. 所有叶子节点(NIL节点,空节点)都是黑色。
4. 每个红色节点必须有两个黑色的子节点(不能有两个连续的红色节点)。
5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

因为`map`的底层是红黑树这种高度平衡的数据结构,所以它能够提供良好的性能保证,使得即使在大量元素存储的情况下也能保持效率。

需要注意的是,在C++11后,还引入了`unordered_map`,它使用哈希表作为底层实现,提供平均常数时间复杂度O(1)的访问性能,但它不保证元素的顺序,并且在最坏情况下可能退化为线性时间复杂度O(n)。
5 changes: 5 additions & 0 deletions problems/STL中,set的底层是如何实现的?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
在C++标准模板库(STL)中,`set` 是基于关联容器的一个抽象数据类型,用于存储不重复的元素。与 `map` 类似,`set` 的底层实现也通常采用红黑树(一种自平衡的二叉搜索树)。这使得 `set` 中的大多数操作(例如插入、删除和搜索)都能以对数时间复杂度 O(log n) 来执行,其中 n 是集合中元素的数量。

使用红黑树作为底层数据结构,`set` 可以保证元素会按照特定的顺序排序,通常是按照键值的递增顺序。红黑树确保了任何时候树都是相对平衡的,所以 `set` 容器在处理大量动态插入和删除操作时依然能够提供良好的性能。

除了 `set`,STL 还提供了 `unordered_set` 容器,其底层实现是基于哈希表。`unordered_set` 不保证元素的有序性,但在理想情况下可以提供更快的平均时间复杂度 O(1) 的访问性能。不过,在最坏的情况下(例如,当哈希函数导致很多碰撞时),它的性能可能会退化到 O(n)。
9 changes: 9 additions & 0 deletions problems/STL容器是线程安全的吗?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
C++标准库(STL)中的容器本身不是线程安全的。这意味着在没有采取外部同步措施的情况下,如果有多个线程同时对同一个容器实例进行写操作,或者同时有一个线程在写操作和另一个线程在读操作,那么这可能会导致数据竞争和未定义行为。

因此,当多个线程需要访问相同的容器时,就需要通过其他方式来确保线程安全。常用的同步机制包括:

1. **互斥锁(Mutexes)**:使用互斥锁来同步对容器的访问。例如,可以在每次操作容器之前加锁,操作完毕后解锁。
2. **读写锁(Reader-Writer Locks)**:如果你的应用程序涉及到更多的读操作而较少的写操作,可以使用读写锁来允许多个读取者同时访问容器,而写入者则需要独占访问权限。
3. **原子操作**:对于简单的操作,如对单个元素的更新,可以考虑使用原子类型 `std::atomic`
4. **并发容器**:某些场景下可以使用专为并发设计的容器,如 `boost` 库提供的一些线程安全版本的容器,或者 `tbb::concurrent_vector` 等。
5. **细粒度锁或无锁编程技术**:在高级应用程序中,可能会使用更复杂的策略,比如分段锁或无锁数据结构,以减小锁的粒度或避免锁的开销。
13 changes: 13 additions & 0 deletions problems/lower_bound()和upper_bound()有什么区别?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
1. **lower_bound()**:
- 返回一个指向范围内第一个**不小于**(即大于或等于)给定值的元素的迭代器。
- 如果给定值不存在于容器中,该函数返回指向第一个大于该值的元素的迭代器。如果所有元素都小于给定值,函数将返回一个指向容器末尾(end)的迭代器。
2. **upper_bound()**:
- 返回一个指向范围内第一个**大于**给定值的元素的迭代器。
- 如果所有元素都小于或等于给定值,函数将返回一个指向容器末尾(end)的迭代器。

**举例说明**:

假设我们有一个包含 {1, 2, 4, 4, 5, 6, 8} 的整数vector,并且我们想要搜索数字4。

- 使用 `lower_bound()` 寻找4会返回指向第一个数字4的迭代器,因为4是数组中第一个"不小于"4的值。
- 使用 `upper_bound()` 寻找4会返回指向数字5的迭代器,这是因为5是数组中第一个"大于"4的值。
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
1. `set`:
- 存储唯一键值的集合,即不允许重复的元素。
- 元素本身就是键值。
- 元素按照特定顺序存储(默认为递增顺序)。
2. `multiset`:
- 类似于 `set`,但它允许重复的键值,即可以有多个相等的元素。
- 元素同样按照特定顺序存储。
3. `map`:
- 存储键值对(pair),每个键值对由一个键和一个值组成。
- 每个键在 `map` 中是唯一的,你不能有两个具有相同键的键值对。
- 键值对按照键的顺序存储。
4. `multimap`:
- 类似于 `map`,但它允许键不唯一,即可以有多个键值对拥有相同的键。
- 键值对同样按照键的顺序存储。

使用情况概述:

- 当你需要存储不重复元素的有序集合时,使用 `set`
- 当你需要存储可能重复元素的有序集合时,使用 `multiset`
- 当你需要维护一组键到值的映射,并且每个键只能关联一个值时,使用 `map`
- 当你需要维护一组键到值的映射,并且一个键可以关联多个值时,使用 `multimap`

所有这四种类型的底层实现通常是红黑树,除了提供插入、删除和搜索操作外,还保证了元素的有序性。然而,如果你不需要元素的排序,并且关注更高效的插入和查找性能,可以选择使用基于哈希表实现的 `unordered_set`, `unordered_multiset`, `unordered_map`, `unordered_multimap` 等无序容器。
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### 1. 抽象化内存管理

- `allocator` 提供了一个抽象层,使得容器能够专注于数据结构和算法的实现,而不必担心具体的内存分配和回收细节。这样,容器的设计和实现就可以独立于底层的内存管理机制。

### 2. 提供统一的接口

- 所有的 STL 容器都使用相同的 `allocator` 接口来分配和释放内存。这提供了一致性,并且使得开发者在需要时能够更容易地替换默认的内存分配策略。

### 3. 支持自定义内存管理策略

- 通过自定义 `allocator`,开发者可以根据应用程序的特定需求调整内存分配策略。例如,在特定场景下可能需要一个高性能的内存池分配器,或者跟踪内存使用情况的分配器,这些都可以通过自定义 `allocator` 来实现。

### 4. 性能优化

- 默认的内存分配器可能并不总是满足特定应用程序的性能需求。通过使用自定义的 `allocator`,开发者可以利用特定的内存分配技巧(如小对象优化、内存池分配等)来提升性能。

### 5. 内存对齐

- 对于某些特定类型的对象,可能需要特殊的内存对齐以达到最佳性能或满足硬件要求。`allocator` 允许对这些对象的内存分配进行适当的对齐处理。
19 changes: 19 additions & 0 deletions problems/什么是RAII原则,他在STL是怎么应用的?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### RAII原则的核心思想:

- 在构造函数中获取资源。
- 在析构函数中释放资源。
- 不直接操作资源,而是通过管理资源的对象来使用资源。

这样做有几个好处:

1. **安全性**:避免资源泄露。由于资源的释放是自动的,因此即使在异常发生时,对象的析构函数也会被调用,资源也相应地会被释放。
2. **简洁性**:代码通常更加简洁,因为资源的管理是自动的,不需要程序员显式编写资源释放代码。
3. **异常安全**:RAII可以帮助提供强异常安全保障,因为资源释放不依赖于程序路径。

### RAII在STL中的应用:

在STL中,RAII广泛应用于各种容器和其他组件中。例如:

- **智能指针**`std::unique_ptr``std::shared_ptr` 是智能指针类,它们对动态分配的内存进行管理。当智能指针的实例离开作用域时,其析构函数会自动释放其所管理的内存。
- **容器类**:如 `std::vector``std::string``std::map` 等,都负责自己内部数据的内存管理。当一个容器对象被销毁时,它的析构函数会释放所有占用的内存,并适当地销毁其元素。
- **锁管理类**:如 `std::lock_guard``std::unique_lock`,它们在构造时获取锁,在析构时释放锁,从而确保在持有锁的代码块执行完毕后,无论是正常退出还是因异常退出,锁都会被释放。
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
泛型编程是一种软件开发方法论,其核心思想是通过抽象和重用代码来增强软件的灵活性、可维护性和复用性。在泛型编程中,算法或数据结构被设计为独立于它们所操作的具体数据类型。这种抽象化允许程序员使用相同的代码基础处理不同类型的数据,只要这些数据类型支持算法所需的操作。

### STL中的泛型编程

C++的标准模板库(STL)是泛型编程的一个经典实例。STL提供了一套通用的容器类(如`vector``list``map`等),算法(如`sort``find``accumulate`等),以及其他实用工具(如迭代器、函数对象等),它们都是泛型化的,可以与任何符合要求的数据类型一起工作。

#### 容器

STL容器是泛型的,因为它们可以存储任何类型的对象。例如,`std::vector<int>` 可以存储整数,而 `std::vector<std::string>` 可以存储字符串。容器通过模板参数化其元素的类型:

```
std::vector<int> intVec; // 存储整数的向量
std::list<double> dblList; // 存储双精度浮点数的列表
```

#### 算法

STL算法也是泛型的,它们通过迭代器与容器进行交互,而不是直接操作容器。这种设计使得相同的算法可以应用于不同类型的容器,只要容器提供了适当类型的迭代器。例如,`std::sort`函数可以对任何连续存储的元素序列进行排序,无论它是`std::vector`、数组还是`std::array`

```
std::vector<int> vec = {4, 1, 3, 5, 2};
std::sort(vec.begin(), vec.end()); // 对向量进行排序
```

#### 迭代器

迭代器在STL的泛型编程中扮演着中介的角色。它们提供了一种访问容器中元素的方法,同时隐藏了容器的内部结构。通过使用迭代器,STL算法可以在不知道或不关心容器具体实现的情况下工作。

#### 函数对象和Lambda表达式

STL允许你通过函数对象(包括lambda表达式)来自定义某些操作,比如自定义比较函数。这增加了STL的灵活性和泛型能力,因为你可以定义算法的行为,而无需修改算法本身。

```
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; }); // 使用lambda表达式降序排序
```

总之,STL通过泛型编程提供了一套强大的、类型无关的工具,使得开发者能够写出既安全又高效的代码,而不用牺牲代码的通用性和复用性。
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
1. **算法复杂度和预期使用场景**:
- `find()` 函数执行线性查找。它逐个检查容器中的元素,直到找到等于指定值的元素或结束。因为它是通过遍历实现的,所以其时间复杂度为 O(n),其中 n 是容器中元素的数量。`find()` 不要求容器中的元素是事先排序的。
- `binary_search()` 函数执行二分查找。它要求容器中的元素已经按非降序排序,并且通过不断将搜索范围缩小一半来查找特定值。因此,其时间复杂度为 O(log n) 。由于这种查找方式依赖于容器的元素顺序,所以在未排序的容器上使用 `binary_search()` 会得到未定义的行为。
2. **返回值**:
- `find()` 返回一个迭代器,指向在容器中找到的第一个等于指定值的元素。如果没有找到,它返回一个等于 end() 迭代器的值。
- `binary_search()` 返回一个布尔值,如果找到指定值则返回 true,否则返回 false。注意,它并不返回目标元素的位置或迭代器。
3. **通用性**:
- `find()` 可以用于任何类型的容器,包括列表、向量、集合等,而且不需要元素是排序的。
- `binary_search()` 通常用于数组或向量等随机访问容器,并且前提是这些容器中的元素已经被排序。
45 changes: 45 additions & 0 deletions problems/如何选择合适的STL容器.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
选择合适的STL容器依赖于你的特定需求,包括你的数据结构、性能要求以及如何使用这些数据。下面是一些常用的STL容器和它们的特点,以及何时最适合使用它们:

1. **std::vector**:
- 动态数组,提供快速的随机访问(O(1)时间复杂度)。
- 适用于元素数量经常变化,但主要操作是在尾部添加或移除元素的场景。
- 不适合频繁在中间或头部插入/删除操作,因为这样的操作会导致后续所有元素移动。
2. **std::deque**:
- 双端队列,支持在头部和尾部高效的插入和删除操作。
- 当需要快速地在序列的两端进行插入或删除时,更优于`std::vector`
3. **std::list****std::forward_list**:
- 分别代表双向和单向链表。
- 提供在任意位置高效插入和删除操作(O(1)时间复杂度)。
- 不支持快速随机访问。
- 当数据结构需要频繁在中间位置插入和删除元素时,链表可能是一个好选择。
4. **std::set****std::multiset**:
- 基于红黑树实现的有序集合和多重集合。
- 自动对元素排序,并保证唯一性(`std::multiset` 允许重复元素)。
- 插入、查找和删除操作具有对数时间复杂度(O(log n))。
- 当需要保存有序的唯一元素集合,并且频繁查询是否存在某个元素时使用。
5. **std::map****std::multimap**:
- 基于红黑树的键-值对集合,自动按键排序。
- `std::multimap` 允许键不唯一。
- 适用于当需要根据键来存取元素,并且需要保持键的有序性时。
6. **std::unordered_set****std::unordered_map****std::unordered_multiset****std::unordered_multimap**:
- 基于哈希表实现的无序容器。
- 提供平均常数时间复杂度的插入、查找和删除操作,但最坏情况下会退化到线性时间。
- 当元素的顺序不重要,且期望快速访问时使用。
7. **std::stack****std::queue****std::priority_queue**:
- 封装了其他容器的适配器,分别提供了栈、队列和优先队列的接口。
- `std::stack``std::queue` 通常基于 `std::deque` 实现。
- `std::priority_queue` 通常基于 `std::vector` 实现,并通过使堆来管理元素的优先级。
- 适用于特定的数据结构需求,如LIFO(后进先出)、FIFO(先进先出)或优先级排序。
8. **std::array**:
- 固定大小的数组封装,提供了标准容器接口。
- 当数组大小已知且不变时使用,它提供了比原始数组更安全和易于使用的接口。

选择合适容器的一般建议是:

- 首选 `std::vector`,除非有特定理由选择其他容器。
- 如果需要高效的插入和删除,考虑 `std::list``std::deque`
- 如果需要保存唯一元素并保持顺序,使用 `std::set`
- 如果元素顺序不重要,但想要快速查找,使用 `std::unordered_set``std::unordered_map`
- 对于特殊用途的容器,如栈、队列或优先队列,选择相应的适配器。

模板参数、内存分配器选项和成员函数的选择也可以影响容器的行为和性能,所以在选择时还需要考虑这些因素。

0 comments on commit d33235b

Please sign in to comment.