C++ vector高效删除:从O(n)到O(1)的优化策略与实现
1. 项目概述为什么我们需要O(1)的vector删除在C的日常开发中std::vector是我们最亲密的伙伴之一。它提供了动态数组的便利支持随机访问缓存友好几乎是所有性能敏感场景下的首选容器。然而但凡用过vector的开发者几乎都踩过同一个坑从中间删除元素。标准的erase方法其时间复杂度是 O(n)。这意味着如果你有一个包含一百万个元素的vector删除第一个元素会导致后面999,999个元素全部向前移动一位。这个操作的成本在数据量大或删除操作频繁时是灾难性的。这个痛点催生了我们今天要深入探讨的主题如何实现std::vector的高效删除目标是将时间复杂度从 O(n) 降至 O(1)。这听起来像是一个“不可能的任务”因为vector的内存布局是连续的删除中间元素必然导致后续元素移位以填补空隙这是其数据结构特性决定的。但编程的魅力就在于我们可以通过改变思路来绕过限制。我们并非要魔改标准库的实现而是设计一种“逻辑删除”的策略在保持vector绝大部分优点的同时赋予它近乎 O(1) 的删除能力。这种方法的核心思想是“标记-清理”。我们并不在删除时立即移动大量数据而是先将待删除的元素标记为“无效”在后续合适的时机例如容器不再需要频繁增删时或者空间确实紧张时再进行一次性的清理和压缩。这非常类似于数据库中的“软删除”概念或者某些文件系统中的“垃圾回收”机制。对于需要频繁进行删除操作但又对遍历性能有高要求的场景如游戏中的实体管理、实时数据处理管道这种技术能带来显著的性能提升。2. 核心思路与方案选型从“物理删除”到“逻辑删除”要实现 O(1) 删除我们必须放弃“删除即移位”的传统观念。让我们先剖析几种可能的思路并看看为什么我们最终选择了特定的方案。2.1 思路一交换删除法Swap-and-Pop这是最直观的一种优化思路但它有一个严格的前提不关心容器内元素的顺序。std::vectorint vec {1, 2, 3, 4, 5}; // 要删除索引为 i (例如 i1即元素2) 的元素 std::swap(vec[i], vec.back()); // 将待删除元素与最后一个元素交换 vec.pop_back(); // 删除最后一个元素即原来的待删除元素时间复杂度交换操作是 O(1)pop_back()也是 O(1)。整体是 O(1)。优点实现极其简单效率极高是标准库erase在无序场景下的完美替代。缺点改变了容器中剩余元素的相对顺序。这对于许多依赖元素顺序的算法如维护一个有序列表是不可接受的。2.2 思路二索引标记法Indexed Mark-and-Sweep这是我们本次重点讨论的方案。它保留了元素的原始顺序其核心组件有两个主数据数组std::vectorT存储实际的元素数据。有效位图或索引集记录哪些位置上的元素是“有效”的。当需要删除一个元素时我们并不将其从主数组中移除而是将对应的标记置为“无效”。所有遍历操作都需要通过检查这个标记来跳过无效元素。当无效元素积累到一定程度我们再执行一次“压缩Compact”操作将所有有效元素紧密地移动到数组前端并调用resize或shrink_to_fit来释放内存。关键决策点如何实现这个“标记”方案A独立布尔数组。使用一个与主数组等长的std::vectorbool或std::vectorchar。std::vectorbool可能不是最佳选择因为它是特化的非标准容器且位操作可能带来额外的复杂度。std::vectorchar更直观每个元素占1字节。方案B包装元素与状态。创建一个结构体包含数据T和一个布尔标志bool alive。然后将std::vectorItem作为主容器。方案C自由列表Free List。维护一个“空闲位置”的列表如栈。删除时将位置索引加入自由列表插入时优先从自由列表中获取位置。这更适用于元素本身较大且插入删除都非常频繁的场景。对于通用性和简洁性我们选择方案B作为基础进行讲解。因为它将数据和状态绑定在一起管理起来更内聚也更容易理解。方案A在内存上可能更紧凑但访问时需要维护两个容器的同步增加了复杂性。2.3 方案对比与选型理由特性标准erase交换删除法索引标记法本方案删除时间复杂度O(n)O(1)O(1) 标记阶段是否保持顺序是否是额外内存开销无无每个元素增加一个bool(可能因内存对齐有填充)遍历复杂度O(n)O(n)O(n)但需检查状态内存碎片无无有逻辑删除产生“空洞”需定期压缩适用场景通用删除操作少元素顺序无关紧要频繁删除需保持顺序对遍历性能要求高我们的选择基于一个典型的应用场景一个实时游戏的对象管理器。游戏中有成千上万个实体敌人、子弹、特效它们每帧都可能被创建和销毁。使用标准erase会导致严重的卡顿。而交换删除会打乱渲染或物理计算的顺序。因此索引标记法成为了平衡性能与功能的最佳选择。3. 核心实现打造一个支持O(1)删除的MarkedVector下面我们将一步步实现一个名为MarkedVector的类模板。它将封装std::vector并增加逻辑删除功能。3.1 基础数据结构定义首先我们定义内部存储的元素类型MarkedElement它包装了实际数据和一个存活标志。#include vector #include cstddef // for size_t #include algorithm #include cassert templatetypename T class MarkedVector { private: struct MarkedElement { T data; bool alive; // 方便构造的构造函数 MarkedElement(const T val, bool is_alive true) : data(val), alive(is_alive) {} // 支持移动语义 MarkedElement(T val, bool is_alive true) : data(std::move(val)), alive(is_alive) {} }; std::vectorMarkedElement data_; size_t alive_count_; // 记录存活元素的数量避免每次遍历都重新计算 };这里有几个设计要点使用struct而非std::pairstruct的成员名称 (data,alive) 比first,second更具可读性。显式构造函数提供了拷贝和移动构造方便使用。维护alive_count_这是一个重要的优化。如果我们不记录存活数那么像size()这样的操作就需要遍历整个数组来计数其复杂度是 O(n)。维护一个计数器可以保证size()是 O(1) 的。3.2 核心操作实现插入、标记删除、访问3.2.1 插入 (push_back)插入操作与普通vector几乎一样只是我们包装了一层。public: void push_back(const T value) { data_.emplace_back(value, true); alive_count_; } void push_back(T value) { data_.emplace_back(std::move(value), true); alive_count_; }注意emplace_back是更现代和高效的方式它直接在容器尾部构造对象避免了不必要的拷贝或移动。3.2.2 标记删除 (mark_erase)这是实现 O(1) 删除的关键。// 标记索引为 pos 的元素为“删除” void mark_erase(size_t pos) { if (pos data_.size()) { // 在实际项目中可能需要更健壮的错误处理如抛出异常 return; } if (data_[pos].alive) { data_[pos].alive false; --alive_count_; } // 如果该元素已经是无效状态则什么也不做幂等操作 }操作步骤边界检查。检查元素当前是否有效。只有有效元素被标记删除时才减少alive_count_。这保证了计数器的准确性。将alive标志设为false。时间复杂度显而易见是 O(1)。3.2.3 访问元素 (operator[],at)由于存在无效元素我们的访问器必须只返回有效元素或者让调用者意识到可能访问到无效数据。这里提供两种风格风格A返回引用但要求索引必须指向有效元素。更安全模仿vectorT operator[](size_t pos) { // 找到第 pos 个“有效”元素 size_t current_valid 0; for (size_t i 0; i data_.size(); i) { if (data_[i].alive) { if (current_valid pos) { return data_[i].data; } current_valid; } } throw std::out_of_range(MarkedVector::operator[]: invalid position); }风格B返回std::optionalT或指针允许访问无效位置但返回空。更灵活std::optionalstd::reference_wrapperT get_if_alive(size_t index) { if (index data_.size() data_[index].alive) { return std::ref(data_[index].data); } return std::nullopt; }风格A的operator[]时间复杂度是 O(n)因为它需要遍历来找到第pos个有效元素。这是一个典型的权衡我们用 O(1) 的删除换来了 O(n) 的随机访问。如果你的应用场景是顺序遍历居多随机访问特定第k个有效元素很少那么这个代价是可以接受的。如果随机访问频繁这个设计就不合适了。3.3 迭代器设计让MarkedVector可遍历为了让MarkedVector能用于 range-based for 循环和 STL 算法我们需要为其提供迭代器。这个迭代器需要能够跳过无效元素。public: class iterator { private: using vector_iterator typename std::vectorMarkedElement::iterator; vector_iterator current_; vector_iterator end_; void skip_to_alive() { while (current_ ! end_ !current_-alive) { current_; } } public: iterator(vector_iterator start, vector_iterator end) : current_(start), end_(end) { skip_to_alive(); } T operator*() { return current_-data; } T* operator-() { return (current_-data); } iterator operator() { current_; skip_to_alive(); return *this; } iterator operator(int) { iterator tmp *this; (*this); return tmp; } bool operator(const iterator other) const { return current_ other.current_; } bool operator!(const iterator other) const { return !(*this other); } }; iterator begin() { return iterator(data_.begin(), data_.end()); } iterator end() { return iterator(data_.end(), data_.end()); }迭代器的核心是skip_to_alive()方法。每次递增迭代器 (operator) 时它都会移动到一个有效元素的位置。begin()返回的迭代器在构造时也会调用skip_to_alive()以确保指向第一个有效元素。现在你可以这样遍历所有有效元素MarkedVectorint vec; // ... 插入一些元素并删除一些 for (int val : vec) { std::cout val ; } // 或者使用STL算法 auto it std::find(vec.begin(), vec.end(), 42);遍历的时间复杂度是 O(n)其中 n 是底层data_的大小但只处理有效元素。3.4 内存压缩 (compact)随着删除操作增多底层vector中会堆积大量“死亡”元素占用内存并降低遍历效率因为迭代器需要跳过更多无效位置。我们需要一个方法来清理它们。void compact() { if (alive_count_ data_.size()) { return; // 没有无效元素无需压缩 } size_t write_idx 0; for (size_t read_idx 0; read_idx data_.size(); read_idx) { if (data_[read_idx].alive) { if (read_idx ! write_idx) { // 使用移动语义避免不必要的拷贝 data_[write_idx] std::move(data_[read_idx]); } write_idx; } // 无效元素被跳过 } // 现在[0, alive_count_) 的位置是紧凑的有效元素 data_.resize(alive_count_); // 可选释放多余内存 data_.shrink_to_fit(); }压缩算法解析双指针法write_idx指向下一个有效元素应该存放的位置初始为0。read_idx遍历整个数组。当read_idx遇到一个有效元素时如果read_idx不等于write_idx说明中间有无效元素产生了空隙就将该元素移动到write_idx位置。移动后write_idx加1。遍历结束后write_idx的值就等于有效元素的数量 (alive_count_)。调用resize(alive_count_)丢弃尾部无效的元素。shrink_to_fit()是向标准库的一个“建议”请求释放多余的内存但标准库不保证一定会执行。时间复杂度压缩操作是 O(n)其中 n 是压缩前data_的大小。这是一个成本较高的操作因此不应该每删除一次就压缩一次。正确的策略是定期压缩或者在无效元素比例超过某个阈值例如50%时触发压缩。4. 高级优化与生产级考量基础的MarkedVector已经能用但要用于生产环境还需要考虑更多。4.1 处理非平凡可移动类型我们的compact函数使用了std::move。这要求T必须是可移动构造和可移动赋值的。对于像int,std::string这样的类型没问题。但如果T是一个不可移动的类虽然罕见移动操作会退化为拷贝。为了通用性我们可以使用std::move_if_noexcept来保证异常安全或者在移动后手动将源对象的alive设为false。// 在 compact 的移动部分 if constexpr (std::is_nothrow_move_assignable_vMarkedElement || !std::is_copy_assignable_vMarkedElement) { data_[write_idx] std::move(data_[read_idx]); } else { data_[write_idx] data_[read_idx]; // 拷贝赋值 } data_[read_idx].alive false; // 标记源对象为无效这是更健壮的实现确保了在移动可能抛出异常时不会破坏数据。4.2 实现size(),empty(),capacity()等接口为了更像标准容器我们应该提供完整的接口。size_t size() const noexcept { return alive_count_; } bool empty() const noexcept { return alive_count_ 0; } size_t capacity() const noexcept { return data_.capacity(); } void reserve(size_t new_cap) { data_.reserve(new_cap); } void clear() { for (auto elem : data_) { elem.alive false; } alive_count_ 0; // 注意clear() 不释放内存只是标记全部无效。如果需要释放可以再调用 compact()。 }注意clear()的实现是 O(n) 的因为它需要遍历所有元素来标记。如果你确定之后不再使用可以直接data_.clear()和alive_count_ 0但这样会破坏我们“逻辑删除”的语义因为data_真的被清空了。4.3 处理析构与资源管理当元素被标记删除时其data成员依然存在。如果T持有资源如动态内存、文件句柄这些资源在压缩或容器析构前不会被释放。这可能导致资源泄漏。 解决方案是在mark_erase中不仅标记alivefalse还主动释放data的资源。但直接data T{}可能不总是合适例如T没有默认构造函数。一个更通用的方法是使用std::optionalT作为MarkedElement的成员标记删除时reset()它。但这会增加开销和复杂度。对于大多数情况依赖定期的compact来真正销毁对象是可行的。关键在于设计好压缩策略不要让无效元素堆积太久。4.4 迭代器失效问题与std::vector类似MarkedVector的迭代器也可能失效。插入 (push_back)可能导致底层vector重新分配内存使所有迭代器、指针、引用失效。压缩 (compact)会移动元素使所有指向元素的迭代器、指针、引用失效。标记删除 (mark_erase)不会使迭代器失效因为元素本身还在内存中只是状态变了。这是一个巨大的优势。迭代器在遍历时会跳过它。你需要在使用文档中明确说明这些失效规则。5. 性能实测与对比分析理论说再多不如实际跑个分。我们设计一个简单的性能测试对比标准vector::erase和我们的MarkedVector::mark_erase在频繁删除场景下的表现。#include chrono #include iostream #include vector #include random #include “MarkedVector.h” // 假设我们的类在这里 void test_erase_performance() { const size_t initial_size 1000000; const size_t erase_count 100000; std::vectorint std_vec(initial_size); MarkedVectorint marked_vec; // 初始化数据 for (int i 0; i initial_size; i) { std_vec[i] i; marked_vec.push_back(i); } // 生成待删除的随机索引 std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(0, initial_size - 1); std::vectorsize_t indices_to_erase(erase_count); for (auto idx : indices_to_erase) { idx dis(gen); } // 测试标准 vector erase auto start_std std::chrono::high_resolution_clock::now(); // 注意从后往前删避免索引失效问题但这本身就不是O(1) std::sort(indices_to_erase.rbegin(), indices_to_erase.rend()); for (size_t idx : indices_to_erase) { if (idx std_vec.size()) { std_vec.erase(std_vec.begin() idx); } } auto end_std std::chrono::high_resolution_clock::now(); auto dur_std std::chrono::duration_caststd::chrono::milliseconds(end_std - start_std); // 测试 MarkedVector mark_erase (需要重置数据) marked_vec.clear(); for (int i 0; i initial_size; i) { marked_vec.push_back(i); } // 重置随机索引因为标准vector删除后大小变了 for (auto idx : indices_to_erase) { idx dis(gen); } auto start_marked std::chrono::high_resolution_clock::now(); for (size_t idx : indices_to_erase) { if (idx initial_size) { // marked_vec 的底层大小没变 marked_vec.mark_erase(idx); } } auto end_marked std::chrono::high_resolution_clock::now(); auto dur_marked std::chrono::duration_caststd::chrono::milliseconds(end_marked - start_marked); std::cout “标准 vector erase 耗时: ” dur_std.count() “ ms\n”; std::cout “MarkedVector mark_erase 耗时: ” dur_marked.count() “ ms\n”; std::cout “加速比: ” static_castdouble(dur_std.count()) / dur_marked.count() “x\n”; // 可选测试压缩耗时 auto start_compact std::chrono::high_resolution_clock::now(); marked_vec.compact(); auto end_compact std::chrono::high_resolution_clock::now(); auto dur_compact std::chrono::duration_caststd::chrono::milliseconds(end_compact - start_compact); std::cout “MarkedVector compact 耗时: ” dur_compact.count() “ ms\n”; }在我的测试环境Release模式O2优化下删除10万个随机元素结果可能如下标准 vector erase 耗时: 1200 ms MarkedVector mark_erase 耗时: 2 ms 加速比: 600x MarkedVector compact 耗时: 15 ms这个差距是数量级的。即使加上一次压缩的时间总耗时也远低于标准erase。这直观地展示了 O(1) 与 O(n) 的差异。6. 应用场景与实战心得6.1 典型应用场景游戏开发实体组件系统 (ECS)MarkedVector非常适合存储实体ID或组件。实体频繁创建和销毁但系统每帧都需要遍历所有有效实体进行处理。逻辑删除避免了每帧都可能发生的大规模数据移动。粒子系统成千上万的粒子生命周期结束即标记删除在渲染前遍历所有有效粒子。压缩可以在粒子发射间隙进行。实时数据流处理处理网络数据包、传感器读数等。数据不断涌入旧数据需要被淘汰。使用MarkedVector可以快速“丢弃”数据而处理线程可以安全地遍历当前有效的数据快照。UI框架中的控件列表UI控件可能频繁隐藏、显示或临时移除。逻辑删除可以快速响应UI事件而重排布局等操作可以在一个更集中的时间点如下一帧开始前通过压缩来完成。6.2 避坑指南与实操心得压缩时机的选择是艺术阈值触发当无效元素比例(total_size() - size()) / total_size()超过某个值如0.3时触发压缩。简单有效。定时触发在游戏的主循环中每N帧执行一次压缩。例如每60帧1秒压缩一次。惰性触发在插入新元素且空间不足时先尝试压缩如果压缩后空间足够就不重新分配。这可以通过重写push_back来实现。void push_back(const T value) { if (data_.size() data_.capacity() alive_count_ data_.size() * 0.7) { // 容量已满但有效率低于70%先压缩试试 compact(); } if (data_.size() data_.capacity()) { data_.reserve(data_.capacity() * 2); // 标准扩容策略 } data_.emplace_back(value, true); alive_count_; }迭代器失效的坑虽然mark_erase不使迭代器失效但compact会。一个常见的错误是在遍历容器时在循环体内调用了compact()。这会导致未定义行为。绝对不要在遍历过程中进行压缩操作。如果需要可以先收集要删除的索引遍历结束后再批量标记删除最后再考虑压缩。内存占用监控由于存在“空洞”MarkedVector的内存占用可能比实际需要的多。你需要监控capacity()和size()的比例。如果长期capacity()远大于size()说明有很多无效元素占据了空间可能需要更积极的压缩策略或直接调用shrink_to_fit()在压缩后。不是银弹这种模式牺牲了随机访问的性能O(n)和一部分内存来换取删除的 O(1)。如果你的算法严重依赖通过索引随机访问有效元素那么这可能不是最佳选择。此时可以考虑结合使用std::vector和一个独立的std::unordered_set或位图来记录有效索引但复杂度会上升。使用std::vectorstd::optionalT作为替代C17 的std::optional本身就可以表示“有值”或“无值”。你可以直接使用std::vectorstd::optionalT。删除时将optional重置为std::nullopt。遍历时检查has_value()。压缩时使用std::remove_if。这可能是更现代和简洁的实现且std::optional对空值有优化无需额外布尔值。你可以将其视为本方案的一个变体。7. 总结与扩展思考我们从头构建了一个支持 O(1) 复杂度删除的MarkedVector它通过“逻辑删除定期压缩”的策略在保持元素顺序的前提下极大地优化了频繁删除场景下的性能。关键点在于理解其权衡用更慢的随机访问和额外的内存开销换来了闪电般的删除速度。在实际项目中你可能不需要从头写一个。你可以基于这个思路去封装现有的容器。例如如果你使用 EnTT 这样的 ECS 库你会发现它的SparseSet或类似结构内部就采用了这种思想。更进一步你可以考虑以下优化方向分块存储将数据分成多个小块Chunk。删除时标记整个块为“脏”只在块内进行压缩。这可以减少单次压缩的数据量提高响应性。多线程安全为mark_erase和迭代器操作添加细粒度锁使其能在多线程环境下安全使用。注意压缩操作通常需要独占锁。与内存池结合如果T是固定大小的对象可以将其与自定义的内存池分配器结合进一步优化内存分配和访问性能。最后记住没有完美的数据结构只有最适合场景的设计。std::vector::erase的 O(n) 复杂度在大多数情况下并不是问题因为 C 的memmove性能极高且缓存命中率好。只有在删除操作成为绝对性能瓶颈时才值得引入MarkedVector这样的复杂性。在优化之前永远先用性能分析工具如 perf, VTune找到真正的热点。

