当前位置:   article > 正文

编程参考 - C++使用hashmap_c++ hashmap

c++ hashmap

C++标准库里的container包含了ordered和unordered的map(std::map和std::unordered_map)。

在ordered map中,各个单元(elements)根据key排序,插入和访问的复杂度是O(log n)。标准库的ordered map的内部实现一般是使用红黑树(red black trees),是一种平衡二叉树( balanced binary search tree)。

但这属于实现细节,不影响我们的使用。

在unordered map中,插入和访问(insert and access)操作的复杂度是O(1)。可以认为仅仅名称和hashtable不一样而已,在Java的标准库中使用的类名就是hashtable。

使用hash方法只是内部实现的细节,在C++标准并没有规定必须使用hashtable来实现unordered map,而且使用unordered_map的抽象名称表示的更准确。

ordered map维持了元素的排序,方便你按顺序从头到尾遍历各个元素,而unordered map中是没有顺序可言的。

使用std::map的例子:

#include <map>

#include <iostream>

#include <cassert>

int main(int argc, char** argv)

{

  std::map<std::string, int> m;

  m["hello"] = 23;

  // check if key is present

  if(m.find("world") != m.end())

    std::cout << "map contains key world!\n";

  // retrieve

  std::cout << m["hello"] << '\n';

  std::map<std::string, int>::iterator i = m.find("hello");

  assert(i != m.end());

  std::cout << "Key: " << i->first << " Value: " << i->second << '\n';

  return 0;

}

输出:

23

Key: hello Value: 23

如果你需要使用的容器类的内容进行排序,时间复杂度O(log n)也没问题的话,就使用std::map。但需要注意,每个元素里的key要能使用小于号的运算符,即实现了operator<。

否则的话,你应该要用hash-table,insert/access的时间复杂度是O(1)。在C++里,相应的类就是std::unordered_map,它的API和std::map类似。还有一点,每个元素的key对象要有hash函数,一般int和string对象是有默认现成的哈希函数的,但有些对象没有的话,就要自己实现。还要能用等号运算符,即实现了operator==。

unordered_map的这个container是在C++ 11标准中引入的。所以你的编译器要支持才行。比如在GCC 4.8中,就需要在CXXFLAGS中加入选项-std=c++11。

注:CXXFLAGS和CFLAGS常用作C/C++程序构建时所使用的编译器选项的环境变量(compiler flags)。

甚至在C++11发布之前,GCC就支持unordered_map,不过是在命名空间std::tr1中。因此,对于老的GCC编译器,你可以尝试这样使用它:

#include <tr1/unordered_map>

std::tr1::unordered_map<std::string, int> m;

使用说明

具体的使用方法,请参见cplusplus.comen.cppreference.com上关于此类的说明文档,包含了各个类成员的介绍。

std::unordered_map - cppreference.com

https://cplusplus.com/reference/unordered_map/

常用的 成员函数分为Capacity、Iterators、Element access、Element lookup、Modifiers、Buckets、Hash policy和Observers几类。

包括如下成员函数:

empty -  判断是否为空

size - 元素数量

begin - 遍历开始

end - 遍历结尾

operator[ ] - 元素访问,如果key不存在则添加,最常用函数,而不是用at访问元素

at - 元素访问,如果key不存在则报异常,不捕捉会导致程序crash退出

find - 查找元素

emplace - 构造新元素并添加,如果元素已存在则返回的bool值为false且操作不成功,否则为true并添加元素

insert - 插入元素,如果元素已存在则返回的bool值为false且操作不成功,否则为true并添加元素

erase - 删除元素

clear - 清除全部元素,无返回值,没有异常情况

swap - 两个map交换内容

成员函数使用举例:

  mymap.erase ( mymap.begin() );      // erasing by iterator

  mymap.erase ("France");             // erasing by key

  mymap.erase ( mymap.find("China"), mymap.end() ); // erasing by range

  mymap.emplace("PINK", "#222222");  // std::map<std::string, std::string>

insert函数使用举例:

// unordered_map::insert

#include <iostream>

#include <string>

