导读:本期聚焦于灯下变量创作的《C++如何用std::sort与std::unique快速实现字符串去重?》,敬请观看详情。同样一个字符串去重需求,用循环加find可能要写几十行判断,而std::sort配合std::unique只需三行核心代码。std::sort负责把相同字符排列到相邻位置,std::unique再通过双指针把所有不重复元素前移,返回新的逻辑结尾迭代器,最后用erase完成物理删除。这样的组合时间复杂度是O(n log n),不依赖额外容器,实现简单且运行效率稳定。但要注意std::unique本身不会改变容器长度,必须配合erase才能真正删除尾部冗余数据。如果把std::unique单独用在未排序的字符串上,只能去掉相邻重复,无法实现全局去重。本文从std::unique底层机制、排序前置条件、多种实现对比以及常见误用几个角度展开,帮你彻底掌握这一组合技巧。

在C++中实现字符串去重,最直接的思路是从前往后扫描,把每个字符与结果串比较,不存在才追加。这样做逻辑清晰,但效率不高,尤其是长字符串场景下会反复执行线性查找。标准库提供了一套更优雅的组合:先调用std::sort对字符串排序,让相同的字符相邻,再调用std::unique把不重复的元素前移,最后用erase真正删除尾部多余元素。这个过程不需要手动循环,代码简洁,时间复杂度也稳定可控。下面从std::unique的底层行为开始分析。

C++如何用std::sort与std::unique快速实现字符串去重?

一、std::unique的底层行为与返回值含义

std::unique并不是真正意义上的删除算法,它更像是一个压缩算法。它遍历一个有序区间,每当发现当前元素与上一个保留元素不同时,就把该元素移动到前面的保留区域中。整个过程使用两个迭代器:一个指向待检查元素,另一个指向下一个可以写入的位置。最终所有不重复元素都被连续地排到区间开头,而尾部剩下的元素值处于未指定状态,容器的实际长度并没有发生改变。

例如字符串经过排序后变成abbbcc,调用std::unique后,区间开头会变成abc,但容器整体长度仍然是6。函数返回的迭代器指向新的逻辑结尾,也就是abc之后的位置。要真正把多余的bbc删掉,必须把这个返回迭代器传递给string的erase成员函数。下面的代码展示了这一过程:

#include <iostream>
#include <string>
#include <algorithm>

int main() {
    std::string s = "abbbcc";
    auto it = std::unique(s.begin(), s.end());
    std::cout << "原始大小: " << s.size() << "\n";
    std::cout << "逻辑去重后内容: " << s.substr(0, it - s.begin()) << "\n";
    std::cout << "容器实际内容: " << s << "\n";
    return 0;
}

运行这段代码会发现,虽然s.size()仍然是6,但前3个字符已经是abc。这种“只压缩不删除”的设计让std::unique可以同时适用于vector、string等连续容器,也避免了频繁调整容器长度带来的开销。理解这一点对于正确使用erase去尾至关重要。

另外,std::unique还可以接受第三个参数作为自定义的相等判断函数。比如对于用户自定义类型、忽略大小写的字符比较,都可以通过传入二元谓词来扩展默认的==判断逻辑。不过对于普通字符串去重,默认版本已经足够简洁。

二、为什么必须先用std::sort排序

std::unique只处理相邻元素,它判断重复的依据是当前元素与上一个已保留元素是否相同。因此如果输入字符串没有排序,重复字符可能分散在不同位置,std::unique就无法发现它们是重复的。比如字符串banana中,字母a出现在第2、第4、第6位,字母n出现在第3、第5位,它们并不相邻。直接调用std::unique再去尾,结果仍然是banana,去重完全无效。

下面这个错误示例展示了直接使用std::unique的后果:

#include <iostream>
#include <string>
#include <algorithm>

int main() {
    std::string s = "banana";
    auto it = std::unique(s.begin(), s.end());
    s.erase(it, s.end());
    std::cout << s << std::endl; // 输出 banana,去重无效
    return 0;
}

要让std::unique发挥全局去重的作用,必须先对字符串排序。排序后相同的字符会聚在一起,std::unique只需一次线性扫描就能把所有重复项压缩掉。完整写法如下:

#include <iostream>
#include <string>
#include <algorithm>

