突破编程_C++_C++11新特性(unordered_multiset)

1 概述

std::unordered_multiset 是一个无序集合,它允许存储重复的键。其内部实现通常基于哈希表,因此元素的插入、删除和查找操作的平均时间复杂度都是 O(1)。然而,需要注意的是,在最坏的情况下,这些操作的时间复杂度可能会退化到 O(n)。std::unordered_multiset 不保证元素的顺序,因此它适用于那些不需要保持元素顺序,但需要快速进行插入、删除和查找操作的场景。

std::unordered_multiset 与 std::multiset 的主要区别如下:

  • 实现原理:std::unordered_multiset 基于哈希表实现,而 std::multiset 基于平衡二叉搜索树实现。
  • 性能特性:std::unordered_multiset 的插入、删除和查找操作在平均情况下的时间复杂度优于std::multiset,但在最坏情况下可能会退化。而std::multiset的性能则更加稳定。
  • 元素顺序:std::unordered_multiset 不保证元素的顺序,而 std::multiset 则按照键的顺序自动排序元素。

std::unordered_multiset 的实现原理主要基于哈希表。其实现原理如下:

首先,std::unordered_multiset 内部维护一个哈希表,该哈希表通常由一系列的桶(buckets)组成。每个桶实际上是一个链表或其他动态数组结构,用于存储具有相同哈希值的元素。

当向std::unordered_multiset 中插入一个元素时,首先会计算该元素的哈希值。这个哈希值是通过一个哈希函数计算得出的,该函数将元素映射到一个整数范围内。然后,根据这个哈希值,可以确定元素应该存放在哪个桶中。具体来说,哈希值通常会对桶的数量取模运算,以确定具体的桶索引。

如果计算得到的桶已经包含了一些元素,新插入的元素将被添加到该桶对应的链表或动态数组的末尾。如果桶是空的,新元素将作为该桶的第一个元素。由于 std::unordered_multiset 允许存储重复的元素,所以即使两个元素具有相同的哈希值,它们也可以被存储在同一个桶中。

当需要从 std::unordered_multiset 中查找一个元素时,同样会计算该元素的哈希值,并定位到相应的桶。然后,会在该桶的链表或动态数组中遍历,查找具有相同值的元素。由于哈希表的设计使得具有相同哈希值的元素都存储在同一个桶中,因此查找操作通常具有较高的效率。

此外,当 std::unordered_multiset 中的元素数量达到某个阈值时,通常需要进行扩容操作,以维持哈希表的性能。扩容操作会创建一个新的、更大的哈希表,并将原哈希表中的元素重新映射到新的哈希表中。扩容操作通常涉及较高的时间和空间开销,因此在设计哈希表时需要考虑扩容策略的合理性。

2 声明与初始化

2.1 声明

首先,需要包含 <unordered_set> 头文件,然后声明一个 std::unordered_multiset 变量,并指定键和值的类型。

#include <unordered_set>  
  
std::unordered_multiset<Type> setName;

其中 Type 是元素的类型,setName 是该 unordered_multiset 变量名称。

2.2 初始化

初始化 std::unordered_multiset 可以通过多种方式进行,包括默认初始化、使用列表初始化器初始化、复制初始化、移动初始化等。

(1)默认初始化

如果没有提供任何初始化器,std::unordered_multiset 将被默认初始化,这意味着它将不包含任何元素,并且会使用默认的哈希函数和键相等性比较对象。

std::unordered_multiset<std::string> myMultiset; // 默认初始化

(2)使用列表初始化器初始化

可以使用列表初始化器(大括号 {})来初始化 std::unordered_multiset,并直接添加一些元素。

std::unordered_multiset<std::string> myMultiset = {
	{"apple"},
	{"banana"},
	{"apple"} // 注意:允许相同的元素  
};

在这个例子中,myMultiset 初始时包含三个元素,其中具有相同两个元素 “apple”。

