您的位置:首页 > 编程语言 > C语言/C++

C++ STL之 map\set\multimap\multiset

2017-09-30 15:18 435 查看

C++ map,set内部数据结构

  1)Set是一种关联容器,它用于存储数据,并且能从一个数据集合中取出数据。它的每个元素的值必须唯一,而且系统会根据该值来自动将数据排序。每个元素的值不能直接被改变。【重点】内部结构采用红黑树的平衡二叉树。multiset
跟set 类似,唯一的区别是允许键值重复!!!

如: 为何map和set的插入删除效率比用其他序列容器高?

       为何每次insert之后,以前保存的iterator不会失效?

       为何map和set不能像vector一样有个reserve函数来预分配数据?

        当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何?

或许有得人能回答出来大概原因,但要彻底明白,还需要了解STL的底层数据结构。 C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和 set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在
STL使用过程中,并不会感到陌生。 C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),所以被STL选择作为了关联容器的内部结构。本文并不会介绍详细AVL树和RB树的实现以及他们的优劣,关于RB树的详细实现参看红黑树: 理论与实现(理论篇)。本文针对开始提出的几个问题的回答,来向大家简单介绍map和set的底层数据结构。

为何map和set的插入删除效率比用其他序列容器高? 大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。

结构图可能如下:

     A

    /  /

  B    C

 / /   / /

D  E F  G

因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。 为何每次insert之后,以前保存的iterator不会失效? 看见了上面答案的解释,你应该已经可以很容易解释这个问题。iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然
被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了
保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时 候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放 到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。 为何map和set不能像vector一样有个reserve函数来预分配数据?
我以前也这么问,究其原理来说时,引起它的原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map声明的时候从参数中传入的Alloc。例如: map, Alloc > intmap; 这时候在intmap中使用的allocator并不是Alloc, 而是通过了转换的Alloc,具体转换的方法时在内部通过Alloc::rebind重新定义了新的节点分配器,详细的实现参看彻底学习STL中的Allocator。其实你就记住一点,在map和set内面的分配器已经发生了变化,reserve方法你就不要奢望了。
当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何? 如果你知道log2的关系你应该就彻底了解这个答案。在map和set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结 果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。 看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
最后,对于map和set Winter还要提的就是它们和一个c语言包装库的效率比较。在许多unix和linux平台下,都有一个库叫isc,里面就提供类似于以下声明的函数: void tree_init(void **tree); void *tree_srch(void **tree, int (*compare)(), void *data); void tree_add(void **tree, int (*compare)(), void *data, void (*del_uar)()); int
tree_delete(void **tree, int (*compare)(), void *data,void (*del_uar)()); int tree_trav(void **tree, int (*trav_uar)()); void tree_mung(void **tree, void (*del_uar)()); 许多人认为直接使用这些函数会比STL map速度快,因为STL map中使用了许多模板什么的。其实不然,它们的区别并不在于算法,而在于内存碎片。如果直接使用这些函数,你需要自己去new一些节点,当节点特别多,
而且进行频繁的删除和插入的时候,内存碎片就会存在,而STL采用自己的Allocator分配内存,以内存池的方式来管理这些内存,会大大减少内存碎 片,从而会提升系统的整体性能。Winter在自己的系统中做过测试,把以前所有直接用isc函数的代码替换成map,程序速度基本一致。当时间运行很长 时间后(例如后台服务程序),map的优势就会体现出来。从另外一个方面讲,使用map会大大降低你的编码难度,同时增加程序的可读性。何乐而不为?

 

10.3 MAP使用



10.3.1. map 对象的定义
map<k, v> m;                  创建一个名为 m 的空 map 对象,其键和值的类型分别为 k 和 v

map<k, v> m(m2);           创建 m2 的副本 m,m 与 m2 必须有相同的键类型和值类型

map<k, v>  m(b, e);         创建 map 类型的对象 m,存储迭代器 b 和 e 标记的范围内所有元素的副本。元素的类型必须能转换为 pair<const k, v>