std::string dedupe_sorted(std::string s) {
    std::sort(s.begin(), s.end());
    auto it = std::unique(s.begin(), s.end());
    s.erase(it, s.end());
    return s;
}

int main() {
    std::string input = "banana";
    std::string output = dedupe_sorted(input);
    std::cout << "去重结果: " << output << std::endl; // abn
    return 0;
}

这种先排序再去重的方案会改变原字符串的字符顺序。如果业务要求保持第一次出现的顺序,就不能使用std::sort。但它胜在代码短、无额外内存分配,并且对于只关心去重后字符集合的场景非常合适。排序时间开销为O(n log n),后续unique和erase均为O(n),整体复杂度由排序决定。

三、保持原顺序的替代方案与对比

如果需求是保留字符首次出现的顺序,可以使用std::unordered_set配合一次遍历。每次读到一个字符时,尝试插入集合,插入成功说明该字符第一次出现,将它追加到结果串;插入失败则跳过。这种方法时间复杂度平均为O(n),但需要使用额外的哈希表空间。

对应的实现如下:

#include <iostream>
#include <string>
#include <unordered_set>

std::string dedupe_keep_order(const std::string& s) {
    std::unordered_set<char> seen;
    std::string result;
    for (char c : s) {
        if (seen.insert(c).second) {
            result.push_back(c);
        }
    }
    return result;
}

int main() {
    std::string input = "banana";
    std::cout << dedupe_keep_order(input) << std::endl; // ban
    return 0;
}

三种常见方案在顺序保持、时间复杂度和空间使用上有明显差异。手动循环加find的方法不需要排序,但每插入一个字符都可能要对结果串做一次线性查找,最坏情况退化为O(n²)。排序加std::unique不保留原顺序,时间复杂度稳定,空间无额外消耗。std::unordered_set保留原顺序且平均速度快,但需要额外的哈希表空间。

实际选择时可以用一个简单标准:如果字符串长度较短且对顺序没有要求,直接使用排序加std::unique;如果字符串很长且必须保留原顺序,优先考虑std::unordered_set;如果只使用C++标准库但又不想引入哈希表,可以使用std::set替代,但它的复杂度会上升到O(n log n)。下面表格简要对比了这些方案的差异。

方案是否保持原顺序时间复杂度额外空间
排序 + std::unique否O(n log n)无
unordered_set遍历是平均O(n)O(n)
手动find追加是最坏O(n²)无
set遍历是O(n log n)O(n)

四、工程实践中的注意事项

使用std::unique时最容易犯的错误就是忽略它的返回值。很多人以为调用std::unique(s.begin(), s.end())之后字符串已经变短,后续直接使用s可能读到尾部未定义内容。实际上必须使用返回值配合erase,这一步不能省略。另一个误区是把std::unique作用在未排序的字符串上,只去除了相邻重复,造成表面上的去重,埋下逻辑隐患。

对于需要忽略大小写的字符串去重,可以在排序前把字符串统一转为小写,再执行排序和unique。如果原始大小写仍需要保留,建议先复制一份用于去重判断,或者使用自定义比较函数。不过自定义比较函数只能影响unique的判断逻辑,不能影响排序阶段,因此排序和unique最好使用一致的大小写规则,否则仍然可能出现重复字符不连续的问题。

当处理中文或其他多字节编码字符串时,直接对std::string按char去重会破坏多字节字符边界,产生无效编码。此时应该考虑使用std::wstring或按UTF-8码点拆分后的容器。对于宽字符串,代码结构完全相同,只需要把类型换为std::wstring、头文件换为<string>并注意输出方式即可。std::sort和std::unique作为模板算法,对连续字符容器具有很好的通用性。

性能敏感场景下,可以先通过reserve为结果串预留容量,减少多次push_back带来的内存重分配。对于排序加unique方案,则没有这种必要,因为直接操作原串,不会产生新的字符串副本。如果输入字符串非常大,排序本身可能成为瓶颈,此时可以考虑使用基数排序或频率统计法,但实现复杂度也会随之上升。总体而言,std::sort与std::unique的组合已经足够应对绝大多数常规字符串去重需求。

C++字符串去重std::sortstd::unique修改时间:2026-09-19 00:49:51

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。