导读:本期聚焦于星河创作的《C++中如何使用std::unique_copy进行去重拷贝?》,敬请观看详情。std::unique_copy 并不是将容器中所有重复元素一次性删除,而是只压缩相邻的重复项。换句话说,如果原始数据没有排序,算法只会移除连续出现的那部分重复值,非相邻的相同元素依然会被保留。这一特性让开发者容易对去重拷贝产生误解。要正确使用它,需要理解其内部逻辑:算法从头到尾遍历输入序列,将每个元素与前一个已输出元素比较,若不相同则写入目标容器,否则跳过。该过程的时间复杂度为 O(n),空间消耗取决于输出容器。使用前需要包含 algorithm 和 iterator 头文件,函数签名支持首尾迭代器、输出迭代器以及可选的二元谓词。配合 sort 或自定义比较规则,能实现稳定且高效的去重拷贝。本文将通过实际代码演示基本用法、排序配合、自定义谓词以及性能与常见问题。

在 C++ 标准库中,std::unique_copy 定义于 <algorithm> 头文件,用于将输入序列中的连续重复元素压缩后拷贝到输出序列。它和 std::unique 同样遵循去除相邻重复项的原则,但不会原地修改容器,而是把去重后的结果写入新的目标范围。调用前需要包含 <algorithm> 和 <iterator>,后者提供 std::back_inserter 等输出迭代器适配器。函数的基本签名如下:template<class InputIt, class OutputIt> OutputIt unique_copy(InputIt first, InputIt last, OutputIt d_first); 此外还有一个重载版本接受二元谓词,用于自定义相等的判断条件。

C++中如何使用std::unique_copy进行去重拷贝?

一、std::unique_copy 的基本用法

先从最简单的整型容器开始。假设有一个 std::vector<int>,其中包含连续重复的元素,我们希望把相邻重复项去掉后放入新的容器。由于输出容器在调用前可能为空,适配器 std::back_inserter 会在每次写入时自动调用 push_back,避免手动预分配容量。下面是一段完整的示例代码。

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

int main() {
    std::vector<int> data{1, 2, 2, 3, 3, 3, 4, 2, 2};
    std::vector<int> result;

    std::unique_copy(data.begin(), data.end(), std::back_inserter(result));

    for (int v : result) {
        std::cout << v << ' ';
    }
    return 0;
}

这段程序会输出 1 2 3 4 2。可以看到,开头的连续两个 2 被压缩为一个,连续三个 3 被压缩为一个,但最后的两个 2 因为在 4 之后重新出现,且彼此相邻,所以被保留了一个。这说明 std::unique_copy 不会检查整个序列中是否存在相同值,它只比较当前元素与上一个已经写入输出的元素。如果两者不同,就写入;如果相同,就跳过。

如果不使用 std::back_inserter,而是传入了普通输出迭代器,就必须保证目标范围有足够的空间,否则写入会越界。例如可以先把目标容器 resize 到输入容器的大小,再调用算法。算法执行完毕后会返回一个输出迭代器,指向最后一个写入元素的下一个位置,可以用这个返回值计算出最终有效长度,并对容器进行 erase 收缩。

从实际效果看,std::unique_copy 非常适合那些不能原地修改的数据源,比如只读的输入区间、共享数据或者需要保留原始顺序副本的场景。相比手动写循环去重,它更加简洁,也能更好地表达意图。

二、和排序配合实现真正意义上的去重

如果业务需求是删除所有重复值,而不是只删除连续重复值,那么就必须先让相同元素相邻。最直接的做法是调用 std::sort 对输入序列排序,再使用 std::unique_copy 进行拷贝去重。下面示例展示了如何把一组乱序整数变成不重复的有序结果。

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

int main() {
    std::vector<int> data{5, 1, 3, 1, 4, 3, 5, 2};
    std::vector<int> result;

    std::sort(data.begin(), data.end());
    std::unique_copy(data.begin(), data.end(), std::back_inserter(result));

    for (int v : result) {
        std::cout << v << ' ';
    }
    return 0;
}

运行后会得到 1 2 3 4 5,所有重复项都被清除。这种组合的时间复杂度主要来自排序,为 O(n log n),std::unique_copy 本身只需线性时间 O(n)。如果原始顺序不重要,并且后续处理依赖有序性,这是非常高效的方案。

与之相比,std::unique 会返回一个新的逻辑末尾迭代器,之后还需要调用容器的 erase 才能真正删除多余元素。而 std::unique_copy 不修改原容器,天然适合把去重结果写入新容器或者输出流。比如读取一个已经排好序的日志文件时,可以直接将 std::istream_iterator 作为输入,将 std::ostream_iterator 作为输出,中间经过 std::unique_copy 完成连续重复行的过滤,代码会非常紧凑。