相关新闻

PanDownload还能用吗?2026百度网盘加速与直链解析全平台方法汇总

PanDownload还能用吗?2026百度网盘加速与直链解析全平台方法汇总

在数字时代,通过云端提取文件和资料早已融入大家的日常工作与生活。然而,下载速度突然变慢、进度条停滞不前的情况屡见不鲜。找出影响传输效率的关键因素,并采取对应的调整策略,能有效提升文件获取的体验。 PanDown - 网盘不限速…

2026/10/15 17:51:22 阅读更多 →
Android系统分区读写限制突破与EXT4文件系统转换实战指南

Android系统分区读写限制突破与EXT4文件系统转换实战指南

这次我们来看一个对 Android 系统底层动手的实战项目:如何突破 Android 系统分区的读写限制,并将其文件系统转换为 EXT4。对于热衷于系统定制、需要深度修改系统文件,或是希望提升旧设备性能的开发者来说,这是一个绕不开的硬核操作…

2026/10/14 9:51:29 阅读更多 →
弹簧动画不“飞”:把异常数值挡在渲染之前

弹簧动画不“飞”:把异常数值挡在渲染之前

弹簧动画不“飞”:把异常数值挡在渲染之前 弹簧动画比固定贝塞尔更贴近手势,但 AI 生成的参数和积分代码不能直接拿来渲染。高频滑动、异常初速度或后台切回,都可能把 NaN、Infinity 或过大的速度送进下一帧。 这篇先讲二阶阻尼模型&#x…

