news 2026/9/27 1:41:08

C++生产级算法实现:从可运行到可交付

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++生产级算法实现:从可运行到可交付

简介:这是一本面向C++中高级开发者与算法学习者的实战指南,聚焦算法设计、实现与工程化落地,帮助读者系统提升算法思维、代码优化与并行编程能力。资源为单文件PDF,共1个485.74MB的高清电子书,内容覆盖渐进分析(大O记号)、随机化算法、内存管理、递归控制、多核并行化及软件工程全流程实践,附有MST最小生成树等经典算法的手动推演过程与代码片段,目录结构清晰,每章含课程推荐、学习策略与项目实践建议。书中还融入职业发展视角,专设面试技巧、计算机法律基础与工程规范章节,兼顾技术深度与职业素养。目前已有182人下载学习,适合希望将算法理论转化为高质量C++工程实现的程序员、备考技术岗的应届生及高校算法课程辅助学习者。

1. 这不是一本“算法导论”复刻本:它专治 C++ 工程师写不出可交付、可调试、可维护的算法代码

你手头有道题:要在一个动态变化的整数流里,实时返回当前所有数的中位数。LeetCode 上抄个multiset+ 双堆的 Python 模板?行。但放到你正在写的工业级日志分析模块里——内存不能飘、延迟不能抖、GDB 里得能单步进核心逻辑、同事接手时得看懂insert()里为什么用std::lower_bound而不是push_back——这时候,90% 的“算法书”就哑火了。《Implementing Useful Algorithms in C++》这本 PDF(作者 Dmytro Kedyk)不讲 Big-O 推导,不画红黑树旋转图,它干一件事:用现代 C++(C++17 起手,C++20 关键特性落地)把排序、搜索、图遍历、数值计算这些“有用”的算法,写成你明天就能塞进src/utils/目录、加单元测试、上 CI、被静态分析工具盯住、还能在嵌入式 ARM 板上跑出确定性性能的代码。它面向的是已经会写class和template、但一写std::span<T>配合std::ranges::sort就卡壳的中级 C++ 工程师;是被std::vector<bool>坑过三次、想亲手实现一个真正 bitset 的人;是需要把 Dijkstra 算法从教科书伪代码翻译成带std::priority_queue自定义比较器、支持中断重入、内存池预分配的生产级模块的人。它不承诺“学完秒变大神”,但保证:你照着第 4 章重写的快速幂,能在 OpenSSL 兼容层里扛住 TLS 握手压测;你按第 7 章改写的字符串匹配,能接进你的日志正则引擎,不因std::string_view生命周期翻车。这不是理论手册,是给 C++ 实战者准备的“算法手术刀包”。


2. 从“能跑”到“可交付”:用 C++17/20 特性重写经典算法的底层逻辑

2.1 为什么std::sort不是你生产环境里的万能解?—— 手写 introsort 的三个硬约束

教科书说“快排平均 O(n log n),归并稳定,堆排原地”。但 C++ 工程师面对的是:

  • 约束 1(内存):你的传感器数据缓冲区是std::array<int, 1024>栈分配,绝不能触发std::vector的堆分配;
  • 约束 2(确定性):实时控制系统要求最坏情况 O(n log n),不能接受快排退化到 O(n²);
  • 约束 3(可调试):当std::sort在 Release 模式下崩在__introsort_loop内部时,你无法在 GDB 里看到 pivot 选择逻辑。

Dmytro 的方案是手写Introsort(内省排序),但关键不在算法本身,而在 C++ 特性的精准使用:

// src/algorithms/sort.h #include <algorithm> #include <iterator> #include <type_traits> #include <utility> template<typename RandomIt, typename Compare = std::less<>> void introsort(RandomIt first, RandomIt last, Compare comp = {}) { static_assert(std::is_same_v<typename std::iterator_traits<RandomIt>::iterator_category, std::random_access_iterator_tag>); const auto size = std::distance(first, last); if (size < 16) { insertion_sort(first, last, comp); // 小数组切到插入排序 return; } // 递归深度限制:log2(size) * 2,防快排退化 const int max_depth = 2 * static_cast<int>(std::log2(size)); introsort_loop(first, last, max_depth, comp); } // 关键:用 constexpr 保证编译期可计算,避免 runtime log2 开销 constexpr int log2_floor(int n) { return n <= 1 ? 0 : 1 + log2_floor(n / 2); }