(3)复制初始化

复制初始化是通过另一个已存在的 std::unordered_multiset 来创建新的 unordered_multiset。

std::unordered_multiset<int> myMultiset1 = {1, 2, 2, 3, 4, 4, 4};  
std::unordered_multiset<int> myMultiset2(myMultiset1); // 复制初始化 

(4)移动初始化

移动初始化是通过右值引用(通常是临时对象或即将被销毁的对象)来初始化新的 std::unordered_multiset。

std::unordered_multiset<int> myMultiset1 = {1, 2, 2, 3, 4, 4, 4};  
std::unordered_multiset<int> myMultiset2(std::move(myMultiset1)); // 移动初始化  
// 此时 myMultiset1 的状态是未定义的,不应再使用  

在实际应用中,列表初始化通常是创建和初始化 std::unordered_multiset 的最常用方式,因为它既直观又方便。复制初始化和移动初始化在需要基于现有集合创建新集合的场景中很有用,尤其是在进行函数参数传递或返回集合时。需要注意的是,移动初始化之后,原集合(在上面的例子中是 myMultiset1)的状态将变为未定义,不应再使用。

3 插入元素

insert 函数用于向 std::unordered_multiset 中插入一个或多个元素。你可以传递一个单独的元素,或者一个包含多个元素的初始化列表。

(1)插入单个元素

std::unordered_multiset<int> myMultiset;
myMultiset.insert(5);  // 插入一个整数 5  
myMultiset.insert(10); // 插入另一个整数 10

(2)插入多个元素

std::unordered_multiset<int> myMultiset;
myMultiset.insert({15, 20, 25}); // 插入一个初始化列表中的多个元素

(3)插入元素范围

如果有一个元素的范围(比如另一个容器或数组),也可以使用 insert 函数来插入这些元素。

std::unordered_multiset<int> myMultiset;
std::vector<int> vec = {30, 35, 40};  
myMultiset.insert(vec.begin(), vec.end()); // 插入 vector 中的所有元素

4 访问元素

4.1 使用迭代器遍历元素

由于 std::unordered_multiset 是一种容器,因此可以使用迭代器来遍历并访问其中的元素。迭代器类似于指针,可以用来访问容器中的元素。

(1)遍历所有元素

可以使用 begin() 和 end() 成员函数来获取指向容器第一个元素和最后一个元素之后位置的迭代器,然后使用这些迭代器遍历容器中的所有元素。

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};

	// 使用迭代器遍历所有元素  
	for (auto it = myMultiset.begin(); it != myMultiset.end(); ++it) {
		std::cout << *it << std::endl;
	}

	return 0;
}

上面代码的输出为:

apple
apple
banana
orange

这个例子创建了一个 std::unordered_multiset 对象 myMultiset,并使用 begin() 和 end() 获取迭代器来遍历容器中的所有元素。

(2)遍历具有特定键的所有元素

由于 std::unordered_multiset 允许键的重复,可能需要遍历具有特定键的所有元素。这可以通过结合 equal_range 函数和迭代器来实现。

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};

	// 查找键为 "apple" 的所有元素  
	auto range = myMultiset.equal_range("apple");
	for (auto it = range.first; it != range.second; ++it) {
		std::cout << *it << std::endl; // 输出值  
	}

	return 0;
}

上面代码的输出为:

apple
apple

在这个例子中,equal_range 返回一个包含两个迭代器的 pair,第一个迭代器指向第一个键为 “apple” 的元素,第二个迭代器指向第一个键不为 “apple” 的元素。通过遍历这个范围来访问所有键为 “apple” 的元素。

4.2 使用 find 函数查找元素

