用std::string::substr()提取n-gram最直接,需遍历起始位置i∈[0, len−n+1),避免越界;len

用 std::string 切子串是最直接的方式
不需要引入第三方库,C++11 起 std::string::substr() 就能搞定 N-gram 提取。核心逻辑就是遍历起始位置,每次取长度为 n 的子串。
常见错误是越界:当字符串长度 len 小于 n 时,substr(i, n) 在 i 接近末尾时会抛 std::out_of_range,或者静默截断(取决于实现和参数)。必须手动限制循环上界为 len - n + 1。
- 只对有效索引操作:
for (int i = 0; i (注意是 <code>,不是 <code>) - 空字符串或
n == 0需提前返回空容器,否则s.length() - n可能成极大正数(无符号整数下溢) - 若需保留空白符(如分词前的原始字符级 n-gram),别先
trim;若做词级别 n-gram,得先按空格/标点切词,再对vector<string></string>做滑动窗口
处理 Unicode 字符要小心 std::string 的字节陷阱
std::string 存的是字节,不是字符。UTF-8 下一个汉字占 3 字节,直接用 substr() 切可能截断码点,产生乱码或非法序列。
真实场景中,如果你的输入来自文件、网络或用户输入,大概率是 UTF-8 编码。这时候“提取 2-gram”是指 2 个 Unicode 字符,不是 2 个字节。
立即学习“C++免费学习笔记(深入)”;
- 简单但不健壮的做法:用
std::wstring+std::locale转宽字符,再切 —— 但 Windows/Linux 对wchar_t宽度定义不同,跨平台易出问题 - 推荐轻量方案:用
utf8cpp库(头文件仅utf8.h)先解码为std::vector<uint32_t></uint32_t>(即 Unicode 码点),再对这个向量做滑动窗口 - 若确定输入全是 ASCII(比如日志 ID、base64 片段),可跳过这步,直接用
substr
性能关键:避免重复分配和拷贝
提取 10 万字符文本的 3-gram,可能生成数万个 std::string 对象。默认方式每调用一次 substr() 都分配新内存,开销明显。
有两种实用优化路径:
- 用
std::string_view(C++17)代替std::string存结果:所有 n-gram 共享原字符串内存,只存偏移和长度,构造零成本 - 如果后续要哈希或查重,直接算
std::hash<:string_view>{}(sv)</:string_view>,比存完整字符串省空间又快 - 若必须用
std::string(比如要传给旧接口),预先reserve()目标容器,避免多次 realloc
边界情况:空格、换行、控制字符怎么算?
N-gram 提取本身不关心语义,但特征工程效果高度依赖预处理策略。同一段文本,“hello world” 的字符 2-gram 是 "he", "el", "ll", "lo", "o ", " w", "wo", "or", "rl", "ld" —— 注意空格也被计入。
是否保留空白,取决于任务目标:
- 做语言模型建模(如拼写纠错):通常保留空格和换行,因它们也是语言的一部分
- 做关键词聚类或分类:常先替换连续空白为单空格,再
erase(remove_if(... isspace ...))彻底去掉所有空白 - 遇到
\0、\r、\t等,substr()照样提取,但后续处理(如写入 CSV)可能出错,建议提前清洗
最易被忽略的是:n-gram 数量随 n 增大衰减极快,且长 n-gram 稀疏性爆炸。实际用时,n 很少超过 5,且常配合 hash trick 或 top-k 截断使用。










