std::unordered_set 本身不保持插入顺序,但可配合 vector 实现去重且保序:用 unordered_set 快速判重(平均 o(1)),仅首次出现的单词加入 vector,自然保留原始顺序;需注意标点处理、大小写敏感性及边界情况。

用 std::unordered_set 去重并保持原始顺序
直接用 std::set 或 std::unordered_set 本身不能保留单词首次出现的顺序,但可以配合遍历实现:边检查边插入,只保留第一次出现的单词。关键不是“去重”,而是“去重且不打乱顺序”。
常见错误是先 split 再用 std::set 排序后输出——这会按字典序重排,完全偏离需求。
- 用
std::istringstream按空格切分,比手写find/substr更可靠(自动跳过多空格) - 用
std::unordered_set<:string></:string>快速判断是否见过该单词,平均 O(1) 查找 - 只把没出现过的单词 push 到结果 vector,自然保序
- 注意:标点未剥离,
"hello"和"hello,"被视为不同单词;如需清洗,得额外处理
std::vector<:string> unique_words;
std::unordered_set<:string> seen;
std::istringstream iss("cat dog cat bird dog");
std::string word;
while (iss >> word) {
if (seen.find(word) == seen.end()) {
seen.insert(word);
unique_words.push_back(word);
}
}</:string></:string>
遇到带标点的单词怎么办
如果输入是 "hello, world! hello.",默认切分得到 "hello,"、"world!"、"hello.",它们互不相等,去重失效。
这不是算法问题,是预处理缺失。C++ 没有内置的“按词干去重”,必须显式剥离常见标点。
- 简单方案:对每个
word调用std::isalnum扫描,提取连续字母数字子串(忽略首尾非字母数字字符) - 更稳做法:用
std::regex_replace(word, std::regex(R"([^\w])"), ""),但性能差,小文本可用 - 别直接用
std::tolower处理整个 string——它不支持 locale,对非 ASCII 字符(如 `é`)可能出错 - 若需大小写不敏感去重,建议先统一转小写再塞入
seen,但注意std::tolower要逐字符调用并 cast 成unsigned char
std::unique 为什么不能直接用
std::unique 只移除**相邻重复项**,不是全局去重。对未排序的 vector 调用它,"cat dog cat" 中第二个 "cat" 不与第一个相邻,不会被删。
有人试过先 sort 再 unique,结果顺序全乱,还得额外记录原始索引——复杂度和出错概率都上升,纯属绕路。
-
std::unique的适用场景是已排序或天然相邻重复的数据流,比如日志里连续多行相同状态 - 它返回的是新逻辑终点迭代器,不自动缩容 vector,常有人忘了跟
erase配合,导致“看似删了实则还在” - 它的比较函数只接受二元谓词,无法接入外部哈希表,做不到“查全局历史”
性能敏感时要注意什么
单次处理几千个单词,怎么写都够快;但若在循环里高频调用(比如解析每行日志),构造 std::unordered_set 和 std::istringstream 的开销就值得抠。
- 复用
std::istringstream对象,调用iss.clear()+iss.str(new_str),避免反复构造 -
std::unordered_set提前reserve(估计单词数),减少 rehash 次数 - 如果确定单词全是 ASCII 且长度 ≤ 16,可考虑用
std::string_view配合自定义哈希(避免每次构造 string) - 极端情况(如嵌入式环境)禁用异常和 RTTI 后,
std::unordered_set的 allocator 行为可能变化,建议单元测试覆盖空输入和超长单词
最易被忽略的是边界:空字符串、全空格输入、连续制表符或换行符——std::istringstream 会跳过所有空白,但若整串只有空白,while (iss >> word) 一次都不进,结果为空 vector,这是正确行为,不是 bug。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