如果只需要查找具有特定键的第一个元素,可以使用 find 函数。如果找到了匹配的元素,find 会返回一个指向该元素的迭代器;如果没有找到,则返回 end() 迭代器。

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};

	std::string value_to_find = "apple";
	auto it = myMultiset.find(value_to_find);

	if (it != myMultiset.end()) {
		std::cout << "Found " << value_to_find << std::endl;
	}
	else {
		std::cout << "Did not find " << value_to_find << std::endl;
	}

	return 0;
}

这个例子使用 find 函数来查找键为 “apple” 的第一个元素。如果找到了,就输出它的值;否则,输出 “Did not find Apple”。

5 删除元素

5.1 使用 erase 成员函数删除单个元素

erase 成员函数可以用于删除容器中的单个元素。可以通过传递一个指向要删除元素的迭代器来调用它。

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};

	// 假设我们有一个指向要删除元素的迭代器  
	auto it = myMultiset.find("apple"); // 查找键为 "apple" 的一个元素  
	if (it != myMultiset.end()) {
		// 使用 erase 删除找到的元素  
		myMultiset.erase(it);
	}

	// 输出容器内容以验证元素已被删除  
	for (const auto& value : myMultiset) {
		std::cout << value << std::endl;
	}

	return 0;
}

上面代码的输出为:

apple
banana
orange

在这个例子中,首先使用 find 函数找到一个键为 “apple” 的元素的迭代器。然后,检查找到的迭代器是否不等于 end(),以确保找到了有效的元素。最后,调用 erase 并传入迭代器来删除该元素。

5.2 使用 erase 成员函数删除删除一系列元素

erase 成员函数还有一个重载版本,它接受两个迭代器作为参数,用于删除一个范围内的所有元素。

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};
      
    // 查找键为 "apple" 的元素范围  
	auto range = myMultiset.equal_range("apple");

	// 删除范围内的所有元素  
	myMultiset.erase(range.first, range.second);

	// 输出容器内容以验证元素已被删除  
	for (const auto& value : myMultiset) {
		std::cout << value << std::endl;
	}
      
    return 0;  
}

上面代码的输出为:

banana
orange

这个例子使用 equal_range 函数来获取一个包含所有键为 “apple” 的元素的范围。然后,调用 erase 并传入这个范围的开始和结束迭代器来删除所有这些元素。

5.3 使用 clear 成员函数删除所有元素

如果想要删除 std::unordered_multiset 中的所有元素,可以使用 clear 成员函数。这将移除容器中的所有键值对,并将容器的大小变为 0。

std::unordered_multiset<std::string> myMultiset  
// 假设 myMultiset 中已经填充了一些元素  
  
myMultiset.clear(); // 删除所有元素,myMultiset 现在为空

5.4 遍历删除

在 std::unordered_multiset 中遍历并删除元素时,需要特别小心,因为删除操作会使指向已删除元素的迭代器失效。如果试图使用一个已经失效的迭代器,程序可能会崩溃或产生不可预测的行为。

一种常见的策略是使用迭代器从 begin() 到 end() 遍历容器,并在遍历过程中删除满足特定条件的元素。但是,由于直接删除当前迭代器指向的元素会使迭代器失效,需要在删除前获取下一个迭代器的位置。这可以通过递增当前迭代器来完成,然后在递增之前检查它是否指向要删除的元素。

以下是一个示例,展示如何在遍历 std::unordered_multiset 时删除所有键为 “apple” 的元素:

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

int main() 
{
	std::unordered_multiset<std::string> myMultiset = {
		{"apple"},
		{"banana"},
		{"apple"},
		{"orange"}
	};

	// 使用迭代器遍历并删除键为 "apple" 的所有元素  
	for (auto it = myMultiset.begin(); it != myMultiset.end(); ) {
		if (*it == "apple") {
			// 删除元素,并使迭代器指向下一个有效元素  
			it = myMultiset.erase(it);
		}
		else {
			// 如果不删除,则递增迭代器  
			++it;
		}
	}

	// 输出容器内容以验证元素已被删除  
	for (const auto& value : myMultiset) {
		std::cout << value << std::endl;
	}

	return 0;
}