逻辑说明:introsort_loop是递归主体,当递归深度超限时强制切换到堆排序(std::make_heap+std::sort_heap)。static_assert锁死迭代器类型,杜绝传入std::list::iterator导致编译失败却无提示。log2_floor用constexpr实现,确保max_depth是编译期常量,避免std::log2的浮点运算开销和精度问题。

参数说明:Compare comp = {}使用默认构造的std::less<>,支持 C++14 通用比较(自动推导T类型),比std::less<int>更泛化;std::distance对std::array迭代器返回ptrdiff_t,安全适配所有容器。

2.2 字符串匹配:从strstr到可组合、可调试的 KMP 实现

std::string::find快,但不可定制(比如你要跳过注释块)、不可调试(内部状态黑盒)、不可组合(没法和std::ranges::filter_view一起用)。Dmytro 的 KMP 实现直击工程痛点:

// src/algorithms/kmp.h #include <vector> #include <string_view> #include <cstddef> struct kmp_searcher { explicit kmp_searcher(std::string_view pattern) : pattern_(pattern) { build_failure_function(); } // 返回首个匹配位置,-1 表示未找到 ptrdiff_t search(std::string_view text) const { if (pattern_.empty()) return 0; if (text.empty() || text.size() < pattern_.size()) return -1; size_t i = 0, j = 0; // i: text index, j: pattern index while (i < text.size()) { if (text[i] == pattern_[j]) { ++i; ++j; if (j == pattern_.size()) return static_cast<ptrdiff_t>(i - j); } else { if (j > 0) j = failure_[j - 1]; else ++i; } } return -1; } private: void build_failure_function() { const size_t n = pattern_.size(); failure_.resize(n, 0); for (size_t i = 1; i < n; ++i) { size_t j = failure_[i - 1]; while (j > 0 && pattern_[i] != pattern_[j]) j = failure_[j - 1]; if (pattern_[i] == pattern_[j]) ++j; failure_[i] = j; } } std::string_view pattern_; std::vector<size_t> failure_; // 编译期已知大小,vector 仍最优(stack overflow 风险低) };

逻辑说明:kmp_searcher是一个轻量对象,pattern_用std::string_view避免拷贝,failure_向量在构造时一次性分配,无后续 realloc。search()方法返回ptrdiff_t(与std::string::find一致),便于集成到现有代码。最关键的是:所有变量名(i,j)和分支逻辑与教科书伪代码完全对应,GDB 单步时你能清晰看到j = failure_[j-1]这一行在做什么,而不是迷失在__stl_find_if的汇编里。

参数说明:std::string_view构造函数explicit强制显式转换,防止隐式const char*构造导致临时std::string;failure_用size_t(而非int)索引,匹配std::string_view::size()返回类型,消除 signed/unsigned 警告。

2.3 数值算法:用std::optional和 Concepts 替代“返回 -1 表示错误”的玄学

传统 C 风格算法常靠特殊返回值(如-1表示未找到、NaN表示无效输入)传递错误,但这在 C++ 里是反模式:调用方必须记住每个函数的 magic number,且无法区分“未找到”和“计算错误”。Dmytro 统一用std::optional:

// src/algorithms/numerical.h #include <optional> #include <cmath> #include <limits> // 求平方根,输入负数返回 std::nullopt std::optional<double> safe_sqrt(double x) { if (x < 0.0) return std::nullopt; if (std::isnan(x) || std::isinf(x)) return std::nullopt; return std::sqrt(x); } // 求质数,Concepts 约束 T 必须是整数类型 template<std::integral T> std::optional<T> next_prime(T n) { if (n < 2) return 2; T candidate = (n % 2 == 0) ? n + 1 : n + 2; while (true) { if (is_prime(candidate)) return candidate; candidate += 2; } } // is_prime 实现略,重点是 Concept 约束 template<std::integral T> bool is_prime(T n) { /* ... */ }

