STL(标准模板库)--容器(二)

1. 序列容器

1.1 array

1.2 vector

1.3 deque

1.4 list

1.5 容器中常见的函数成员

2. 容器适配器

2.1 stack

2.2 queue

2.3 priority_queue

3. 堆用法

4. 智能指针

通常用容器保存指针比保存对象更好,而且大多数时候,保存智能指针比原生指针好。下面是一些原因:

  • 在容器中保存指针需要复制指针而不是它所指向的对象。复制指针通常比复制对象快。
  • 在容器中保存指针可以得到多态性。存放元素基类指针的容器也可以保存其派生类型的指针。当要处理有共同基类的任意对象序列时,这种功能是非常有用的。应用这一特性的一个常见示例是展示一个含有直线、曲线和几何形状的对象序列。
  • 对指针容器的内容进行排序的速度要比对对象排序快;因为只需要移动指针,不需要移动对象。
  • 保存智能指针要比保存原生指针安全,因为在对象不再被引用时,自由存储区的对象会被自动删除。这样就不会产生内存泄漏。不指向任何对象的指针默认为 nullptr。

C++里面的四个智能指针:auto_ptr, shared_ptr, weak_ptr, unique_ptr, 其中后三个是C++11支持的,第一个已经瘪C11弃用了。

智能指针的作用是管理一个指针,因为当申请的空间在函数结束时忘记释放内存时,就会造成内存泄漏。使用智能指针可以很大程度上避免这一问题。智能指针本身就是一个类,当超出类的作用域,类就会自动调用析构函数释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间。

4.1 auto_ptr

采用所有权模式。