上面代码的输出为:

banana
orange

这个例子使用了一个 for 循环来遍历 myMultiset。在循环内部,首先检查当前迭代器的键是否为 “apple”。如果是,则调用 erase 来删除该元素,并更新迭代器 it 为 erase 返回的下一个有效迭代器。如果不删除当前元素,就递增迭代器以继续遍历。

注意:erase 成员函数返回指向被删除元素之后的那个元素的迭代器。如果 erase 被调用在最后一个元素上,它将返回 end()。这就是为什么循环条件仍然是 it != myMultiset.end(),因为即使删除了当前元素,it 在更新后仍然可能不是 end()。

这种遍历并删除元素的方法在大多数情况下都是安全的,因为它避免了使用已经失效的迭代器。然而,如果容器在遍历过程中被其他线程修改,那么可能会出现数据竞争。在多线程环境中,应确保适当的同步机制来避免这种情况。

6 使用自定义键类型

当使用 std::unordered_multiset 时,可以使用自定义键类型作为存储的元素。为了使用自定义键类型,需要确保自定义类型满足以下要求:

能够被哈希,以便 unordered_multiset 可以快速找到元素。这通常通过为自定义类型提供一个合适的哈希函数来实现。
能够比较相等性,以便 unordered_multiset 可以判断两个元素是否相等。这通常通过为自定义类型重载 operator== 来实现。
下面是一个使用自定义键类型的 std::unordered_multiset 的示例:

首先,定义类型,并提供哈希函数和相等性比较:

#include <iostream>  
#include <unordered_set>  
#include <string>  
#include <functional> // for std::hash  
  
// 自定义类型  
struct Person {  
    std::string name;  
    int age;  
  
    // 重载 operator== 以比较两个 Person 对象是否相等  
    bool operator==(const Person& other) const {  
        return name == other.name && age == other.age;  
    }  
};  
  
// 为 Person 类型提供哈希函数  
struct PersonHash {  
    std::size_t operator()(const Person& p) const {  
        std::size_t h1 = std::hash<std::string>()(p.name);  
        std::size_t h2 = std::hash<int>()(p.age);  
        return h1 ^ (h2 << 1); // 简单的组合两个哈希值的方式  
    }  
};

然后,创建一个 std::unordered_multiset,并使用自定义类型和哈希函数:

int main() 
{
	// 创建 unordered_multiset,使用自定义的 Person 类型和 PersonHash 哈希函数  
	std::unordered_multiset<Person, PersonHash> people;

	// 插入自定义类型的元素  
	people.insert(Person{ "Alice", 30 });
	people.insert(Person{ "Bob", 25 });
	people.insert(Person{ "Alice", 30 }); // 可以插入重复的元素  

	// 访问元素  
	for (const auto& person : people) {
		std::cout << person.name << " " << person.age << std::endl;
	}

	return 0;
}

上面代码的输出为:

Alice 30
Alice 30
Bob 25

这个示例定义了一个 Person 结构体,它包含 name 和 age 两个成员变量。然后重载了 operator== 以比较两个 Person 对象是否相等,并定义了一个 PersonHash 结构体来提供 Person 类型的哈希函数。接下来,使用这些自定义类型和哈希函数创建了一个 std::unordered_multiset<Person, PersonHash> 对象,并向其中插入了几个 Person 对象。最后,使用范围基础的 for循环遍历并访问了容器中的元素。

注意:在哈希函数中组合多个哈希值时,可能需要采用更复杂的技术来避免哈希冲突。上面的这个示例使用了异或(^)操作符和左移(<<)操作符来组合哈希值,但在实际应用中可能需要更稳健的方法。此外,如果 Person 类型的成员变量之一(如 name)已经是一个可以被哈希的类型(如 std::string),则可以直接使用该类型的哈希函数,而无需自己实现整个哈希函数。

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值