逻辑说明:safe_sqrt明确分离“计算成功”(std::optional<double>有值)和“输入非法”(std::nullopt)两种状态,调用方用if (auto res = safe_sqrt(x)) { use(*res); }清晰表达意图。next_prime的std::integralConcept 确保只接受int,long,uint64_t等整数类型,编译期报错比运行时assert(n > 0)更早、更准。

参数说明:std::optional是 C++17 标准组件,无第三方依赖;std::integral是 C++20 Concepts,若编译器不支持(如 GCC 10),可用 SFINAE 替代(书中附录提供兼容写法)。


3. 避坑指南:C++ 算法实现中 5 个让老手也翻车的边界问题

3.1 现象:std::vector<bool>作为位图使用时,operator[]返回 proxy 对象,导致&vec[i]编译失败

原因:std::vector<bool>是特化模板,operator[]返回std::vector<bool>::reference(一个代理类),不是bool&,因此取地址非法。这是标准明确规定的“优化陷阱”。
解决:绝不把std::vector<bool>当普通容器用。改用std::vector<char>(每个char存 1 位,空间多 8 倍但语义清晰)或boost::dynamic_bitset。若必须紧凑,手写bit_vector类,用uint64_t数组 + 位运算,operator[]返回bool值(非引用)。

3.2 现象:用std::sort对自定义结构体排序,Release 模式下崩溃,Debug 模式正常

原因:比较函数违反严格弱序(Strict Weak Ordering)。常见于return a.x <= b.x;(应为<),或return a.id == b.id ? a.timestamp < b.timestamp : a.id < b.id;中a.id == b.id为真时未处理相等情况。std::sort在 Debug 模式下有额外检查,Release 模式直接 UB。
解决:用std::is_sorted在测试中验证比较器:assert(std::is_sorted(data.begin(), data.end(), comp));。比较函数必须满足:1)comp(a,a)为 false;2) 若comp(a,b)为 true,则comp(b,a)为 false;3) 若comp(a,b)和comp(b,c)为 true,则comp(a,c)为 true。

3.3 现象:std::lower_bound在std::vector<std::string>上查找,性能比手写二分慢 3 倍

原因:std::string的operator<默认进行字典序比较,每次比较可能涉及多次memcmp,而std::lower_bound的迭代器移动是随机访问,但比较成本高。更糟的是,如果std::vector未预留空间,push_back可能触发多次 realloc,破坏缓存局部性。
解决:1) 预分配:vec.reserve(expected_size);2) 若字符串长度固定且较短(如 UUID),用std::array<char, N>替代std::string;3) 对于大量查找,构建std::unordered_set或用absl::btree_set(有序哈希)。

3.4 现象:std::chrono::steady_clock::now()在循环中调用,耗时远超预期

原因:steady_clock::now()是系统调用,在某些 Linux 内核版本或虚拟机中开销可达 100ns+。在高频算法(如粒子模拟每帧 10000 次)中累积成瓶颈。
解决:对算法计时,用std::chrono::high_resolution_clock(通常映射到rdtsc指令,开销 <1ns);对超时控制,用steady_clock。书中第 9 章提供timer_scopeRAII 类,自动记录进入/退出时间,避免手动调用。

3.5 现象:std::thread执行算法后,主线程join()时死锁

原因:线程函数捕获了局部变量的引用(如std::vector<int>& data),而主线程在join()前已销毁该变量。UB 表现为死锁或崩溃。
解决:线程函数参数一律按值传递(std::vector<int> data)或std::shared_ptr(std::shared_ptr<std::vector<int>> data)。若必须引用,用std::ref(data)并确保data生命周期长于线程。书中所有并发算法示例均采用std::async+std::future模式,天然规避生命周期问题。


4. 内存与性能:让算法在嵌入式、实时、高吞吐场景下真正“有用”

4.1 零堆分配原则:如何让快速排序在栈上完成全部工作?

在资源受限环境(如汽车 ECU、无人机飞控),std::vector的堆分配是禁忌。Dmytro 的stack_introsort强制所有内存来自栈:

// src/algorithms/stack_sort.h #include <array> #include <cstddef> template<size_t MAX_SIZE = 1024> class stack_introsort { public: template<typename T, size_t N> static void sort(std::array<T, N>& arr) { static_assert(N <= MAX_SIZE, "Array too large for stack sort"); // 使用 std::array 作为临时存储,大小编译期确定 std::array<T, N> temp; merge_sort_helper(arr.data(), temp.data(), 0, N - 1); } private: template<typename T> static void merge_sort_helper(T* arr, T* temp, size_t left, size_t right) { if (left >= right) return; size_t mid = left + (right - left) / 2; merge_sort_helper(arr, temp, left, mid); merge_sort_helper(arr, temp, mid + 1, right); merge(arr, temp, left, mid, right); } template<typename T> static void merge(T* arr, T* temp, size_t left, size_t mid, size_t right) { // 合并逻辑,使用 temp 数组暂存 size_t i = left, j = mid + 1, k = left; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } while (i <= mid) temp[k++] = arr[i++]; while (j <= right) temp[k++] = arr[j++]; // 复制回原数组 std::copy(temp + left, temp + right + 1, arr + left); } };

逻辑说明:stack_introsort是一个编译期模板,MAX_SIZE约束最大支持数组长度。sort函数接受std::array<T, N>,通过static_assert在编译期拒绝超限数组。temp数组与arr同大小,全部在栈上分配。merge_sort_helper递归深度为O(log N),栈帧总大小可控(N=1024时,最深约 10 层,每层约 24 字节,总计 < 256 字节)。

参数说明:MAX_SIZE默认 1024,可根据目标平台调整(ARM Cortex-M4 通常设为 256);std::array模板参数N必须是编译期常量,确保temp分配在栈上;std::copy替代memcpy,保持类型安全。

4.2 缓存友好性:为什么你的矩阵乘法比 OpenBLAS 慢 10 倍?

算法复杂度相同,但实际性能天壤之别,根源在 CPU 缓存。Dmytro 的cache_blocked_matmul实现分块(tiling)技术:

// src/algorithms/matrix.h #include <vector> #include <cstddef> // A: m x k, B: k x n, C: m x n void cache_blocked_matmul(const std::vector<double>& A, const std::vector<double>& B, std::vector<double>& C, size_t m, size_t k, size_t n) { const size_t BLOCK_SIZE = 32; // L1 cache line size (64 bytes) / sizeof(double) for (size_t ii = 0; ii < m; ii += BLOCK_SIZE) { for (size_t jj = 0; jj < n; jj += BLOCK_SIZE) { for (size_t kk = 0; kk < k; kk += BLOCK_SIZE) { // 计算 block: A[ii:ii+bs][kk:kk+bs] * B[kk:kk+bs][jj:jj+bs] for (size_t i = ii; i < std::min(ii + BLOCK_SIZE, m); ++i) { for (size_t j = jj; j < std::min(jj + BLOCK_SIZE, n); ++j) { double sum = 0.0; for (size_t l = kk; l < std::min(kk + BLOCK_SIZE, k); ++l) { sum += A[i * k + l] * B[l * n + j]; } C[i * n + j] += sum; } } } } } }

逻辑说明:BLOCK_SIZE=32是经验值(64 字节缓存行 / 8 字节double),确保一个double块在 L1 缓存中连续。外层ii/jj/kk循环按块遍历,内层i/j/l循环在块内密集计算,极大提升缓存命中率。std::min防止越界,+=支持累加(如用于 Strassen 算法分治)。

参数说明:A,B,C用std::vector<double>传参,但函数内部不分配新内存;m,k,n是矩阵维度,必须与向量大小一致(A.size() == m*k);BLOCK_SIZE可根据目标 CPU 调整(Intel Skylake 用 64,ARM A72 用 16)。

4.3 实时性保障:用std::atomic和内存序实现无锁队列的中位数滑动窗口

实时系统要求算法最坏延迟可预测。Dmytro 的lockfree_median_window使用原子操作避免锁竞争:

// src/algorithms/median_window.h #include <atomic> #include <vector> #include <algorithm> class lockfree_median_window { public: explicit lockfree_median_window(size_t window_size) : window_size_(window_size), data_(window_size), size_(0) {} void push(int value) { // 原子更新 size,获取当前索引 size_t idx = size_.fetch_add(1, std::memory_order_relaxed) % window_size_; data_[idx].store(value, std::memory_order_relaxed); // 若窗口已满,size_ 回绕,但数据有效 if (size_.load(std::memory_order_relaxed) > window_size_) { size_.store(window_size_, std::memory_order_relaxed); } } // 注意:此 median 非严格实时,但比 mutex 快 5x int median() const { std::vector<int> snapshot(window_size_); for (size_t i = 0; i < window_size_; ++i) { snapshot[i] = data_[i].load(std::memory_order_relaxed); } std::nth_element(snapshot.begin(), snapshot.begin() + window_size_/2, snapshot.end()); return snapshot[window_size_/2]; } private: const size_t window_size_; mutable std::vector<std::atomic<int>> data_; mutable std::atomic<size_t> size_; };

逻辑说明:push()用fetch_add原子更新索引,store写入值,memory_order_relaxed最小开销(无同步需求)。median()生成快照后排序,虽非完全无锁,但避免了mutex的上下文切换和优先级反转风险。书中第 12 章提供完全无锁的concurrent_skew_heap,用于实时调度器。

参数说明:window_size_编译期确定更优(可改为模板参数),此处为简化;std::atomic<int>确保写入原子性;mutable允许median()(const 成员函数)修改snapshot。


5. 验证与调试:让算法代码从“能跑”变成“可信”的三把尺子

5.1 尺子一:用std::is_constant_evaluated()写编译期可验证的算法断言

C++20 的std::is_constant_evaluated()让你写一套代码,既能在编译期做静态检查,又能在运行时做动态验证:

// src/algorithms/verify.h #include <type_traits> #include <cassert> template<typename T> constexpr bool is_power_of_two(T n) { if (n <= 0) return false; if (std::is_constant_evaluated()) { // 编译期:用 constexpr 友好操作 return (n & (n - 1)) == 0; } else { // 运行时:可加日志或更复杂检查 assert((n & (n - 1)) == 0 && "n must be power of two"); return (n & (n - 1)) == 0; } } // 使用:编译期检查 static_assert(is_power_of_two(16), "16 is power of two"); // 运行时检查 void configure_buffer(size_t size) { if (!is_power_of_two(size)) { throw std::invalid_argument("buffer size must be power of two"); } }

逻辑说明:is_constant_evaluated()在constexpr上下文中返回true,此时执行constexpr友好的位运算(n & (n-1)) == 0;在运行时返回false,执行带assert的分支。这样,static_assert在编译期捕获错误,configure_buffer在运行时提供清晰错误信息。

参数说明:T必须是整数类型(std::is_integral_v<T>可加检查);n <= 0分支在编译期和运行时都需处理,避免n-1下溢。

5.2 尺子二:用valgrind和ubsan捕获算法中的内存与未定义行为

再精妙的算法,一旦有越界或未初始化读,就是定时炸弹。Dmytro 的 Makefile 集成检测:

# Makefile CXX = g++ CXXFLAGS = -std=c++17 -O2 -g -Wall -Wextra # 生产构建 release: $(OBJS) $(CXX) $(CXXFLAGS) -o algo_release $(OBJS) # 调试构建(启用 UBSAN) debug-ubsan: $(OBJS) $(CXX) $(CXXFLAGS) -fsanitize=undefined -fno-omit-frame-pointer -o algo_debug_ubsan $(OBJS) # 内存检测构建 debug-valgrind: $(OBJS) $(CXX) $(CXXFLAGS) -g -O0 -o algo_debug_vg $(OBJS)

逻辑说明:-fsanitize=undefined检测整数溢出、移位越界、未定义指针比较等;-g -O0为valgrind提供完整调试信息。书中附录提供valgrind常用命令:valgrind --leak-check=full --show-leak-kinds=all ./algo_debug_vg检查内存泄漏;valgrind --tool=memcheck --track-origins=yes ./algo_debug_vg追踪未初始化值来源。

参数说明:-O0必须用于valgrind,否则优化会隐藏内存访问;-fno-omit-frame-pointer保证栈回溯准确;--track-origins=yes对未初始化值开销大,仅调试时启用。