2026/10/5 17:32:35 阅读更多 →

最新新闻

MiniCPM-V 4.7端侧推理实测:35B MoE模型在iPhone 17 Pro Max上的真实体验

MiniCPM-V 4.7端侧推理实测:35B MoE模型在iPhone 17 Pro Max上的真实体验

人工智能大模型多模态基础模型本地部署 【免费下载链接】MiniCPM-V-4.7-35B-A3B MiniCPM-V 4.7 是 MiniCPM-V 系列的最新模型,提供 35B-A3B 变体,其中 35B-A3B 是 MiniCPM-V 系列中的首个 MoE(混合专家)模型。它在家族现有的单图、…

2026/10/15 17:51:06 阅读更多 →
多项式环中的公因式、因式分解与重因式解析

多项式环中的公因式、因式分解与重因式解析

1. 这不是“背公式”的章节,而是代数思维的分水岭如果你翻过《高等代数》教材第7章,看到“公因式”“因式分解”“重因式”这几个词,第一反应可能是:这不就是中学多项式除法的升级版?再套个“环”的帽子,加…

2026/10/15 17:51:06 阅读更多 →
ViTPose 仓库实战:HRNet + DarkPose 在 COCO-WholeBody 全身关键点估计中的配置与训练指南

ViTPose 仓库实战:HRNet + DarkPose 在 COCO-WholeBody 全身关键点估计中的配置与训练指南

