应使用 std::unordered_map 统计字符频次并按原字符串顺序遍历查找首个出现一次的字符,避免依赖 map 的哈希顺序;对扩展 ascii 需转 unsigned char 防负值越界,utf-8 字符串需先解码为 code point 才能正确处理。

用 std::unordered_map 统计频次再遍历
最直接的做法是两趟扫描:第一趟记录每个字符出现次数,第二趟从头找第一个频次为 1 的字符。关键在于用 std::unordered_map<char int></char> 做 O(1) 插入和查询,避免嵌套循环导致 O(n²)。
常见错误是把第二趟遍历写成对 map 迭代(比如 for (auto& p : freq)),这会按哈希顺序而非字符串原始顺序遍历,结果不可靠。
- 必须用原字符串下标或迭代器顺序遍历,不能依赖 map 的遍历顺序
- 注意
char有符号性问题:如果字符串含扩展 ASCII(如 0x80–0xFF),在某些平台char默认为 signed,可能作为负数索引 map 出错;稳妥做法是转成unsigned char再转char或直接用int键 - 空字符串或全重复时返回什么?C++ 没强制约定,建议明确返回
'\0'或抛异常,别让调用方猜
用 std::vector<int></int> 代替 map(仅限 ASCII)
如果确定输入只有 ASCII 字符(0–127),用长度 256 的 std::vector<int></int> 当计数数组,比 unordered_map 更快、更省内存,且无哈希开销。
示例逻辑:
std::vector<int> count(256, 0);
for (char c : s) count[static_cast<unsigned char>(c)]++;
for (char c : s) {
if (count[static_cast<unsigned char>(c)] == 1) return c;
}
return '\0';
</unsigned></unsigned></int>
容易踩的坑:
- 忘记
static_cast<unsigned char></unsigned>—— 直接用char作下标,遇到负值会越界 - 初始化大小写错:写成
vector<int>(128)</int>会漏掉高位 ASCII(如 DEL、© 等),实际需 256 - 误以为 UTF-8 单字节 = ASCII —— 如果字符串含 UTF-8 多字节字符,此法完全失效
单趟扫描 + 记录首次位置(适合面试优化)
想只遍历一次?可以边扫边记每个字符第一次出现的位置,同时统计次数。第二次不需要再扫字符串,而是扫一个固定大小的结构(比如 256 元素数组),挑出次数为 1 且位置最小的那个。
但注意:这个“单趟”只是减少字符串访问次数,内部仍要查 256 个槽位,对短字符串反而不如朴素两趟快。
- 需要两个辅助数组:
first_pos[256](初始设为 -1)和count[256] - 更新逻辑:若
first_pos[c] == -1,则设为当前索引;无论是否首次,都count[c]++ - 最后遍历
first_pos找所有count[i] == 1中first_pos[i]最小的对应字符 —— 注意跳过first_pos[i] == -1的项
遇到中文或 UTF-8 字符串怎么办
C++ 标准库字符串 std::string 存的是字节,不是字符。所谓“中文字符”在 UTF-8 下占 3 字节,s[i] 取出来的是单个字节,不是完整汉字。
这意味着上面所有基于 char 的方案,对 UTF-8 字符串都会错判——把一个汉字拆成 3 个不同“字符”处理。
- 真要支持 Unicode,得先用 ICU、utf8cpp 或 C++20 的
<charconv></charconv>/std::u8string解码成 code point,再统计 - 简单场景下,可约定输入为 Latin-1 或 UTF-32(即
std::u32string),这时每个char32_t对应一个 Unicode 字符,原方法稍改类型即可复用 - 别试图用
std::wstring+mbstowcs混搭,Windows 和 Linux 的宽字符编码不一致,移植性极差
实际项目里,如果协议明确是 UTF-8 且需处理中文,优先确认是否真需要“字符级”去重——有时业务上“字节唯一”反而更合理,比如日志 ID 或 base64 片段。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