auto_ptr<string> p1(new string ("I reigned lonely as a cloud.");
auto_ptr<string> p2;
p2 = p1;//auto_ptr不会报错
此时不会报错,p2剥夺了p1的所有权,但是当运行访问p1时将会报错。所以auto_ptr存在潜在的内存崩溃问题!

4.2 unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格 拥有概念,保证同一时间只有一个智能指针可以指向该对象,有效地避免资源泄露。
另外unique_ptr还有更加聪明的地方:当程序试图将一个unque_ptr赋值给另一个时,如果源unique_ptr是一个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做,比如:

	unique_ptr<string> pu1(new string("Hello world"));
	unique_ptr<string> pu2;
	pu2 = pu1;		//not allowed
	unique_ptr<string> pu3;
	pu3 = uique_ptr<string>(new string("You"));	//allowed

4.3 shared_ptr

share_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和器相关资源会在“”最后一个引用被销毁”时释放。shared_ptr使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造。还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源就会被释放。
shared_ptr时为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:
use_count	返回引用计数的个数
unique	返回是否独占所有权(use_count为1)
swap	交换两个shared_ptr对象(即交换所拥有的对象)
reset	放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少
get	返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的。如shared_ptr<int> sp(new int(1));sp与sp.get()是等价的

3.4 weak_ptr

weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或者减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互作用,那么这两个指针的引用计数永远不会下降为0,资源永远不会释放。它是对对象的一种弱作用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

5. 关联容器

5.1 map

序列容器是管理数据的宝贵工具,但对大多数应用程序而言,序列容器不提供方便的数据访问机制。举个简单的示例,当我们用它处理姓名和地址时,在这种场景下,序列容器可能并不能如我们所愿。一种典型的方法是通过名称来寻找地址。如果记录保存在序列容器中,就只能通过搜索得到这些数据。相比而言,map 容器提供了一种更有效的存储和访问数据的方法。

map 容器是关联容器的一种。在关联容器中,对象的位置取决于和它关联的键的值。键可以是基本类型,也可以是类类型。字符串经常被用来作为键,如果想要保存姓名和地址的记录,就可以这么使用。名称通常可能是一个或多个字符串。关联容器中的对象位置的确定取决于容器中的键的类型,而且对于特定容器类型的内部组织方式,不同的 STL 有不同的实现。

map<K,T> 类模板定义在 map 文件头中,它定义了一个保存 T 类型对象的 map,每个 T 类型的对象都有一个关联的 K 类型的键。容器内对象的位置是通过比较键决定的。可以用适当的键值从 map 容器中检索对象。图 1 展示了一个用名称作为键的 map<K,T> 容器,对象是整数值,用来表示年龄。

图 1 表示的是 map<Name,size_t> 类型的容器,其中的 Name 类可以这样定义:

class Name
{
private:
    std::string firstname{}; 
    std::string secondname{};
public:
    Name(std::string first, std::string second) : firstname{first}, secondname{second}{};
    Name()=default;
    bool operator<(const Name& name)const{
        return secondname < name.secondname ||((secondname == name.secondname) && (firstname < name.firstname));
    }
};

为了可以在必要时生成默认的元素,容器保存的对象通常需要定义一个默认的构造函数。当两个 Name 对象的 secondname 不同时,成员函数 operator<() 通过比较 secondname 来确定对象的顺序。如果 secondname 相等,比较结果就由 firstname 决定。string 类定义了 operator<(),因而可以用默认的 less< string > 来比较。

不要因为 map 使用 less< K> 对元素排序就被误导,这些元素并没有被组织成一个简单的有序序列,STL map 容器对元素的组织方式并没有具体要求,但元素一般都会保存在一个平衡二叉树中。容器中的元素被组织成一个平衡二叉树,因而树的高度——根节点和叶节点之间的高度是最低的。如果每个节点的左子树和右子树的高度差不超过 1,那么可以说这棵二叉树就是平衡的。图 2 展示了图 1 所表示的 map 容器可能的平衡二叉树。

5.1.1 map基本操作

创建和初始化
map 类模板有 4 个类型参数,但一般只需要指定前两个模板参数的值。第 1 个是键的类型,第 2 个是所保存对象的类型,第 3 个和第 4 个模板参数分别定义了用来比较键的函数对象的类型以及为 map 分配内存的对象的类型。最后两个参数有默认值。
map<> 容器类的默认构造函数会创建一个空的 map 容器。例如,可以创建一个这样的容器,size_t 类型的值表示年龄,作为它保存的值,string 类型的值表示名称,作为它的键:

std::map<std::string, size_t> people;
std::map<std::string, size_t> people2{{"Ann", 25}, {"Bill", 46},{"Jack", 32},{"Jill", 32}};
//utility 头文件中定义了 make_pair < T1,T2 >() 函数模板,它提供了一种组合 T1 和 T2 类型对象的简单方法。
std::map<std::string,size_t> people{std::make_pair("Ann",25),std::make_pair("Bill", 46),std::make_pair("Jack", 32),std::make_pair("Jill", 32)};
std::map<std::string, size_t> personnel {people}; // Duplicate people map
std::map<std::string, size_t> personnel {++std::begin(people),std::end(people)};

插入数据
map<K,T> 容器的成员函数 insert() 有多个版本,它们可以在 map 中插入一个或多个 pair<const K,T> 对象。只能插入 map 中不存在的元素。下面这个代码片段展示了如何插入单个元素:

std::map<std:: string,size_t> people {std::make_pair ("Ann",25),std::make_pair("Bill",46) , std::make_pair ("Jack",32), std::make_pair("Jill",32)};
auto pr = std::make_pair("Fred",22); //Create a pair element and insert it
auto ret_pr = people.insert(pr);
std::cout << ret_pr.first->first << " "<< ret_pr.first->second<< "" << std:: boolalpha <<ret_pr.second << "\n"; // Fred 22 true

构造元素
map 的成员函数 emplace_hint() 和 emplace() 生成元素的方式在本质上是一样的,除了必须为前者提供一个指示元素生成位置的迭代器,作为 emplace_hint() 的第一个参数。例如:

std::map<Name, size_t> people;
auto pr = people.emplace(Name{"Dan","Druff"}, 77);
auto iter = people.emplace_hint (pr.first, Name {"Cal","Cutta"}, 62);

需要注意的是,它和 emplace() 的返回值是不一样的。emplace_hint() 的返回值不是一个 pair 对象,如果新元素被插入,它返回的是指向新元素的迭代器;如果没有插入,返回的是和这个键匹配的现有元素的迭代器,拥有相同的 key 值,如果不是现有元素的话。没有提示可以直接判断是否生成了新元素。唯一的方法是,用 size() 成员函数来获取 map 中对应元素的数量来检查 map 元素增加的数量。例如:

auto pr = people.emplace(Name{"Dan", "Druff"}, 77);
auto count = people.size ();
auto iter = people.emplace_hint (pr.first, Name {"Cal", "Cutta"}, 62);
if(count < people.size ()) std::cout <<"Success!\n";

访问元素
我们已经知道,可以获取 map 容器的开始和结束迭代器以及反向迭代器,它们都可以访问容器中的所有元素。map 的成员函数 at() 返回的是参数键对应的对象。如果这个键不存在,就会拋出 out_of_range 异常。下面展示如何使用这个函数:

#include <iostream>
#include <string>
#include <map>
#include <iomanip>
using namespace std;
class Name
{
private:
	string firstname{};
	string secondname{};
public:
	Name(string first, string second) : firstname{ first }, secondname{ second } {};
	Name() = default;
	bool operator<(const Name& name)const{
		return secondname < name.secondname || ((secondname == name.secondname) && (firstname < name.firstname));
	}
	void getname(const Name& name) {
		cout << "{ " << name.firstname << " , " << name.secondname << " }" ;
	}
};
int main()
{
	map<Name, size_t> people;
	auto pr = people.emplace(Name{ "Dan", "Druff" }, 77);
	Name key;
	try
	{
		key = Name{ "Dan", "Druff" };
		auto value = people.at(key);
		key.getname(key);
		cout << "is aged " << value << std::endl;


		key = Name{ "Don", "Druff" };
		value = people.at(key);
		key.getname(key);
		cout << " is aged " << value << std::endl;
	}
	catch (const std::out_of_range& e)
	{
		std::cerr << e.what() << '\n';
		key.getname(key);
		cout << " was not found." << std::endl;
	}
}

需要在 try 代码块中调用 map 的成员函数 at(),因为抛出的任何未捕获的异常都会导致程序的终止。这段代码获取了 people 容器中的两个对象,它们分别与两个 Name 键关联。如果 map 容器中的内容由执行的前一节中的代码段决定,输出效果如下:

Dan Druff is aged 77
invalid map<K, T> key
Don Druff was not found.

删除元素
map 的成员函数 erase() 可以移除键和参数匹配的元素,然后返回所移除元素的个数,例如:

std::map<std::string, size_t> people {{ "Fred", 45}, {"Joan", 33},{"Jill", 22}};
std::string name{"Joan"};
if(people.erase(name))
    std::cout << name << " was removed." << std::endl;
else
    std::cout << name << " was not found" << std::endl;
//Joan was removed.
auto iter = people.erase(std::begin(people));
if(iter == std::end(people))
    std::cout << "The last element was removed."<< std::endl;
else
    std::cout << "The element preceding " << iter->first << " was removed." << std::endl;
//The element preceding Jill was removed.

当最后一个元素被移除时,这段代码会输出一条消息或被移除元素后面元素的键。也有更高级版本的 erase(),它可以移除两个迭代器参数所定义范围内的元素,例如:

auto iter = people.erase(++std::begin(people), --std::end(people));//Erase all except 1st & last

返回的迭代器指向这段元素中最后一个被删除的元素。如果想删除容器中的所有元素,可以调用成员函数 clear()。

5.1.2 pair and tuple

pair<const K,T>: pair是一种比较简单的模板类型,只有两个public数据成员first和second。

下面有 4 种不同的方式来创建一个 pair 对象:

std::string s1 {"test”}, s2{"that"};
	std::pair<std::string, std::string> my_pair{s1, s2};
	std::pair<std::string, std::string> your_pair{std::string {"test"},std::string {"that"}};
	std::pair<std::string, std::string> his_pair{"test", std::string {"that"}};
	std::pair<std::string, std::string> her_pair{"test", "that"};
make_pair<T1,T2> 函数模板是一个辅助函数,可以生成并返回一个 pair<T1,T2> 对象。 可以如下所示生成先前代码块中的 pair 对象:
auto my_pair = std::make_pair(s1, s2);
auto your_pair = std::make_pair(std::string {"test"},std::string {"that"});
auto his_pair = std::make_pair<std::string, std::string>("test",std::string {"that"});
auto her_pair = std::make_pair<std::string, std::string>("test", "that");
pair 对象也可以复制或移动构造它的成员变量。例如:
std::pair<std::string, std::string> new_pair{my_pair}; // Copy constructor
std::pair<std::string, std::string> old_pair{std::make_pair(std::string{"his"},std::string{"hers"})};

tuple<>: pair 模板泛化,tuple 模板定义在 tuple 头文件中。
生成 tuple 对象的最简单方式是使用定义在 tuple 头文件中的辅助函数 make_tuple()。这个函数可以接受不同类型的任意个数的参数,返回的 tuple 的类型由参数的类型决定。例如:

auto my_tuple = std::make_tuple (Name{"Peter”,"Piper"},42,std::string{"914 626 7890"});

my_tuple 对象是 tuple<Name,int,string> 类型,因为模板类型参数是由 make_tuple() 函数的参数推导出来的。如果提供给 make_tuple() 的第三个参数是一个字符串常量,my_tuple 的类型将是 tuple<Name,int,const*>,这和之前的不同。

tuple 对象的构造函数提供了可能会用到的每一种选择。例如:

std::tuple<std::string, size_t> my_tl;//Default initialization
std:: tuple<Name, std::string> my_t2 {Name {"Andy", "Capp"},std::string{“Programmer”}};
std::tuple<Name,std::string> copy_my_t2{my_t2}; // Copy constructor
std::tuple<std::string, std::string> my_t3 {"this", "that"};// Implicit conversion

tuple 中的对象由默认构造函数用默认值初始化。为 my_t2 调用的构造函数将参数移到 tuple 的元素中。下一条语句会调用拷贝构造函数来生成 tuple,在最后一个构造函数调用中,将参数隐式转换为 string 类型并生成了一个 tuple 元素。

也可以用 pair 对象构造 tuple 对象,pair 可以是左值,也可以是右值。显然,tuple 只能有两个元素。下面有两个示例:

auto the_pair = std::make_pair("these","those");
std::tuple<std::string, std::string> my_t4 {the_pair}; 
std::tuple<std::string, std::string> my_t5 {std::pair <std::string, std::string > { "this", "that"}};

全局的 tie<>() 函数模板定义在 tuple 头文件中,它提供了另一种访问 tuple 元素的方式。这个函数可以把 tuple 中的元素值转换为可以绑定到 tie<>() 的左值集合。tie<>() 的模板类型参数是从函数参数中推导的。例如:

auto my_tuple = std::make_tuple(Name{"Peter","Piper"}, 42, std::string{"914 626 7890"});
Name name{};
size_t age{};
std::string phone{};
std::tie(name, age, phone) = my_tuple;
//只保存 my_tuple 中 name 和 phone 的值:
//ignore 定义在 tuple 中,它被用来标记 tie() 函数中要被忽略的值。
std::tie(name, std::ignore,phone) = my_tuple;

5.1.3 multimap

multimap 容器保存的是有序的键/值对,但它可以保存重复的元素。multimap 中会出现具有相同键的元素序列,它们会被添加到容器中。multimap 和 map 有相同范围的构造函数,默认的比较键的函数是 less< K>()。

std::multimap<string, string〉 pets; // Element is pair{pet_type, pet_name}
auto iter = pets.insert (std::pair<string, string>{string{"dog"}, string{"Fang"}});
iter = pets.insert(iter, std::make_pair("dog", "Spot")); // Insert Spot before Fang
pets.insert(std::make_pair("dog", "Rover"));// Inserts Rover after Fang
pets.insert (std::make_pair ("cat", "Korky"));// Inserts Korky before all dogs
pets.insert ({{ "rat", "Roland"}, {"pig", "Pinky" }, {"pig", "Perky"}});//Inserts list elements

和 map —样,multimap 的成员函数 emplace() 可以在容器的适当位置构造元素。在插入具有相同键的元素时,可以使用 multimap 的成员函数 emplace_hint(),可以通过为这个函数提供一个迭代器形式的提示符来控制元素的生成位置:

auto iter = pets.emplace("rabbit”,"Flopsy");
iter = pets.emplace_hint (iter, "rabbit", "Mopsy");// Create preceding Flopsy

multimap 的成员函数 find() 可以返回一个键和参数匹配的元素的迭代器。例如:

std::multimap<std::string, size_t> people {{"Ann",25},{"Bill", 46}, {"Jack", 77}, {"Jack", 32},{"Jill", 32}, {"Ann", 35} };
std::string name {"Bill"};
auto iter = people.find(name);
if (iter ! = std::end (people))
    std::cout << name << " is " << iter->second << std::endl;
iter = people.find ("Ann");
if (iter != std::end(people))
    std::cout << iter->first << " is " << iter->second <<std::endl;

multimap 成员函数 equal_range() 就可以做到这一点。它会返回一个封装了两个迭代器的 pair 对象,这两个迭代器所确定范围内的元素的键和参数值相等。例如:

auto pr = people.equal_range("Ann");
if(pr.first != std::end(people))
{
    for (auto iter = pr.first ; iter != pr.second; ++iter)
        std:cout << iter->first << " is " << iter->second << std::endl;
}

equal_range() 的参数可以是和键同类型的对象,或是不同类型的但可以和键比较的对象。返回的 pair 对象的成员变量 first 是一个迭代器,它指向第一个大于等于参数的元素;如果键和参数相等的元素存在的话,它是第一个键和参数相同的元素。如果键不存在,pair 的成员变量 first 就是容器的结束迭代器,所以应该总是对它们进行捡查。

通过调用 multimap 的成员函数 count() 可以知道有多少个元素的键和给定的键相同。

auto n = people.count("Jack"); // Returns 2

multimap 的成员函数 erase() 有 3 个版本:

  1. 以待删除兀素的迭代器作为参数,这个函数没有返回值;
  2. 以一个键作为参数,它会删除容器中所有含这个键的元素,返回容器中被移除元素的个数;
  3. 接受两个迭代器参数,它们指定了容器中的一段元素,这个范围内的所有元素都会被删除,这个函数返回的迭代器指向最后一个被删除元素的后一个位置。

5.1.4 hash

哈希值可能是表达式k%m产生的。

显然,这种方法最多允许有 m 个不同的哈希值,值的范围为 0 到 m-1。可以很容易地看到哪里会产生重复的哈希值。值为 k+m、k+2*m 的键会有重复的哈希值,m 值的选择对于减少重复哈希值的出现至关重要,而且可以保证值是均匀分布的。如果 m 是 2 的幂,也就是 2n,哈希值的最小位为 k 的 n 位。这显然不是一个好的结果,因为 k 的大多数位都没有影响到哈希值;理想情况下,键的所有数位应该都可以影响哈希结果。m 通常是一个质数,因为它可以使哈希值更加均匀地分布在这个范围内。

另一种更好的计算哈希值的方式是选择一个常量 a,将它和 k 相乘,用 ak 除以整数 m 来计算它的余数,然后从 (ak)%m 的结果中选择一个长度值作为哈希值。显然 a 和 m 的选择是非常重要的。对于 32 位的计算机来说,m 通常选为 232。乘数 a 是和 m 相近的质数,这就意味着 a 和 m 除了 1 之外没有其他的公共因子。此外,a 的二进制表示中头部和尾部不能为 0,否则会和其他头部有 0 或尾部有 0 的键值产生碰撞。基于这些原因,这个算法也被叫作乘法哈希。

也有几个专门哈希字符串的算法。其中一个将字符串看作一定个数的单词,使用像乘法算法这样的方法来计算第一个单词的哈希值,然后加上下一个单词,再计算它的哈希值,重复这个过程,直到计算出所有单词最后的哈希值。

生成哈希值的函数
functional 头文件中定义了无序关联容器使用的特例化 hash< K> 模板。hash< K> 模板定义了可以从 K 类型的对象生成哈希值的函数对象的类型。hash< K> 实例的成员函数 operator() 接受 K 类型的单个参数,然后返回 size_t 类型的哈希值。对于基本类型和指针类型,也定义了特例化的 hash< K> 模板。

hash< K> 模板专用的算法取决于实现,需要满足一些具体的要求。这些要求如下:

  • 不能拋出异常
  • 对于相等的键必须产生相等的哈希值
  • 对于不相等的键产生碰撞的可能性必须最小接近 size_t 最大值的倒数

注意,相等键生成相等的哈希值只适用于单次执行。这也就意味着,在不同的场合允许给定的键可以生成不同的哈希值。这就使我们可以在哈希算法中使用随机数,当对密码进行哈希时,这是我们所希望使用的。
下面是一个用 hash 生成整数的哈希值的示例:

std::hash<int> hash_int;// Function object to hash int
std::vector<int> {-5, -2, 2, 5, 10};
std::transform(std::begin(n), std::end(n), std::ostream_iterator<size_t> (std:: cout," "),hash_int);

这里使用 transform() 算法来哈希 vector 中的元素。transform() 参数中的前两个迭代器指定了被操作元素的范围,第三个参数是一个指定输出地址的迭代器,这里是一个 ostream 迭代器,最后一个参数是应用到范围元素上的函数对象 hash< int>。

5.1.5 unordered map

unordered_map 包含的是有唯一键的键/值对元素。容器中的元素不是有序的。元素的位置由键的哈希值确定,因而必须有一个适用于键类型的哈希函数。如果用类对象作为键,需要为它定义一个实现了哈希函数的函数对象。如果键是 STL 提供的类型,通过特例化 hash,容器可以生成这种键对应的哈希函数。

一个给定的哈希值会选择特定的格子,因为哈希值可能的个数几乎可以肯定会大于格子的个数,两个不同的哈希值可能会映射到同一个格子上。因此,不同键会产生相同的哈希值,会产生碰撞,而且两个不同的哈希值选择相同的格子也会导致碰撞的产生。

下面的一些参数可以影响元素存储的管理:

  1. 容器中格子的个数有一个默认值,但也可以定指定初始个数。
  2. 载入因子是每个格子平均保存的元素的个数。这个值等于容器中元素个数除以格子的个数。

最大载入因子,默认是 1.0,但也可以修改。这是载入因子的上限。当容器达到最大载入因子时,容器会为格子分配更多的空间,这通常也会对容器中的元素重新进行哈希。

unordered_map 必须能够比较元素是否相等。当容器中有相同的键时,对从包含多个元素的格子中检索到的元素进行确认和选择很有必要。容器默认会使用定义在 functional 头文件中的equal_to< K> 模板。它会用 == 运算符来比较元素,所以当键相等时,容器会认为它们是相同的,这一点和 map 容器不同,map 容器使用的是等价。如果使用的键是没有实现 operator=() 的类类型,那就必须提供一个函数对象来比较键。

初始化

std::unordered_map<std::string, size_t> people {{"Jan",44}, {"Jim", 33}, {"Joe", 99}}; // Name,age
//在构造函数中指定应该分配的格子的个数
std::unordered_map<std::string,size_t> people {{ { "Jan", 44}, {"Jim", 33}, {"Joe", 99}}, 10};
std::vector<std::pair<string, size_t>>folks {{ "Jan",44}, {"Jim", 33}, {"Joe", 99},{"Dan", 22},{"Ann", 55}, {"Don", 77}};
std::unordered_map<string, size_t> neighbors {std::begin(folks), std::end(folks) , 500};

插入元素
unordered_map 容器的成员函数 insert() 提供的能力和 map 谷器的这个函数相同。可以通过复制或移动来插入一个元素,可以使用也可以不使用提示符来指明插入的位置。可以插入初始化列表中指定的元素或由两个迭代器指定范围内的元素。

std:: unordered_map<std:: string, size_t> people { {"Jim", 33}, { "Joe", 99}};// Name,age
//调用 people 的成员函数 bucket_count() 来获取格子个数;
std::cout <<"people container has " << people.bucket_count()<<" buckets.\n"; // 8 buckets
auto pr = people.insert (std::pair<string, size_t> {"Jan", 44});// Move insert
std:: cout << "Element " << (pr.second ? "was" : "was not") << " inserted." << std::endl;

在任何时候都可以调用成员函数 rehash() 来改变格子的个数:

people.rehash(15); // Make bucket count 15

增加最大载入因子,也就是增加每个格子所包含的元素个数:

people.max_load_factor(1.2*people.max_load_factor()); // 工ncrease max load factor by 20%

访问元素
unordered_map 的迭代器是可以使用的,因此可以用基于范围的 for 循环来访问它的元 素,例如:

for(const auto& person : people)
    std::cout << person.first << " is "<< person.second <<std::endl;

访问 unordered_map 的个别格子及其包含的元素。可以用这个容器的成员函数 begin() 和 end() 的重载版来做到这一点,它们可以返回容器元素的迭代器。格子的索引从 0 开始,可以通过将索引值传给容器的成员函数 begin() 来获取给定位置的格子中第一个元素的迭代器。例如:

auto iter = people.begin(1); // Returns an iterator for the 2nd bucket

将索引值传给容器的成员函数 cbegin() 会返回一个 const 迭代器,它指向位于索引位置的格子中的第一个元素。这个容器的成员函数 end() 和 cend() 也有这样的版本,它们接受一个索引值,分别返回一个迭代器和一个 const 迭代器,它们指向位于指定位置的格子中的最后一个元素的下一个位置。可以输出特定格子中的元素,也就是说,需要使用循环:

size_t index{1};
std::cout <<"The elements in bucket["<< index << "] are:\n";
for(auto iter = people.begin(index); iter != people.end(index); ++iter)
    std::cout <<iter->first << " is " <<iter->second <<std::endl;

unordered_map 的成员函数 bucket_count() 返回的格子个数。bucket_size() 可以返回参数指定的格子中的元素个数。bucket() 返回的是格子的索引值,包含和传入的参数键匹配的元素。可以用不同的方式来组合使用它们。例如:

string key {"May"};
if(people.find(key) != std::end(people))
    std::cout << "The number of elements in the bucket containing " << key << " is "<< people.bucket_size(people.bucket(key)) << std:: endl;

删除元素
调用 unordered_map 的成员函数 erase() 来移除元素。参数可以是标识元素的一个键或是指向它的一个迭代器。当参数是键时,erase() 会返回一个整数,它是移除元素的个数,所以 0 表示没有找到匹配的元素。当参数是迭代器时,返回的迭代器指向被移除元素后的元素。下面是一些示例:

auto n = people.erase ("Jim");// Returns 0 if key not found
auto iter = people.find ("May") ; // Returns end iterator if key not found
if(iter != people.end())
    iter = people.erase (iter) ;// Returns iterator for element after "May"
//Remove all except 1st and last
auto iter = people.erase(++std:rbegin(people),--std:rend(people));

返回的迭代器指向被移除的最后一个元素的下一个位置。

成员函数 clear() 会移除所有的元素。

5.1.6 unordered multimap

unordered_multimap 是一个允许有重复键的无序 map。因此,它支持的操作实际上和 unordered_map 容器是相同的,为了处理多个重复键所做的添加和更改除外。

std:: unordered_multimap<std::string,size_t> people {{"Jim",33}, {"Joe",99}};

可以使用 insert()、emplace()、emplace_hint() 来添加新元素,这和 unordered_multimap 相同,只要参数和容器中的元素类型一致。这些成员函数都会返回一个指向容器中新元素的迭代器;在使用 inserted emplace() 的情况下,unordered_multimap 和 unordered_map 有些不同,unordered_multimap 的这两个函数会返回一个 pair 对象,它用来说明插入是否成功。如果不成功,也是一个迭代器。例如:

auto iter = people.emplace("Jan", 45);
people.insert({"Jan", 44});
people.emplace_hint(iter, "Jan", 46);

unordered_map 支持的成员函数 at() 和 operator 对于 unordered_multimap 来说并不可用,因为潜在的重复键。唯一的选择是使用 find() 和 equal_range() 来访问元素。find() 总会返回它所找到的第一个元素的迭代器,如果找不到这个键,会返回一个结束迭代器。可以以键为参数调用 count() 来发现容器中给定键的元素个数。下面展示实际用法:

std:: string key{"Jan"};
auto n = people.count(key);//Number of elements stored with key
if(n == 1)
    std::cout << key << " is " << people.find(key)->second<<std::endl;
else if (n > 1)
{
    auto pr = people.equal_range (key); // pair of begin & end iterators returned
    while(pr.first != pr.second)
    {
        std::cout << key << " is " << pr.first->second << std::endl;
        ++pr.first; // Increment begin iterator
    }
}

5.2 set

set 是具有共同特征的事物的集合。集合在 STL 中有两个概念,它们都涉及一系列的数学思想。集合可以是由两个迭代器定义的范围内的一系列对象,也可以是一种有特殊特征的容器类型。set 容器是关联容器,其中的对象是对象它们自己的键。

定义 set 的模板有 4 种,其中两种默认使用 less< T> 来对元素排序,另外两种使用哈希值来保存元素。有序 set 的模板定义在 set 头文件中。无序 set 的模板定义在 unordered_set 头文件中。因此有序 set 包含的元素必须支持比较运算,无序 set 中的元素必须支持哈希运算。

定义 set 容器的模板如下:

  • set< T> 容器保存 T 类型的对象,而且保存的对象是唯一的。其中保存的元素是有序的,默认用 less< T> 对象比较。可以用相等、不相等来判断对象是否相同。
  • multiSet< T> 容器和 set< T> 容器保存 T 类型对象的方式相同,但它可以保存重复的对象。
  • unorderd_set< T> 容器保存 T 类型的对象,而且对象是唯一的。元素在容器中的位置由元素的哈希值决定。默认用 equal_to< T> 对象来判断元素是否相
  • unordered_multiset< T> 容器保存 T 类型对象的方式和unorderd_set< T> 相同,但它可以保存重复的对象。

一般来说,当 set 中有大量元素时,在无序 set 上执行的随机插入和检索操作要比有序 set 快。在有 n 个元素的有序 set 中检索元素的时间复杂度是 logn。在无序 set 中检索元素的平均时间复杂度是常量,这和元素的个数无关,尽管实际性能会受元素哈希操作和内部组织效率的影响。

对象在容器中的存放位置取决于有序 set 的比较函数和无序 set 的哈希函数,对于保存同一种对象的不同 set,我们可以使用不同的比较函数或哈希函数。

5.2.1 set基本操作

初始化
set< T> 容器内部元素的组织方式和 map<K,T> 相同,都是平衡二叉树。可以使用初始化列表来初始化 set 容器:

set<int> numbers {8, 7, 6, 5, 4, 3, 2, 1};

默认比较函数是less< int>,因此容器中的元素会自动升序排列。
添加元素
set 中没有实现成员函数 at(),也没有实现 operator 。除了这些操作外,set 容器提供 map 容器所提供的大部分操作。可以使用 insert()、emplace()、emplace_hint() 成员函数来向 set 中添加元素。
insert()插入元素示例:
插入单个元素会返回一个 pair<iterator,bool> 对象。插入单个元素和一个标识,会返回一个迭代器。插入一段元素或一个初始化列表就不会有返回值。当 insert() 的参数是初始化列表时,会用列表中的字符串创建 string 对象。

set<string, greater<string>> words {"one", "two", "three"};
auto pr1 = words.insert("four");
auto pr2 = words.insert ("two") ;
auto iter3 = words.insert(pr.first, "seven");
words.insert ({ "five","six"}) ;
string wrds[] {"eight", "nine", "ten"};
words.insert(wrds.begin() ,wrds.end());

emplace()插入元素示例:
成员函数 emplace() 会返回一个 pair<iterator,bool> 对象,而 emplace_hint() 只返回一个迭代器。前者的参数被直接传入元素的构造函数,用来创建元素。emplace_hint() 的第一个参数是一个迭代器,它指出了元素可能的插入位置,随后的参数会被传入元素的构造函数。

std::set<std::pair<string,string>> names;
auto pr = names.emplace("Lisa", "Carr");
auto iter = names.emplace_hint(pr.first, "Joe", "King");

删除元素
成员函数clear()会删除set中所有元素。成员函数erase()会删除迭代器指定位置的元素或与对象匹配的元素。例如:

set<int> numbers {2, 4, 6, 8, 10, 12, 14};
auto iter = numbers.erase(++std::begin(numbers));
auto n = numbers.erase(12);
n = numbers.erase(13);
numbers.clear();

成员函数erase()可以删除一段元素:

set<int> numbers {2, 4, 6, 8, 10, 12, 14};
auto iter1 = numbers.begin(); // iter1 points to 1st element
advance(iterl, 5); // Points to 6th element-12
auto iter = numbers.erase(++std:rbegin(numbers), iter1);// Remove 2nd to 5th inclusive. iter points to 12

访问元素
set 的成员函数 find() 会返回一个和参数匹配的元素的迭代器。如果对象不在 set 中,会返回一个结束迭代器。例如:

set<string> words {"one", "two","three", "four","five"};
auto iter = words.find ("one") ; // iter points to "one"
iter = words.find(string{"two"});   // iter points to "two"
iter = words.find ("six");   // iter is std:: end (words)

调用成员函数 count() 可以返回指定键所对应的元素个数,返回值通常是 0 或 1,因为 set 容器中的元素是唯一的。set 容器模板定义了成员函数 equal_range()、lower_bound()、 upper_bound(),这和 multiset 容器在很大程度上是一致的。

5.2.2 set迭代器

set< T> 容器的成员返回的迭代器都是双向迭代器。这些迭代器的类型的别名定义在 set< T> 模板中,可以从 set 中得到类型别名有 iterator、reverse_iterator、const_iterator、 const_reverse_iterator,从它们的名称就可以看出它们的类型。例如:

  • 成员函数 begin() 和 end() 会返回 iterator 类型的迭代器;
  • 成员函数 rbegin() 和 rend() 会返回 reverse_iterator 类型的迭代器;
  • 成员函数 cbegin() 和 cend() 会返回 const_iterator 类型的迭代器。
  • 成员函数 crbegin() 和 crend() 可以返回 const_reverse_iterator 类型的迭代器。

5.2.3 multiset

multiset相当于可以保存重复元素的set集合。

  • multiset 容器和 set 容器有相同的成员函数,但是因为multiset 可以保存重复元素,有些函数的表现会有些不同。和 set 容器中的成员函数表现不同的是:
  • insert() 总是可以成功执行。当插入单个元素时,返回的迭代器指向插入的元素。当插入一段元素时,返回的迭代器指向插入的最后一个元素。
  • emplace() 和 emplace_hint() 总是成功。它们都指向创建的新元素。
  • find() 会返回和参数匹配的第一个元素的迭代器,如果都不匹配,则返回容器的结束迭代器。
  • equal_range() 返回一个包含迭代器的 pair 对象,它定义了一个和参数匹配的元素段。如果没有元素匹配的话,pair 的第一个成员是容器的结束迭代器;在这种情况下,第二个成员是比参数大的第一个元素,如果都没有的话,它也是容器的结束迭代器。
  • lower_bound() 返回和参数匹配的第一个元素的迭代器,如果没有匹配的元素,会返回容器的结束迭代器。返回的迭代器和 range() 返回的 pair 的第一个成员相同。
  • upper_bound() 返回的迭代器和 equal_range() 返回的 pair 的第二个成员相同。
  • count() 返回和参数匹配的元素的个数。
    用 multiset 容器代替 map,实现分析单词出现次数的程序:
// Determining word frequency
#include <iostream>                               // For standard streams
#include <iomanip>                                // For stream manipulators
#include <string>                                 // For string class
#include <sstream>                                // For istringstream
#include <algorithm>                              // For replace_if() & for_each()
#include <set>                                    // For map container
#include <iterator>                               // For advance()
#include <cctype>                                 // For isalpha()

using std::string;

int main()
{
    std::cout << "Enter some text and enter * to end:\n";
    string text_in {};
    std::getline(std::cin, text_in, '*');

    // Replace non-alphabetic characters by a space
    std::replace_if(std::begin(text_in), std::end(text_in), [](const char& ch){ return !isalpha(ch); }, ' ');

    std::istringstream text(text_in);             // Text input string as a stream
    std::istream_iterator<string> begin(text);    // Stream iterator
    std::istream_iterator<string> end;            // End stream iterator

    std::multiset<string> words;                  // Container to store words
    size_t max_len {};                            // Maximum word length

    // Get the words, store in the container, and find maximum length
    std::for_each(begin, end, [&max_len, &words](const string& word){
                words.emplace(word);
                max_len = std::max(max_len, word.length());
    });

    size_t per_line {4},                           // Outputs per line
         count {};                               // No. of words output
 
    for(auto iter = std::begin(words); iter != std::end(words); iter = words.upper_bound(*iter))
    {
        std::cout << std::left << std::setw(max_len + 1) << *iter<< std::setw(3) << std::right << words.count(*iter) << "  ";
        if(++count % per_line == 0)
            std::cout << std::endl;
    }
    std::cout << std::endl;
}

5.2.4 unordered_set

unordered_set< T> 容器类型的模板定义在 unordered_set 头文件中。 unordered_set< T> 可以用保存的元素作为它们自己的键。T 类型的对象在容器中的位置由它们的哈希值决定,因而需要定义一个 Hash< T> 函数。
初始化

unordered_set<string> things {16}; // 16 buckets
unordered_set<string> words {"one", "two", "three", "four"};// Initializer list
unordered_set<string> some_words {++begin(words), end (words)};  // Range
unordered_set<string> copy_wrds {words}; // Copy constructor

unordered_set 容器没有成员函数 at(),并且也没有定义下标运算,它和 unordered_map 有相同类型的成员函数。
添加元素
成员函数 insert() 可以插入作为参数传入的单个元素。在这种情况下,它会返回一个 pair 对象,这个 pair 对象包含一个迭代器,以及一个附加的布尔值用来说明插入是否成功。如果元素被插入,返回的迭代器会指向新元素;如果没有被插入,迭代器指向阻止插入的元素。可以用一个迭代器作为 insert() 的第一个参数,它指定了元素被插入的位置,如果忽略插入位置,在这种情况下,只会返回一个迭代器。

auto pr = words.insert("ninety"); // Returns a pair - an iterator & a bool value
auto iter = words.insert (pr.first, "nine"); // 1st arg is a hint. Returns an iterator
words.insert({"ten", "seven", "six"});  // Inserting an initializer list

unordered_set 容器的成员函数 emplace() 和 emplace_hint() 可以在容器的适当位置创建元素。正如我们之前所见的 set 容器,传入 emplace() 的参数会被传入元素的构造函数,用来创建元素。emplace_hint() 的迭代器参数可以指定元素的插入位置,后面是构造元素需要的参数。例如:

unordered_set<pair<string, string>, Hash_pair> names;
auto pr = names.emplace ("Jack", "Jones") ; // Returns pair<iterator, bool>
auto iter = names.emplace_hint (pr.first, "John", "Smith"); // Returns an iterator

查找元素
调用 unordered_set 的 find() 会返回一个迭代器。这个迭代器指向和参数哈希值匹配的元素,如果没有匹配的元素,会返回这个容器的结束迭代器。例如:

std::pair<string, string> person {"John", "Smith"};
if(names.find(person) != std::end(names))
    std::cout << "We found " << person.first << " " << person.second << std::endl;
else
    std::cout << "There's no " << person.first << " " << person.second << std::endl;

unordered_set 容器中的元素是无序的,因此也不需要成员函数 upper_bound() 和 lower_bound()。成员函数 equal_range() 会返回一个以迭代器为成员的 pair,它指定了和参数匹配的一段元素。

删除元素
成员函数erase()和clear()可以删除集合的元素。erase()可以删除容器中和传入参数的哈希值相同的元素。

std::pair<string, string> person { "John", "Smith"};
auto iter = names.find(person);
if(iter != std::end(names))
    names.erase(iter);

find_if() 算法的前两个参数定义了一个元素段的范围,它会找到这段元素中第一个可以使第三个参数返回 true 的元素,然后返回这个元素的迭代器。
假设需要移除 names 容器中名称以字符 ‘S’ 开始的所有元素,那么下面这个循环就可以实现:

while(true)
{
    auto iter = std::find_if(std::begin(names), std::end(names),[](const std::pair<string, string>& pr ){ return pr.second[0] == 'S';});
    if(iter == std::end(names))
        break;
    names.erase(iter);
}

5.2.5 set_union

第一个版本的set_union()函数模板实现了集合的并集运算,它需要5个参数:两个迭代器用来指定左操作数的集合范围,另两个迭代器用来作为右操作数的集合范围,还有一个迭代器用来指向结果存放位置。例如:

std::vector<int> set1 {1, 2, 3, 4, 5, 6};
std::vector<int> set2 {4, 5, 6, 7, 8, 9};
std::vector<int> result;
std::set_union(std::begin (set1), std::end(set1), // Range for set that is left operand
std::begin(set2), std::end(set2), // Range for set that is right operand
std::back_inserter(result));    // Destination for the result:1 2 3 4 5 6 7 8 9

set1 和 set2 中的初始值都是升序。如果它们都不是,那么在使用 set_union() 算法前,需要对 vector 容器排序。 back_inserter() 函数模板定义在 iterator 头文件中,它会调用传入参数的函数 push_back() 来返回一个 back_inserter_iterator 对象。所以,set1 和 set2 并集中的元素会被保存在 result 中。从并集运算得到的元素集合是容器元素的副本,因此这个运算并不会影响容器的原始内容。

如果不需要保存运算结果,可以用一个流迭代器输出这些元素:

std::set_union(std::begin(set1), std::end(set1), std::begin(set2), std::end(set2),std::ostream_iterator<int> {std::cout, " "});

第二个版本的set_union()函数模板接受的第六个参数是一个用来比较集合元素的函数对象。下面是它的可能的用法:

std:: set<int, std::greater<int>>set1 {1, 2, 3, 4, 5, 6}; // Contains 6 5 4 3 2 1
std:: set<int, std:: greater<int>>set2 {4, 5, 6, 7, 8, 9}; // Contains 9 8 7 6 5 4
std::set<int, std::greater<int>>result; // Elements in descending sequence
std::set_union(std::begin(set1), std::end(set1),std::begin(set2), std::end(set2),std::inserter(result, std::begin(result)), // Result destination: 9 8 7 6 5 4 3 2 1
std::greater<int>()); // Function object for comparing elements

这两个版本的 set_union() 函数都会返回一个指向被复制元素段末尾后一个位置的迭代器。如果目的容器包含操作前的元素,这是很有用的。例如,如果目的容器是 vector 容器,set_union() 用 front_insert_iterator,如果用 back_inserter_iterator,可以用这个容器的结束迭代器,插入这个新元素,set_union() 返回的迭代器会指向第一个原始元素。

5.2.6 set_intersection

set_intersection()是创建两个集合的交集,用法与set_union()相似。set_intersection() 算法会返回一个迭代器,它指向目的容器中插入的最后一个元素的下一个位置。下面的一些语句可以说明它的用法:

std::set<string> words1 {"one", "two", "three", "four", "five", "six"};
std::set<string> words2 {"four""five", "six", "seven", "eight", "nine"};
std::set<string> result;
std::set_intersection(std::begin(words1), std::end(words1), std::begin(words2), std::end(words2),std::inserter(result, std::begin(result)));
// Result: "five" "four" "six"

5.2.7 set_difference

set_difference()算法可以创建两个集合的差集,它也有两个版本的函数并且参数集和set_union()相同。

std::set<string, std::greater<string>> words1 {"one", "two", "three", "four", "five", "six" };
std::set<string, std::greater<string>> words2 { "four", "five", "six", "seven", "eight", "nine"};
std::set<string, std::greater<string>> result;
std::set_difference(std::begin(words1) , std::end(words1),
std::begin(words2), std::end(words2), std::inserter(result, std::begin(result)),std::greater<string>());
// Result: "two", "three", "one"

通过从 words 集合中移除 word1 和 word2 共有的元素来获取差集,差集由来自 word1 的元素组成。结果得到 words1 的前三个元素的降序序列。这个算法也会返回一个迭代器,它指向目的容器被插入的最后一个元素的下一个位置。

set_symmetric_difference()算法和先前的集合算法遵循同一种模式。下面的几条语句展示了它的用法:

std::set<string> words1 { "one", "two", "three", "four", "five", "six" };
std::set<string> words2 {"four", "five", "six", "seven", "eight", "nine"};
std::set_symmetric_difference(std::begin(words1), std::end(words1),std::begin(words2), std::end(words2),std::ostream_iterator<string> {std::cout," "});
//cout: eight nine one seven three two

5.2.7 includes

includes() 算法可以比较两个元素的集合,如果第一个集合中的全部元素都来自第二个集合,它会返回 true。如果第二个集合是空的集合,它也返回 true。下面是一些示例:

std::set<string> words1 { "one", "two", "three", "four", " five", "six"};
std::set<string> words2 {"four", "two", " seven"}; std::multiset<string> words3;
std::cout << std::boolalpha>> std::includes(std::begin(words1), std::end(words1), std::begin(words2), std::end(words2))>> std::endl; //Output: false
std::cout << std::boolalpha>> std::includes(std::begin(words1), std::end(words1), std::begin(words2), std::begin(words2))>> std::endl; //Output: true
std::set_union(std::begin(words1), std::end(words1), std::begin(words2), std::end(words2),std::inserter(words3, std::begin(words3)));
std::cout << std::boolalpha>> std::includes(std::begin(words3), std::end(words3), std::begin(words2), std::end(words2))>> std::endl; //Output: true

这里有两个 string 元素的 set 容器,words1 和 words2,它们都是用初始化列表初始化的。第一条输出语句显示 false,因为 word1 不包含 word2 中的 string(“seven”) 元素。第二条输出语句显示 true,因为第二个操作数指定的集合是空的,也就是说它的开始迭代器和结束迭代器相同。

set_union() 函数会通过 inserter_iterator 将 words1 和 words2 中的并集复制 word3 中。结果 word3 会包含 words2 中的全部元素,因而第三条输出语句会显示 true。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值