#include <unordered_map>

int main ()

{

  std::unordered_map<std::string,double>

              myrecipe,

              mypantry = {{"milk",2.0},{"flour",1.5}};

  std::pair<std::string,double> myshopping ("baking powder",0.3);

  myrecipe.insert (myshopping);                        // copy insertion

  myrecipe.insert (std::make_pair<std::string,double>("eggs",6.0)); // move insertion

  myrecipe.insert (mypantry.begin(), mypantry.end());  // range insertion

  myrecipe.insert ( {{"sugar",0.8},{"salt",0.1}} );    // initializer list insertion

  std::cout << "myrecipe contains:" << std::endl;

  for (auto& x: myrecipe)

    std::cout << x.first << ": " << x.second << std::endl;

  std::cout << std::endl;

  return 0;

}

输出:

myrecipe contains:

salt: 0.1

eggs: 6

sugar: 0.8

baking powder: 0.3

flour: 1.5

milk: 2

程序例子:

#include <iostream>

#include <string>

#include <unordered_map>

int main(){

    // Create an unordered_map of three strings (that map to strings)

    std::unordered_map<std::string, std::string> u = {

        {"RED","#FF0000"},

        {"GREEN","#00FF00"},

        {"BLUE","#0000FF"}

    };

    // Add two new entries to the unordered_map

    u["BLACK"] = "#000000";

    u["WHITE"] = "#FFFFFF";

    std::cout << "\nOutput values by key:\n"

                 "The HEX of color RED is:[" << u["RED"] << "]\n"

                 "The HEX of color BLACK is:[" << u["BLACK"] << "]\n\n";

    std::cout << "Use operator[] with non-existent key to insert a new key-value pair:\n";

    std::cout << "Key:[" << "new_key" << "] Value:[" << u["new_key"] << "]\n";

    // Assign new value for "new key"

    std::cout << "\nSet value for new_key: #111111.\n";

    u["new_key"] = "#111111";

    

    std::cout << "\nIterate and print key-value pairs, using `auto`;\n"

                 "new_key is now one of the keys in the map:\n";

    u.emplace("PINK", "#222222");  

    u.insert(std::make_pair<std::string,std::string>("YELLOW", "#222222"));

    for( const auto& n : u ) {

        std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";

    }

    

    return 0;

}

输出结果:

Output values by key:

