C++ 关联容器:如何移除重复元素?
一、什么是重复元素?
移除重复元素只有对包含“multi
”的四种关联容器有意义。因为其他容器永远没有重复元素。
对于 multimap
和 unordered_multimap
,重复元素的概念可以有多种含义:两个元素可能具有相同的键,也可能具有相同的键和值。
由于具有相同键的元素在容器中没有特定的顺序,无法在 O(n)
时间内移除 (键,值) 重复元素,因为它们可能不会相邻。因此,这里暂时不会考虑这种情况。这里只关注键来确定两个元素是否为重复元素。
对于集合,由于键和值本身就是一个,所以没有歧义。
注意,在 C++11 之前,是不知道最终保留哪个重复元素的。一般来说它将是迭代过程中遇到的第一个元素,但由于它们没有特定的顺序,因此意义不大。在 C++11 中,插入操作将元素添加到包含等效键的范围的上界。
此外,重复键在 multimap
和 unordered_multimap
之间含义不同:前者使用等价性(使用“小于”语义),而后者使用相等性(使用“等于”语义)。这种差异也适用于 multiset
和 unordered_multiset
。
因此,两个元素是“重复元素”可能有多种含义。所以,可以将其封装在一个比较策略中:DuplicatePolicy
,它接受两个元素并返回一个布尔值,指示它们是否为重复元素。
在所有情况下,思路与根据谓词移除元素时相同:遍历集合并移除重复元素,同时注意不要使迭代器失效。
二、遍历算法
实现示例:
template<typename AssociativeContainer, typename DuplicatePolicy>
void unique(AssociativeContainer& container, DuplicatePolicy areDuplicates)
{
if (container.size() > 1) {
auto it = begin(container);
auto previousIt = it;
++it;
while (it != end(container)) {
if (areDuplicates(*previousIt, *it)) {
it = container.erase(it);
} else {
previousIt = it;
++it;
}
}
}
}
接下来解释上面的代码原理。
判断是否需要移除重复项:
if (container.size() > 1)
该算法将同时考虑两个连续的迭代器,以进行比较。只有在容器至少包含一个元素时才能这样做。事实上,如果它没有至少两个元素,则根本没有重复元素需要移除。
auto it = begin(container);
auto previousIt = it;
++it;
这里使 it
指向容器的第二个元素,使 previousIt
指向第一个元素。
while (it != end(container))
it
是两个迭代器中领先的迭代器,循环会一直继续,直到它到达容器的末尾。
if (areDuplicates(*previousIt, *it)) {
it = container.erase(it);
} else {
previousIt = it;
++it;
}
这种结构是为了避免迭代器失效。请注意,当元素与前一个元素不等价时,将继续遍历容器,并将前一个元素移到下一个位置。
三、如何实现移除策略
可以让客户端代码通过传递一个描述如何识别两个重复元素的 lambda 来调用 unique
。但这会带来几个问题:
- 它会使
unique
的每个调用点都包含低级且冗余的信息, - 存在 lambda 错误的风险,尤其是在容器具有自定义比较器的情况下。
为了解决这个问题,可以提供默认值,这些默认值将对应于各种情况。
3.1、std::multimap
和 std::multiset
先从非哈希multi
容器开始,即 std::multimap
和 std::multiset
。它们都提供了一个名为 value_comp
的方法,该方法返回一个比较两个元素的键的函数。
看起来是比较值的函数,对吧?事实上,与它的名字相反,value_comp
对于映射来说并不比较值。它只比较键。很简单的道理,因为容器不知道如何比较与键关联的值。该方法被称为 value_comp
是因为它接受值,并比较它们的键。
为了消除 std::multimap
中具有重复键的条目,可以这样实现:
[&container](std::pair<const Key, Value> const& element1,
std::pair<const Key, Value> const& element2) {
return !container.value_comp()(element1, element2) &&
!container.value_comp()(element2, element1);
}
multimap
和 multiset
使用等价性,而不是相等性。这意味着 value_comp
返回一个比较元素在“小于”意义上的函数,而不是“等于”。要检查两个元素是否为重复元素,查看是否没有一个元素小于另一个元素。
因此,std::multimap
的 unique
函数将是:
template<typename Key, typename Value, typename Comparator>
void unique(std::multimap<Key, Value, Comparator>& container)
{
return unique(container, [&container](std::pair<const Key, Value> const& element1,
std::pair<const Key, Value> const& element2)
{
return !container.value_comp()(element1, element2) &&
!container.value_comp()(element2, element1);
});
}
multiset
的函数遵循相同的逻辑:
template<typename Key, typename Comparator>
void unique(std::multiset<Key, Comparator>& container)
{
return unique(container, [&container](Key const& element1,
Key const& element2)
{
return !container.value_comp()(element1, element2) &&
!container.value_comp()(element2, element1);
});
}
3.2、std::unordered_multimap
和 std::unordered_multiset
现在转向哈希multi
容器:std::unordered_multimap
和 std::unordered_multiset
。
记住,为了在一次遍历中有效地从容器中移除重复元素,这些重复元素需要彼此相邻。这样一来算法的时间复杂度是 O(n)。它不会对容器中的每个值执行完全搜索(这将是 O ( n 2 ) O(n^2) O(n2))。
但是 unordered_multimap
和 unordered_multiset
是……无序的!所以它可能不会起作用?其实,它是可以起作用,这得益于这些容器的一个特性:具有相同键的元素在迭代顺序中保证是连续的。这就非常的好,省去了不少麻烦。
此外,这些容器遵循其键的相等性逻辑。即它们的比较函数具有“等于”的语义,而不是“小于”。
它们提供了一个方法来访问它们的比较器:key_eq
,它返回一个比较键的函数。此方法是非哈希容器中 key_comp
的对应方法。但是没有 value_comp
的等价方法。没有 value_eq
可以接受两个元素并比较它们的键。因此,必须使用 key_eq
,并自己将键传递给它。
以下是 std::unordered_multimap
的示例代码:
template<typename Key, typename Value, typename Comparator>
void unique(std::unordered_multimap<Key, Value, Comparator>& container)
{
return unique(container, [&container](std::pair<const Key, Value> const& element1,
std::pair<const Key, Value> const& element2)
{
return container.key_eq()(element1.first, element2.first);
});
}
std::unordered_multiset
的代码遵循相同的逻辑:
template<typename Key, typename Comparator>
void unique(std::unordered_multiset<Key, Comparator>& container)
{
return unique(container, [&container](Key const& element1,
Key const& element2)
{
return container.key_eq()(element1, element2);
});
}
3.3、完整示例代码
#include <set>
#include <map>
#include <unordered_map>
#include <unordered_set>
namespace details
{
template<typename AssociativeContainer, typename DuplicatePolicy>
void unique_associative(AssociativeContainer& container, DuplicatePolicy areDuplicates)
{
if (container.size() > 1)
{
auto it = begin(container);
auto previousIt = it;
++it;
while (it != end(container))
{
if (areDuplicates(*previousIt, *it))
{
it = container.erase(it);
}
else
{
previousIt = it;
++it;
}
}
}
}
}
template<typename Key, typename Value, typename Comparator>
void unique(std::multimap<Key, Value, Comparator>& container)
{
return details::unique_associative(container, [&container](std::pair<const Key, Value> const& element1,
std::pair<const Key, Value> const& element2)
{
return !container.value_comp()(element1, element2) &&
!container.value_comp()(element2, element1);
});
}
template<typename Key, typename Comparator>
void unique(std::multiset<Key, Comparator>& container)
{
return details::unique_associative(container, [&container](Key const& element1,
Key const& element2)
{
return !container.value_comp()(element1, element2) &&
!container.value_comp()(element2, element1);
});
}
template<typename Key, typename Value, typename Comparator>
void unique(std::unordered_multimap<Key, Value, Comparator>& container)
{
return details::unique_associative(container, [&container](std::pair<const Key, Value> const& element1,
std::pair<const Key, Value> const& element2)
{
return container.key_eq()(element1.first, element2.first);
});
}
template<typename Key, typename Comparator>
void unique(std::unordered_multiset<Key, Comparator>& container)
{
return details::unique_associative(container, [&container](Key const& element1,
Key const& element2)
{
return container.key_eq()(element1, element2);
});
}
四、总结
通过本文学习了如何使用 C++ 中的关联容器来移除重复元素。从 multimap
和 multiset
开始,介绍了如何使用 value_comp
方法来判断重复元素,然后展示了如何利用 std::pair
来实现这一目标。讨论了 unordered_multimap
和 unordered_multiset
的情况,介绍了如何利用 key_eq
方法来判断重复元素。提供了完整的示例代码,展示了如何编写一个通用的函数来移除重复元素。