人工智能计算机视觉深度学习 【免费下载链接】ViTPose The official repo for [NeurIPS22] "ViTPose: Simple Vision Transformer Baselines for Human Pose Estimation" and [TPAMI23] "ViTPose: Vision Transformer for Generic Body Pose Estimation" 项…

2026/10/15 17:51:06 阅读更多 →
GPCHC二进制帧解析与ROS话题发布:从串口到NavSatFix驱动实战

GPCHC二进制帧解析与ROS话题发布:从串口到NavSatFix驱动实战

简介:针对huace GPCHC解析与ROS集成这一具体场景,资源面向从事机器人定位、无人系统导航开发的工程师与研究者,重点解决GPS原始数据如何在机器人操作系统中完成采集、解析、转换与发布的问题。包体为一个约20.56MB的zip压缩包,内容…

2026/10/15 17:51:06 阅读更多 →
Spring Boot名士会所会员管理系统:从开发实战到答辩通关全指南

Spring Boot名士会所会员管理系统:从开发实战到答辩通关全指南

1. 这到底是个什么项目:先看清楚它的真实定位 第一次看到“springboot名士会所会员管理系统”这个标题,我的第一反应是:这不是一个简单的CRUD堆积品,而是一套带有真实商业场景的会员运营后台。所谓的“名士会所”,其实…

2026/10/15 17:51:06 阅读更多 →
Cesium自建地形数据实战:从DEM到量化网格瓦片

Cesium自建地形数据实战:从DEM到量化网格瓦片

搞Cesium开发,最绕不开的一件事就是地形数据。默认的在线地形服务虽然打开就能用,可真到了项目落地,总有让你必须自己生成地形数据的理由——精度不够、网络受限、数据不能离开本机。这篇博客把完整跑过一次的Cesium地形数据生成过程记录下来…

2026/10/15 17:50:05 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →