[{"categories":["业务开发算法"],"collections":null,"content":"通过分析C++ STL容器，理解基础数据结构。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:0:0","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#"},{"categories":["业务开发算法"],"collections":null,"content":"一、动态数组 vector 数组：相同类型、连续内存、顺序存储。因此能基于下标，实现对数组元素O(1)的随机访问：数组的开始地址 + index * 元素大小。 静态数组需要提前指定数组大小，如果实际使用超过了数组大小，需要手动实现申请一片更大内存，将原来数组的内容拷贝到新内存，并释放原本内存的操作。动态数组把这些细节封装起来，无需用户关注扩容的操作。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:1:0","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#一动态数组-vector"},{"categories":["业务开发算法"],"collections":null,"content":"vector定义和扩容实现 template \u0026lt;class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) \u0026gt; class vector : protected _Vector_base\u0026lt;_Tp, _Alloc\u0026gt; { ... protected: _Tp* _M_start; //表示目前使用空间的头 _Tp* _M_finish; //表示目前使用空间的尾 _Tp* _M_end_of_storage; //表示目前可用空间的尾 ... }; 存了3个指针，分别表示数组的开始、结束、可用空间的结束。据此可以计算出size和capacity。 当capacity不足时，会触发扩容，测试扩容： #include \u0026lt;vector\u0026gt; #include \u0026lt;iostream\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; v; for (int i = 0; i \u0026lt; 20; i++) { std::cout \u0026lt;\u0026lt; \u0026#34;size: \u0026#34; \u0026lt;\u0026lt; v.size() \u0026lt;\u0026lt; \u0026#34; capacity \u0026#34; \u0026lt;\u0026lt; v.capacity() \u0026lt;\u0026lt; std::endl; v.push_back(i); } return 0; } 输出： size: 0 capacity 0 size: 1 capacity 1 size: 2 capacity 2 size: 3 capacity 4 size: 4 capacity 4 size: 5 capacity 8 size: 6 capacity 8 size: 7 capacity 8 size: 8 capacity 8 size: 9 capacity 16 size: 10 capacity 16 size: 11 capacity 16 size: 12 capacity 16 size: 13 capacity 16 size: 14 capacity 16 size: 15 capacity 16 size: 16 capacity 16 size: 17 capacity 32 size: 18 capacity 32 size: 19 capacity 32 观察到当size \u0026gt; capacity时会触发扩容。并且capacity按2的指数倍增长。 vector扩容逻辑： void push_back(const _Tp\u0026amp; __x) {//在最尾端插入元素 if (_M_finish != _M_end_of_storage) {//若有可用的内存空间 construct(_M_finish, __x);//构造对象 ++_M_finish; } else//若没有可用的内存空间,调用以下函数，把x插入到指定位置 _M_insert_aux(end(), __x); } template \u0026lt;class _Tp, class _Alloc\u0026gt; void vector\u0026lt;_Tp, _Alloc\u0026gt;::_M_insert_aux(iterator __position, const _Tp\u0026amp; __x) { if (_M_finish != _M_end_of_storage) { construct(_M_finish, *(_M_finish - 1)); ++_M_finish; _Tp __x_copy = __x; copy_backward(__position, _M_finish - 2, _M_finish - 1); *__position = __x_copy; } else { const size_type __old_size = size(); const size_type __len = __old_size != 0 ? 2 * __old_size : 1; iterator __new_start = _M_allocate(__len); iterator __new_finish = __new_start; __STL_TRY { __new_finish = uninitialized_copy(_M_start, __position, __new_start); construct(__new_finish, __x); ++__new_finish; __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); } __STL_UNWIND((destroy(__new_start,__new_finish), _M_deallocate(__new_start,__len))); destroy(begin(), end()); _M_deallocate(_M_start, _M_end_of_storage - _M_start); _M_start = __new_start; _M_finish = __new_finish; _M_end_of_storage = __new_start + __len; } } 扩容在_M_insert_aux中实现： 确保当前size等于capacity 申请当前size两倍的内存空间 深拷贝 释放原有内存空间 更新vector中的指针 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:1:1","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#vector定义和扩容实现"},{"categories":["业务开发算法"],"collections":null,"content":"思考：为什么会设计成倍增？ 若每次扩容固定增加 k 个空间，连续插入 n 个元素的总拷贝次数为： k + 2k + 3k + ... + n ≈ n²/(2k) = O(n²)， 均摊到每次 push_back 是 O(n)，性能随规模急剧恶化。 容量按 1, 2, 4, 8, \u0026hellip;, n 增长，总拷贝次数： 1 + 2 + 4 + ... + n ≈ 2n = O(n)， 均摊到每次 push_back 是 O(1)。这就是「push_back 均摊 O(1)」的来源。 所以，vector扩容设计成倍增，是为了保证push_back的均摊复杂度为O(1)。不同的编译器扩容因子可能不同，gcc和clang默认是2，msvc默认是1.5。选2可以做到时间最优，但是为什么有的编译器选择1.5？ 1.5的最差空间利用率更高 考虑到内存复用的问题，假设扩容因子为2，已分配块大小依次是 1, 2, 4, 8, 16\u0026hellip;：1+2+4+\u0026hellip;+2^(k-1)=2^k-1\u0026lt;2^k，前面所有已释放内存加起来，永远不够装下下一次扩容的大小。如果使用对齐内存池，会导致数组扩容只能往堆的高地址不断延伸，对内存分配器不友好。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:1:2","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#思考为什么会设计成倍增"},{"categories":["业务开发算法"],"collections":null,"content":"思考：为什么pop不会触发缩容？ 扩容是必要性问题，而缩容不是必须的。有可能用户就是需要保持原本的容量，所以是否缩容的选择应该交给使用者。这是STL的设计哲学：零开销抽象，把策略留给用户。 对象池 / 复用：vector 被反复清空再填充，保留 capacity 反而避免了反复申请释放，性能更好。 容量稳定抖动：size 在某个区间波动，释放只会带来无用拷贝。 明确知道不再增长：用户可以主动调用 shrink_to_fit()。 同时C++标准层面的硬约束：pop_back、clear、erase 在标准里都被视为不应失败的操作。如果执行缩容，需要重新申请内存，可能会分配失败，就不符合规范（影响整个RAII析构链）。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:1:3","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#思考为什么pop不会触发缩容"},{"categories":["业务开发算法"],"collections":null,"content":"二、双向链表 list 链表：顺序结构，使用指针关联节点地址，无需连续存储，一般来说比数组的内存使用率更高（无需提前分配）。链表更适用于删除、插入、遍历操作频繁的场景，而不适用于随机访问索引频繁的场景，如内存池、LRU。 链表主要包括单链表、双向链表、循环链表。STL中的list则是循环双向链表: 实现双向迭代器，支持std::reverse、list::reverse()、list::sort()等算法的实现。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:2:0","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#二双向链表-list"},{"categories":["业务开发算法"],"collections":null,"content":"list定义和实现 链表节点的定义： template \u0026lt;class T\u0026gt; struct __list_node { __list_node\u0026lt;T\u0026gt;* next; // 前驱节点指针 __list_node\u0026lt;T\u0026gt;* prev; // 后继节点指针 T data; //存储数据 }; list定义： template\u0026lt;typename T\u0026gt; class list{ protected: typedef __list_node\u0026lt;T\u0026gt; list_node; // 显示定义list_node类型 typedef allocator\u0026lt;list_node\u0026gt; nodeAllocator; // 定义allocator类型 public: typedef T value_type; typedef T\u0026amp; reference; typedef value_type* pointer; typedef list_node* link_type; typedef const value_type* const_pointer; typedef size_t size_type; public: typedef __list_iterator\u0026lt;value_type\u0026gt; iterator; // 迭代器类型重写 private: link_type node; // dummy节点 // ...... } 每个list都有一个虚拟节点，用于标记整个循环链表的首位连接处： dummy节点的引入是为了统一end语义：end指向的都是无效值。因此遍历list写法： for (std::list\u0026lt;int\u0026gt;::iterator it=mylist.begin(); it != mylist.end(); ++it) { std::cout \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; *it; } insert和erase实现： // 在position之前插入节点 iterator insert(iterator position, const T\u0026amp; x) { lik_type tmp = create_node(x); // 创建一个临时节点 tmp-\u0026gt;next = position.node; // 将该节点的后继指针指向当前位置的节点 tmp-\u0026gt;prev = position.node-\u0026gt;prev; // 将该节点的前驱指针指向当前位置的前驱节点 (link_type(position.node-\u0026gt;prev))-\u0026gt;next = tmp; // 将前驱节点本来指向当前节点的后继指针改为指向该临时节点 position.node-\u0026gt;prev = tmp; // 同样，当前位置的前驱指针也要修改为指向该临时节点 return tmp; } // 删除position的节点 iterator erase(iterator position) { link_type next_node = link_type(position.node-\u0026gt;next); link_type prev_node = link_type(position.node-\u0026gt;prev); prev_node-\u0026gt;next = next_node; next_node-\u0026gt;prev = prev_node; destroy_node(position.node); return iterator(next_node); } 据此可以实现： push_back(x): insert(end(), x) push_front(x): insert(begin(), x) pop_back(): erase(--end()) pop_front(): erase(begin()) ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:2:1","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#list定义和实现"},{"categories":["业务开发算法"],"collections":null,"content":"注意事项 遍历list的缓存命中率极差，实际遍历速度比vector慢一个数量级。 list每个节点额外存了两个指针，16字节，不一定比vector利用率更高。 list比vector的优势在于可以快速进行头插入和头删除，其他场景都建议使用vector。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:2:2","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#注意事项"},{"categories":["业务开发算法"],"collections":null,"content":"三、双端队列 deque 队列的所有操作都发生在队列的两端。双端队列（double ended queue）在队列两端都可以进行插入和删除操作，更加灵活。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:3:0","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#三双端队列-deque"},{"categories":["业务开发算法"],"collections":null,"content":"deque定义和实现 deque使用了分级的思想，使用map管理多段连续内存（分段连续空间）： template \u0026lt;class _Tp, class _Alloc\u0026gt; class _Deque_base { ... protected: _Tp** _M_map; size_t _M_map_size; iterator _M_start; iterator _M_finish; ... } deque的迭代器屏蔽了底层分块连续的细节，对外可以认为deque的内存空间是连续的。可以直接队deque的元素进行随机访问，时间复杂度是O(1)。 以push_back为例，看push操作的实现： 如果当前node下面还有空间，直接使用下面的空间即可。 如果当前node已经占满了，查看map。 如果map下面还有没有使用的node，使用这个node。 如果map下面没有剩余node，查看map使用量。 如果map使用率没有过一半，把当前map上已用区间放到中间位置，然后执行3。 如果map使用率超过一半，重新申请一个map，然后执行3。 pop操作则是在一个node空闲后，回收这段内存。所以在临界点反复push和pop每次都会触发node内存的申请和释放（对象池复用vector比deque更香）。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:3:1","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#deque定义和实现"},{"categories":["业务开发算法"],"collections":null,"content":"思考：为什么不使用vector或list？ vector在头部插入元素的时间复杂度是O(n)，不符合队列两端操作都要高效的需求。如果是大小确定的循环队列，用vector实现就比较合适。 list不支持随机访问，并且list每push一个元素就需要去申请内存，相比当前的实现一次性可以申请一段连续内存，list方案申请内存的频率更高，性能更差。 deque的随机访问也是O(1)，扩容也不需要深拷贝，实现看起来比vector更加全面。但是每次随机访问需要计算map位置和node中偏移，并且缓存命中率更低，实际性能不如vector。\u0026ldquo;只有在真的需要两端高效\u0026rdquo; 的场景才用 deque，否则 vector 永远是更快的选择。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:3:2","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#思考为什么不使用vector或list"},{"categories":["业务开发算法"],"collections":null,"content":"思考：node该开多大？ STL的策略：每个node 512字节，或者每个node固定16个元素。 // 每个 node 的元素数：若 sizeof(T) \u0026lt; 512，则为 512/sizeof(T)，否则为 1 inline size_t __deque_buf_size(size_t n, size_t sz) { return n != 0 ? n : (sz \u0026lt; 512 ? size_t(512 / sz) : size_t(1)); } 分析： node太小：map大，缓存命中率低 node太大：单node内存浪费严重（和vector的缺点类似） 512字节接近一个cpu cache line的大小，是性能拐点。 ","date":"2026-04-26","objectID":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/:3:3","tags":null,"title":"【算法】基础数据结构","uri":"/posts/%E4%B8%9A%E5%8A%A1%E5%BC%80%E5%8F%91%E7%AE%97%E6%B3%95/1.%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/#思考node该开多大"},{"categories":["hello world"],"collections":null,"content":"刚好是秋冬交替，红叶的最后窗口，搭同事的顺风车一起去毕棚沟散散心。 因为是临时计划没有提前准备，比较匆忙。但是还好订到了一间民宿，在这个旺季价格也比较良心。 周五傍晚出发，抵达理县；第二天一早就步行到景区。 先是坐大巴到了第一个点，这时候天灰蒙蒙的，云雾缭绕。 沿着湖畔走，一般是金黄的树林，一边是绿白相间的群山，甚是养眼。 后面几个点都是坐电瓶车，一弯又一弯。 到山顶差不多是正午的时候，那叫一个阳光明媚。 在山顶拍拍照，晒晒太阳，就走“原始深林”的小道下山了。小道晒不到太阳，路上结了很多冰，非常滑，得非常小心。 下山后有那么一点高反，和发烧差不多的感觉，吃完饭基本上就好了。 ","date":"2025-11-08","objectID":"/posts/helloworld/%E6%AF%95%E6%A3%9A%E6%B2%9F/:0:0","tags":null,"title":"毕棚沟之行","uri":"/posts/helloworld/%E6%AF%95%E6%A3%9A%E6%B2%9F/#"}]