当然,如果需要保留元素首次出现的原始顺序,同时又要求全局去重,那么排序加 std::unique_copy 并不合适。此时可以借助 std::unordered_set 记录已经出现过的元素,但要注意无序容器的哈希开销和内存占用。选择哪种方案,需要根据数据规模、元素类型和顺序要求综合判断。

三、自定义去重规则

默认情况下,std::unique_copy 使用 operator== 判断两个元素是否相等。对于字符串去重,如果希望忽略大小写,就可以传入一个自定义二元谓词。谓词应当返回 true 表示两个元素被视为相等。需要注意的是,输入序列必须已经按照相同规则分组,否则非相邻的等价元素仍然无法被合并。下面代码先按忽略大小写排序,再按相同规则去重。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <iterator>
#include <cctype>

int main() {
    std::vector<std::string> words{"Apple", "apple", "banana", "Banana", "cherry"};

    auto equal_ignore_case = [](const std::string& a, const std::string& b) {
        return std::equal(a.begin(), a.end(), b.begin(), b.end(),
            [](unsigned char x, unsigned char y) {
                return std::tolower(x) == std::tolower(y);
            });
    };

    auto less_ignore_case = [](const std::string& a, const std::string& b) {
        return std::lexicographical_compare(
            a.begin(), a.end(), b.begin(), b.end(),
            [](unsigned char x, unsigned char y) {
                return std::tolower(x) < std::tolower(y);
            });
    };

    std::sort(words.begin(), words.end(), less_ignore_case);

    std::vector<std::string> result;
    std::unique_copy(words.begin(), words.end(), std::back_inserter(result), equal_ignore_case);

    for (const auto& w : result) {
        std::cout << w << '\n';
    }
    return 0;
}

上述代码执行后,Apple 和 apple 会被视为同一字符串,Banana 和 banana 也会被合并,最终输出只包含 Apple、banana、cherry。谓词内部使用了 std::equal 和 std::tolower 逐个字符比较,这样可以避免直接修改原字符串的大小写形式。

自定义谓词并不局限于字符串。任何一个结构体或类,只要能够提取出一个用于比较的键,都可以按照该键去重。比如有一个订单列表,希望按照订单号去重,就可以在排序和去重时同时比较 order_id。但必须注意,排序使用的比较规则和 std::unique_copy 使用的相等规则要保持一致。若排序按订单号升序,而去重按客户名判断相等,相邻元素可能并不等价,最终结果会不符合预期。

另外,谓词如果过于复杂,比如需要访问外部状态或做大量计算,可能会降低算法性能。对于轻量级比较,lambda 表达式或函数对象通常足够;对于重负载场景,可以考虑提前预处理出标准化键,再基于键值进行排序和去重,这样能避免在每次比较中重复执行昂贵操作。

四、复杂度、注意事项与常见误区

std::unique_copy 的性能非常稳定:它最多进行 last - first - 1 次比较,时间复杂度为 O(n)。在已知序列已经按分组规则排列好的前提下,这是最直接的去重拷贝方式。它不像基于哈希的方式需要额外构建桶结构,也不像红黑树那样需要维护节点平衡。

最常见的误区是以为它能够全局去重。比如把 {1, 2, 1} 直接交给 std::unique_copy,会得到 {1, 2, 1},因为第二个 1 虽然和第一个 1 相等,但中间隔着 2,它们并不是连续重复项。要解决这个问题,要么先排序,要么根据需求改用基于哈希集合的辅助过滤。

另一个常见问题与输出迭代器有关。如果目标容器没有预先分配空间,又使用了 std::back_inserter,不会出现问题;但如果使用 result.begin() 作为输出迭代器,而 result 是空的,程序就会产生未定义行为。建议优先使用 std::back_inserter 或者提前调整目标容器大小。

此外,复制元素时可能会产生较多拷贝开销。对于存储大对象的容器,可以考虑在去重前先将对象移动到一个新容器,或者让输出容器直接保存指针。如果使用 C++11 及之后的移动语义,可以配合 std::move_iterator 减少不必要的拷贝,但需要注意 std::unique_copy 的输入范围如果是普通迭代器,输出时会调用赋值操作,具体是否移动取决于元素类型的赋值运算符实现。

总体而言,std::unique_copy 是 C++ 标准算法库中一个灵活且高效的工具。只要理解它只处理相邻重复项这一核心语义,并在需要全局去重时先排序或选择替代方案,就能在日志处理、数据清洗、流式输出等场景中写出更加可靠的代码。

C++算法std::unique_copy去重拷贝修改时间:2026-09-20 11:07:03

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