The HEX of color RED is:[#FF0000]

The HEX of color BLACK is:[#000000]

Use operator[] with non-existent key to insert a new key-value pair:

Key:[new_key] Value:[]

Set value for new_key: #111111.

Iterate and print key-value pairs, using `auto`;

new_key is now one of the keys in the map:

Key:[YELLOW] Value:[#222222]

Key:[new_key] Value:[#111111]

Key:[BLUE] Value:[#0000FF]

Key:[RED] Value:[#FF0000]

Key:[GREEN] Value:[#00FF00]

Key:[BLACK] Value:[#000000]

Key:[WHITE] Value:[#FFFFFF]

Key:[PINK] Value:[#222222]

HashTable简单原理介绍

针对一个key对象,使用hash函数计算得到一个整数值,也叫hash值或哈希值。

相同的key对象,得到的是相同的hash值。不同的key对象,计算的值一般都是不同的,如果相同,就认为是碰撞(collisions)发生了,这不是我们所希望见到的情况。

使用计算得到整数值,也就是hash值唯一标记了一个对象,根据hash值,就能快速判断两个对象是否相等,如果hash值不同则一定不同,如果hash相等,可以进一步比较对象内容来判断是否相同。

除此以外,也可以使用hash值来作为索引,访问和此对象相关的内容,这就是map中的key和value。

上面计算出的hash值的范围是由hash算法决定的,因为尽可能不同的对象所计算出的hash值是不同的,那这个整数hash值就不会太小。如果直接作为索引,需要的空间就会很大。

所以一般是先确定一个空间大小,然后用hash值对这个空间大小取模,来存放和访问相关信息。

如果取模以后的整数值相同,而对象却不同,就发生了碰撞,就要再使用一个公式,寻找一个新位置。当位置不够了,就要增加整个空间的大小。

实际使用举例以及注意事项

1,直接定义map没有问题。

#include <map>

typedef struct {

unsigned char BD_ADDR0;

unsigned char BD_ADDR1;

unsigned char BD_ADDR2;

unsigned char BD_ADDR3;

unsigned char BD_ADDR4;

unsigned char BD_ADDR5;

} BD_ADDR_t;

int main()

{

  std::map<BD_ADDR_t, int> mymap;

  return 0;

}

2,但如果使用map就会出现编译错误,比如添加一个元素。

int main() {

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::map<BD_ADDR_t, int> mymap;

  mymap[addr] = 1;

  return 0;

}

错误信息:

$ g++ -o main main.cpp

In file included from /usr/include/c++/9/bits/stl_tree.h:65,

                 from /usr/include/c++/9/map:60,

                 from main.cpp:2:

/usr/include/c++/9/bits/stl_function.h: In instantiation of ‘constexpr bool std::less<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = BD_ADDR_t]’:

/usr/include/c++/9/bits/stl_map.h:497:32:   required from ‘std::map<_Key, _Tp, _Compare, _Alloc>::mapped_type& std::map<_Key, _Tp, _Compare, _Alloc>::operator[](const key_type&) [with _Key = BD_ADDR_t; _Tp = int; _Compare = std::less<BD_ADDR_t>; _Alloc = std::allocator<std::pair<const BD_ADDR_t, int> >; std::map<_Key, _Tp, _Compare, _Alloc>::mapped_type = int; std::map<_Key, _Tp, _Compare, _Alloc>::key_type = BD_ADDR_t]’

main.cpp:18:13:   required from here

/usr/include/c++/9/bits/stl_function.h:386:20: error: no match for ‘operator<’ (operand types are ‘const BD_ADDR_t’ and ‘const BD_ADDR_t’)

  386 |       { return __x < __y; }

      |                ~~~~^~~~~

只要使用要访问元素的函数,都会出错,比如find函数也一样。但其他函数可能运行正常,比如调用empty函数的话,就能正常编译过。

3,看到提示,是因为这个key类型没有定义operator<。我们一般使用int和string类型,是有这个运算符的,如果使用其他的类型,就可能需要自己实现operator<。

添加一个operator<函数再试一下,这个也叫operator overloading,C++里的运算符重载:

bool operator<(BD_ADDR_t a,  BD_ADDR_t b) {return false;}

int main()

{

  bool flag;

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::map<BD_ADDR_t, int> mymap;

  mymap[addr] = 2;

  flag = mymap.empty();

  return 0;

}

现在编译运行都成功。所以,当使用map的访问元素的功能时,编译器在根据模板生成代码时,就会去寻找key对象的小于号操作符,如果找不到,就会报错。

只需要一个小于号就能比较两个对象,分三种情况:

a<b, 直接判断出;

a>b, 使用b<a的运算来判断。

a==b,如果a<b为假,b<a也为假,就是a==b,或者表达为 !(a<b || b<a)

4,如果是unordered_map,仅仅是定义就会出错:

#include <unordered_map>

typedef struct {

unsigned char BD_ADDR0;

unsigned char BD_ADDR1;

unsigned char BD_ADDR2;

unsigned char BD_ADDR3;

unsigned char BD_ADDR4;

unsigned char BD_ADDR5;

} BD_ADDR_t;

int main()

{

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::unordered_map<BD_ADDR_t, int> mymap;

  return 0;

}

       

编译错误里有出现:

/usr/include/c++/9/bits/hashtable_policy.h:1096:7: error: use of deleted function ‘std::hash<BD_ADDR_t>::hash()’

所以需要给BD_ADDR_t对象定义一个hash函数,语法如下:

unordered_map<KeyType, ValueType, MyHashType> um;

例子代码:

#include <unordered_map>

typedef struct {

unsigned char BD_ADDR0;

unsigned char BD_ADDR1;

unsigned char BD_ADDR2;

unsigned char BD_ADDR3;

unsigned char BD_ADDR4;

unsigned char BD_ADDR5;

} BD_ADDR_t;

class MyHashFunction{

public:

  size_t operator()(const BD_ADDR_t & p) const

  {

    return p.BD_ADDR0 + p.BD_ADDR1 + p.BD_ADDR2 + p.BD_ADDR3 + p.BD_ADDR4 + p.BD_ADDR5;

  }

};

int main()

{

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::unordered_map<BD_ADDR_t, int, MyHashFunction> mymap;

  return 0;

}

现在的代码就可以编过了。这里定义了一个类,对()运算符进行重载,对BD_ADDR_t类型对象进行计算hash值,返回一个size_t类型。

5,在给unordered_map添加了hash function之后,再调用访问元素的函数,发现还会出错,提示的编译错误是在模板代码进行扩展时没有找到operator==的定义。

例子代码和编译错误显示如下:

#include <unordered_map>

typedef struct {

unsigned char BD_ADDR0;

unsigned char BD_ADDR1;

unsigned char BD_ADDR2;

unsigned char BD_ADDR3;

unsigned char BD_ADDR4;

unsigned char BD_ADDR5;

} BD_ADDR_t;

class MyHashFunction{

public:

  size_t operator()(const BD_ADDR_t & p) const

  {

    return p.BD_ADDR0 + p.BD_ADDR1 + p.BD_ADDR2 + p.BD_ADDR3 + p.BD_ADDR4 + p.BD_ADDR5;

  }

};

int main()

{

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::unordered_map<BD_ADDR_t, int, MyHashFunction> mymap;

  mymap[addr] = 123;

  return 0;

}

编译错误:

main.cpp:30:13:   required from here

/usr/include/c++/9/bits/stl_function.h:356:20: error: no match for ‘operator==’ (operand types are ‘const BD_ADDR_t’ and ‘const BD_ADDR_t’)

  356 |       { return __x == __y; }

      |                ~~~~^~~~~~

这个是因为unordered_map和map的对元素的访问原理不同。map的每个key的位置也都按顺序排好,所以需要根据小于号来找到位置,遍历二叉树的元素时,判断是当前节点的左分支还是右分支,或者就是当前节点。

而unordered_map是hash值计算后直接得到位置,再进行访问,但有可能出现碰撞,碰撞时要判断两个对象是否一样,所以就需要等号运算符。

6,添加一个operator==函数。

#include <unordered_map>

typedef struct {

unsigned char BD_ADDR0;

unsigned char BD_ADDR1;

unsigned char BD_ADDR2;

unsigned char BD_ADDR3;

unsigned char BD_ADDR4;

unsigned char BD_ADDR5;

} BD_ADDR_t;

class MyHashFunction{

public:

  size_t operator()(const BD_ADDR_t & p) const

  {

    return p.BD_ADDR0 + p.BD_ADDR1 + p.BD_ADDR2 + p.BD_ADDR3 + p.BD_ADDR4 + p.BD_ADDR5;

  }

};

bool operator==(const BD_ADDR_t & a, const BD_ADDR_t & b){

  return (a.BD_ADDR0 == b.BD_ADDR0) && (a.BD_ADDR1 == b.BD_ADDR1) &&

          (a.BD_ADDR2 == b.BD_ADDR2) && (a.BD_ADDR3 == b.BD_ADDR3) &&

          (a.BD_ADDR4 == b.BD_ADDR4) && (a.BD_ADDR5 == b.BD_ADDR5);

}

int main()

{

  bool flag;

  BD_ADDR_t addr = {1,2,3,4,5,6};

  std::unordered_map<BD_ADDR_t, int, MyHashFunction> mymap;

  mymap[addr] = 123;

  return 0;

}

编译成功,可以正常使用了。

参考:

What is the best way to use a HashMap in C++? - Stack Overflow

c++ - what the difference between map and hashmap in STL - Stack Overflow

map vs. hash_map in C++ - Stack Overflow

How to create an unordered_map of user defined class in C++? - GeeksforGeeks

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/227415
推荐阅读
相关标签
  

闽ICP备14008679号