10.3.2. map 定义的类型

map<K,V>::key_type              在 map 容器中,用做索引的键的类型

map<K,V>::mapped_type        在 map 容器中,键所关联的值的类型

map<K,V>::value_type            一个 pair 类型,它的 first 元素具有 const map<K,V>::key_type 类型,而 second 元素则为 map<K,V>::mapped_type 类型。value_type 是存储元素的键以及值的 pair 类型,而且键为 const。

map 迭代器进行解引用将产生 pair 类型的对象,对迭代器进行解引用将获得一个 pair 对象,它的 first 成员存放键,为const,而 second 成员则存放值。

map<string, int>::iterator map_it = word_count.begin();   map_it->first;  map_it->second;

map 类额外定义了两种类型:key_type 和 mapped_type;

如 map<string, int>::key_type;

10.3.3. 给 map 添加元素

定义了 map 容器后,下一步工作就是在容器中添加键-值元素对。该项工作可使用 insert 成员实现;或者,先用下标操作符获取元素,然后给获取的元素赋值。在这两种情况下,一个给定的键只能对应于一个元素这一事实影响了这些操作的行为。

10.3.4. 使用下标访问 map 对象

map <string, int> word_count;

word_count["Anna"] = 1;

1. 在 word_count 中查找键为 Anna 的元素,没有找到。

2. 将一个新的键-值对插入到 word_count 中。它的键是 const string 类型的对象,保存 Anna。而它的值则采用值初始化,这就意味着在本例中值为 0。

3. 将这个新的键-值对插入到 word_count 中。

4. 读取新插入的元素,并将它的值赋为 1。

使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。

map<string, int> word_count; // empty map from string to int

string word;

while (cin >> word)

++word_count[word];

10.3.5. map::insert 的使用

m.insert(e)                
  e是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个pair 类型对象,包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素

m.insert(beg,end)          beg 和 end 是标记元素范围的迭代器,其中的元素必须为m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。返回 void 类型

m.insert(iter,e)              e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。返回一个迭代器,指向 m 中具有给定键的元素

word_count.insert(map<string, int>::value_type("Anna", 1));

在添加新 map 元素时,使用 insert 成员可避免使用下标操作符所带来的副作用:不必要的初始化。

传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:

word_count.insert(make_pair("Anna", 1));

或使用 typedef

typedef map<string,int>::value_type valType;

word_count.insert(valType("Anna", 1));

检测 insert 的返回值

1)

如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作.

2)

但是,带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 false。在这两种情况下,迭代器都将指向具有给定键的元素。

map<string, int> word_count; // empty map from string to int

string word;

while (cin >> word) {

// inserts element with key equal to word and value 1;

// if word already in word_count, insert does nothing

pair<map<string, int>::iterator, bool> ret =

word_count.insert(make_pair(word, 1));

if (!ret.second) // word already in word_count

++ret.first->second; // increment counter

}

操作:

++((ret.first)->second); // equivalent expression

10.3.6. 查找并读取 map 中的元素

m.count(k)              返回 m 中 k 的出现次数

m.find(k)                 如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器(第 3.4 节)

对于 map 对象,count 成员的返回值只能是 0 或 1。map 容器只允许一个键对应一个实例,所以 count 可有效地表明一个键是否存在。而对于 multimaps容器,count 的返回值将有更多的用途.

int occurs = 0;

if (word_count.count("foobar"))

occurs = word_count["foobar"];

在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用 find 操作。

int occurs = 0;

map<string,int>::iterator it = word_count.find("foobar");

if (it != word_count.end())

occurs = it->second;

10.3.7. 从 map 对象中删除元素

1)参数为迭代器的时候,map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。

2)参数为key_type时候,erase 函数返回被删除元素的个数。对于 map 容器,该值必然是 0 或 1。如果返回 0,则表示欲删除的元素在 map 不存在。
m.erase(k)                              删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数

