在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
今天想到哈希函数,好像解决冲突的只了解了一种链地址法而且也很模糊,就查了些资料复习一下 1、哈希 这种转换是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,因此不能从散列值来唯一地确定输入值。 简单的说,哈希就是一种将任意长度的消息压缩到某一固定长度的信息摘要函数。 2、哈希表 最经典的一种实现方法就是拉链法,它的数据结构是链表的数组:
数组的特点是:寻址容易,插入和删除困难。 链表的特点是:寻址困难,插入和删除容易。 对于某个元素,我们通过哈希算法,根据元素特征计算元素在数组中的下标,从而将元素分配、插入到不同的链表中去。在查找时,我们同样通过元素特征找到正确的链表,再从链表中找出正确的元素。 3、HashMap 的数据结构 三、几个核心问题 index = hashCode % n; 我们可以使用以下方法来实现: index = hashCode & (n-1); 但是,以上按位与的操作跟取模运算并不等价,这可能会带来index分布不均匀问题。 举个例子,假设数组大小为15,则hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。 如果能够保证按位与的操作跟取模运算是等价的,那么不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。 那么,为什么如何实现位运算(&)跟取模运算(%)的等价呢?我们看以下等式: X % 2^n = X & (2^n – 1) 假设n为3,则2^3 = 8,表示成二进制就是1000。2^3-1 = 7 ,表示成二进制就是0111。 此时X & (2^3 – 1) 就相当于取X的二进制的最后三位数。 从二进制角度来看,X / 2^n 相当于 X >> n,即把X右移n位,被移掉的部分(后n位),则是X % 2^n,也就是余数。 因此,计算 X % 2^n,实际上就是要获取 X 的后n位。 我们注意到,2^n 的后 n+1 位都是1,其余为0,于是 2^n-1 的后 n 位都是1,其余为0。 因此,X 跟 2^n-1 做按位与运算,将得到X 的后n位。 所以,只要保证数组的大小是2^n,就可以使用位运算来替代取模运算了。 因此,当拿到一个用户指定的数组大小时,我们总是会再做一层处理,以保证实际的数组大小为 2^n: 1 size_t getTableSize(size_t capacity) { 2 // 计算超过 capacity 的最小 2^n 3 size_t ssize = 1; 4 while (ssize < capacity) { 5 ssize <<= 1; 6 } 7 return ssize; 8 } 2. 哈希策略:如何将元素均匀地分配到各个桶内由于我们将使用key的hashCode来计算该元素在数组中的下标,所以我们希望hashCode是一个size_t类型。所以我们的哈希函数最首要的就是要把各种类型的key转换成size_t类型,以下是代码实现: 1 #ifndef cache_hash_func_H__ 2 #define cache_hash_func_H__ 3 4 #include <string> 5 6 namespace HashMap { 7 8 /** 9 * hash算法仿函数 10 */ 11 template<class KeyType> 12 struct cache_hash_func { 13 }; 14 15 inline std::size_t cache_hash_string(const char* __s) { 16 unsigned long __h = 0; 17 for (; *__s; ++__s) 18 __h = 5 * __h + *__s; 19 return std::size_t(__h); 20 } 21 22 template<> 23 struct cache_hash_func<std::string> { 24 std::size_t operator()(const std::string & __s) const { 25 return cache_hash_string(__s.c_str()); 26 } 27 }; 28 29 template<> 30 struct cache_hash_func<char*> { 31 std::size_t operator()(const char* __s) const { 32 return cache_hash_string(__s); 33 } 34 }; 35 36 template<> 37 struct cache_hash_func<const char*> { 38 std::size_t operator()(const char* __s) const { 39 return cache_hash_string(__s); 40 } 41 }; 42 43 template<> 44 struct cache_hash_func<char> { 45 std::size_t operator()(char __x) const { 46 return __x; 47 } 48 }; 49 50 template<> 51 struct cache_hash_func<unsigned char> { 52 std::size_t operator()(unsigned char __x) const { 53 return __x; 54 } 55 }; 56 57 template<> 58 struct cache_hash_func<signed char> { 59 std::size_t operator()(unsigned char __x) const { 60 return __x; 61 } 62 }; 63 64 template<> 65 struct cache_hash_func<short> { 66 std::size_t operator()(short __x) const { 67 return __x; 68 } 69 }; 70 71 template<> 72 struct cache_hash_func<unsigned short> { 73 std::size_t operator()(unsigned short __x) const { 74 return __x; 75 } 76 }; 77 78 template<> 79 struct cache_hash_func<int> { 80 std::size_t operator()(int __x) const { 81 return __x; 82 } 83 }; 84 85 template<> 86 struct cache_hash_func<unsigned int> { 87 std::size_t operator()(unsigned int __x) const { 88 return __x; 89 } 90 }; 91 92 template<> 93 struct cache_hash_func<long> { 94 std::size_t operator()(long __x) const { 95 return __x ^ (__x >> 32); 96 } 97 }; 98 99 template<> 100 struct cache_hash_func<unsigned long> { 101 std::size_t operator()(unsigned long __x) const { 102 return __x ^ (__x >> 32); 103 } 104 }; 105 106 } 可以看到,上面实现的hash函数比较随意,难以产生较为均匀(即冲突少)的hashCode。 为了防止质量低下的hashCode()函数实现,我们使用getHash()方法对一个对象的hashCode进行重新计算:(下面这个就是hash方法的精髓) 1 size_t getHash(size_t h) const { 2 h ^= (h >>> 20) ^ (h >>> 12); 3 return h ^ (h >>> 7) ^ (h >>> 4); 4 } 这段代码对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。 getHash的更多实现解析可参考: 全网把Map中的hash()分析的最透彻的文章,别无二家。http://www.hollischuang.com/archives/2091 3. 多线程:如何实现无读锁,低写锁 同时,单向链表的使用,给我们带来了一个意想不到的好处:多个读线程和一个写线程并发操作不会出问题。 假设链表中目前包含A和B节点,此时要在它们之间插入C节点,步骤如下: 在完成1和2两步之后,读线程查询链表只能看到A和B,链表是完整的。 读操作代码: entry_ptr get(const KeyType & key) { 写操作代码: //返回值表示key是否已经存在, 已存在返回true 1. unordered_map的定义 template<class Key,
第2个参数,存储mapped value。 第3个参数,为哈希函数的函数对象。它将key作为参数,并利用函数对象中的哈希函数返回类型为size_t的唯一哈希值。默认值为std::hash<key>。 第4个参数,为等比函数的函数对象。它内部通过等比操作符’=='来判断两个key是否相等,返回值为bool类型。默认值是std::equal_to<key>。在unordered_map中,任意两个元素之间始终返回false。 2. 问题分析 因为都是函数对象,它们两个的实际定义方法并没有很大差别。不过后者比前者多了一个方法。因为等比函数的函数对象默认值std::equal_to<key>内部是通过调用操作符"=="进行等值判断,因此我们可以直接在自定义类里面进行operator==()重载(成员和友元都可以)。 因此,如果要将自定义类型作为unordered_map的键值,需如下两个步骤: 定义哈希函数的函数对象; 定义等比函数的函数对象或者在自定义类里重载operator==()。 3. 定义方法 为了避免重复,下文以讨论哈希函数的函数对象为主,参数4则是通过直接在自定义类里面对operator==()进行重载。 我们选一种实现 重载operator()的类#include <iostream> #include <string> #include <unordered_map> #include <functional> using namespace std; class Person{ public: string name; int age; Person(string n, int a){ name = n; age = a; } bool operator==(const Person & p) const { return name == p.name && age == p.age; } }; struct hash_name{ size_t operator()(const Person & p) const{ return hash<string>()(p.name) ^ hash<int>()(p.age);// hash<string>()(p.name)就是求这个string对应hash值得方法 } }; int main(int argc, char* argv[]){ unordered_map<Person, int, hash_name> ids; //不需要把哈希函数传入构造器 ids[Person("Mark", 17)] = 40561; ids[Person("Andrew",16)] = 40562; for ( auto ii = ids.begin() ; ii != ids.end() ; ii++ ) cout << ii->first.name << " " << ii->first.age << " : " << ii->second << endl; return 0; }
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论