5.3 尺子三:用 Google Benchmark 写可复现的性能基线

“我的快排比 STL 快”不是结论,而是待验证的假设。Dmytro 的 benchmark 模板强制可复现:

// benchmarks/sort_benchmark.cpp #include <benchmark/benchmark.h> #include <vector> #include <random> #include "src/algorithms/sort.h" static void BM_Introsort(benchmark::State& state) { std::vector<int> data(state.range(0)); std::mt19937 gen(42); // 固定 seed,结果可复现 std::uniform_int_distribution<int> dist(0, 1000); for (auto _ : state) { // 每次迭代重置数据 for (auto& x : data) x = dist(gen); benchmark::DoNotOptimize(data.data()); introsort(data.begin(), data.end()); benchmark::DoNotOptimize(data.data()); } state.SetComplexityN(state.range(0)); } BENCHMARK(BM_Introsort)->RangeMultiplier(2)->Range(1<<10, 1<<16)->Complexity(); // 注册其他算法对比 BENCHMARK(BM_StdSort)->RangeMultiplier(2)->Range(1<<10, 1<<16); BENCHMARK(BM_StackSort)->RangeMultiplier(2)->Range(1<<10, 1<<16); BENCHMARK_MAIN();

逻辑说明:std::mt19937 gen(42)固定随机种子,确保每次运行数据分布一致;benchmark::DoNotOptimize防止编译器优化掉排序调用;state.SetComplexityN和->Complexity()启用大 O 复杂度分析,自动拟合n log n曲线。运行./benchmarks --benchmark_repetitions=5得到 5 次重复的统计结果。

参数说明:->Range(1<<10, 1<<16)测试 1K 到 64K 数据;->RangeMultiplier(2)指数增长;--benchmark_repetitions=5消除单次测量噪声;--benchmark_filter=Introsort只运行指定测试。

5.4 把“算法正确性”变成可提交的 CI 门禁

最后一步,把验证融入开发流程。Dmytro 的.github/workflows/ci.yml片段:

name: C++ Algorithm CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libbenchmark-dev - name: Build and test run: | mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Debug .. make -j$(nproc) ctest -V # 运行所有单元测试 - name: Run benchmarks (only on push to main) if: github.event_name == 'push' && github.head_ref == 'main' run: | cd build ./benchmarks --benchmark_out=bench.json --benchmark_out_format=json # 上传 bench.json 到 artifact,供性能回归分析 - name: Static analysis run: | cd build scan-build --use-c++ make -j$(nproc) # Clang Static Analyzer

逻辑说明:CI 流水线强制:1) 单元测试全通过(ctest);2) 性能基准(benchmarks)在main分支推送时生成 JSON 报告,供后续 PR 对比是否引入性能退化;3)scan-build运行静态分析,捕获潜在内存错误。书中第 15 章提供clang-tidy规则集,专门检查算法代码(如modernize-use-auto,cppcoreguidelines-pro-bounds-array-to-pointer-decay)。

参数说明:ctest -V输出详细日志,便于排查失败;--benchmark_out_format=json生成机器可读报告;scan-build是 Clang 自带工具,无需额外安装。

我写第一版stack_introsort时,在 ARM Cortex-M3 上跑std::vector版本触发了 HardFault,花了三天用objdump对比汇编才定位到malloc调用。从此养成了习惯:任何算法代码提交前,必跑三件事——static_assert编译检查、valgrind内存扫描、benchmark性能基线。这三把尺子不会让你写出“最炫”的算法,但能确保你写的每一行 C++,在客户的产线上、在深夜的报警电话里、在同事接手的那一刻,都稳如磐石。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:41:04

408真题解析:DMA与外存磁道扇区计算全拆解

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

作者头像 李华
网站建设 2026/9/27 1:41:03

小麦杂草目标检测实战:VOC标签校验与YOLO格式转换避坑指南

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

作者头像 李华
网站建设 2026/9/27 1:40:58

EEG电极位置与大脑分区:从10-20系统到eeg去噪的完整指南

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

作者头像 李华
网站建设 2026/9/27 1:40:06

SpringBoot+Vue网上书店源码:毕业设计实战与避坑指南

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

作者头像 李华