m.erase(p)                              从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void

m.erase(b,e)                           从 m 中删除一段范围内的元素,该范围由迭代器对 b 和 e 标记。b 和 e 必须标记 m 中的一段有效范围:即 b 和 e 都必须指向m

中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型

10.4.1. set 容器的定义和使用

在 set 中添加元素

1)

set<string> set1; // empty set

set1.insert("the"); // set1 now has one element

set1.insert("and"); // set1 now has two elements

2)

set<int> iset2; // empty set

iset2.insert(ivec.begin(), ivec.end());

调用 insert 函数时,提供一对迭代器实参,插入其标记范围内所有的元素。该版本的 insert 函数类似于形参为一对迭代器的构造函数——对于一个键,仅插入一个元素:

与 map 容器的操作一样,带有一个键参数的 insert 版本返回 pair 类型对象,包含一个迭代器和一个 bool 值,迭代器指向拥有该键的元素,而 bool 值表明是否添加了元素。

使用迭代器对的 insert 版本返回 void 类型。

从 set 中获取元素

iset.find(1) // returns iterator that refers to the element with key == 1

iset.find(11) // returns iterator == iset.end()(假设最多只有十个元素,从1到10)

iset.count(1) // returns 1

iset.count(11) // returns 0

set 容器不提供下标操作符。为了通过键从 set 中获取元素,可使用 find运算。如果只需简单地判断某个元素是否存在,同样可以使用 count 运算,返回 set 中该键对应的元素个数。当然,对于 set 容器,count 的返回值只能是1(该元素存在)或 0(该元素不存在):

正如不能修改 map 中元素的键部分一样,set 中的键也为 const。

set<int>::iterator set_it = iset.find(1);

*set_it = 11; // error: keys in a set are read-only

cout << *set_it << endl; // ok: can read the key

10.5. multimap 和 multiset 类型

multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算。不能对 multimap 对象使用下标操作,

因为在这类容器中,某个键可能对应多个值。

10.5.1 删除操作:

带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数。而带有一个或一对迭代器参数的版本只删除指定的元素,并返回 void类型:

multimap<string, string> authors;

string search_item("Kazuo Ishiguro");

multimap<string, string>::size_type cnt =authors.erase(search_item);

10.5.2. 在 multimap 和 multiset 中查找元素

1)使用 find 和 count 操作

使用 find 和 count 可有效地解决问题。count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:

typedef multimap<string, string>::size_type sz_type;

sz_type entries = authors.count(search_item);

multimap<string,string>::iterator iter =authors.find(search_item);

for (sz_type cnt = 0; cnt != entries; ++cnt, ++iter)

cout <<iter->second << endl; // print each title

2)另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_bound。

m.lower_bound(k)                      返回一个迭代器,指向键不小于 k(>=k) 的第一个元素

m.upper_bound(k)                      返回一个迭代器,指向键大于 k (> k) 的第一个元素

m.equal_range(k)                      返回一个迭代器的 pair 对象它的 first 成员等价于 m.lower_bound(k)。而 second 成员则等价于 m.upper_bound(k)

如果该键k不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。

如果所查找的元素拥有 multimap 容器中最大的键,那么的该键上调用 upper_bound 将返回超出末端迭代器。如果所查找的键不存在,而且比 multimap 容器中所有的键都大,则 low_bound 也将返回超出末端迭代器。

typedef multimap<string, string>::iterator authors_it;

authors_it beg = authors.lower_bound(search_item),

end = authors.upper_bound(search_item);// loop through the number of entries there are for this author

while (beg != end)

{

cout << beg->second << endl; // print each title

++beg;

}若该键没有关联的元素,则 lower_bound 和 upper_bound 返回相同的迭代器:都指向同一个元素或同时指向 multimap 的超出末端位置。它们都指向在保

持容器元素顺序的前提下该键应被插入的位置














内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: