首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++】哈希表的实现

【C++】哈希表的实现

作者头像
用户11290673
发布2025-01-13 08:38:59
发布2025-01-13 08:38:59
3890
举报
文章被收录于专栏:学习学习

1.哈希概念

哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找。

1.1直接定址法

当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在[a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标。也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。

1.2哈希冲突

直接定址法的缺点也⾮常明显,当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。假设我们只有数据范围是[0, 9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计算出的值必须在[0, M)之间。

这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做 哈希冲突,或者哈希碰撞 。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。

1.3负载因子

假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 负载因子等于N/M,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;

1.4将关键字转化为整数

我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后⾯代码实现中再进⾏细节展⽰。下⾯哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。

1.5哈希函数

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到,但是我们要尽量往这个⽅向去考量设计

1.5.1除法散列法/除留余数法

除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。

当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key %

本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:

{63 , 31}看起来没有关联的值,如果M是16,也就是2的4次方 ,那么计算出的哈希值都是15,因为63的二进制后8位是 00111111,31的⼆进制后8位是 00011111。如果是 10的X次方,就更明显了,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是10的2次方 ,那么计算出的哈希值都是12。

当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。

需要说明的是,实践中也是⼋仙过海,各显神通,Java的HashMap采⽤除法散列法时就是2的整数

次幂做哈希表的⼤⼩M,这样玩的话,就不⽤取模,⽽可以直接位运算,相对⽽⾔位运算⽐模更⾼

效⼀些。但是他不是单纯的去取模,⽐如M是2^16次⽅,本质是取后16位,那么⽤key’ =

key>>16,然后把key和key' 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围

内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀⼀些即可。所以我们上⾯建

议M取不太接近2的整数次冥的⼀个质数的理论是⼤多数数据结构书籍中写的理论吗,但是实践中,灵活运⽤,抓住本质,⽽不能死读书。

1.5.2乘法散列法

乘法散列法对哈希表⼤⼩M没有要求,他的⼤思路第⼀步:⽤关键字 K 乘上常数 A (0<A<1),并抽

取出 k*A 的⼩数部分。第⼆步:后再⽤M乘以k*A 的⼩数部分,再向下取整。

h ( key ) = floor ( M × (( A × key)%1.0)), 其中floor表⽰对表达式进⾏下取整,A∈(0,1),这⾥

最重要的是A的值应该如何设定,Knuth认为A= ( 5 − 1)/2 = 0.6180339887.... (⻩⾦分割点])⽐较好。

乘法散列法对哈希表⼤⼩M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A*key

= 762.6539420558,取⼩数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =

669.6366651392,那么h(1234) = 669。

1.5.3全域散列法

如果存在⼀个恶意的对⼿,他针对我们提供的散列函数,特意构造出⼀个发⽣严重冲突的数据集,

⽐如,让所有关键字全部落⼊同⼀个位置中。这种情况是可以存在的,只要散列函数是公开且确定

的,就可以实现此攻击。解决⽅法⾃然是⻅招拆招,给散列函数增加随机性,攻击者就⽆法找出确

定可以导致最坏情况的数据。这种⽅法叫做全域散列。

h ab ( key ) = (( a × key + b )% P )%M ,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的

任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。

假设P=17,M=6,a = 3, b = 4, 则 h 34 (8) = ((3 × 8 + 4)%17)%6 = 5。

需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查

改都固定使⽤这个散列函数,否则每次哈希都是随机选⼀个散列函数,那么插⼊是⼀个散列函数,

查找⼜是另⼀个散列函数,就会导致找不到插⼊的key了。

1.5.4其他⽅法

上⾯的⼏种⽅法是《算法导论》书籍中讲解的⽅法。

《殷⼈昆 数据结构:⽤⾯向对象⽅法与C++语⾔描述 (第⼆版)》和 《[数据结构(C语⾔版)].严蔚

敏_吴伟⺠》等教材型书籍上⾯还给出了平⽅取中法、折叠法、随机数法、数学分析法等,这些⽅

法相对更适⽤于⼀些局限的特定场景,有兴趣可以去看看这些书籍。

1.6处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。

1.6.1开放定址法

在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。

线性探测

从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛

到哈希表尾,则回绕到哈希表头的位置。

h ( key ) = hash 0 = key % M, hash0位置冲突了,则线性探测公式为:

hc ( key , i ) = hashi = ( hash 0 + i ) % Mi = {1, 2, 3, ..., M − 1},因为负载因⼦⼩于1,

则最多探测M-1次,⼀定能找到⼀个存储key的位置。

线性探测的⽐较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1,

hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位

置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。

下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中。

⼆次探测

从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为

⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表

尾的位置;

h ( key ) = hash 0 = key % M , hash0位置冲突了,则⼆次探测公式为:

hc ( key , i ) = hashi = ( hash 0 ± i 2 ) % Mi = {1, 2, 3, ..., }

⼆次探测当 hashi = ( hash 0 − i 2 )% M 时,当hashi<0时,需要hashi += M

下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中。

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

1.6.2开放定址法代码实现

开放定址法在实践中,不如下⾯讲的链地址法,因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的都是哈希表中的空间,始终存在互相影响的问题。所以开放定址法,我们简单选择线性探测实现即可。

开放定址法的哈希表结构

1enum State 2{ 3EXIST, 4EMPTY, 5DELETE 6}; 7template < class K , class V > 8struct HashData 9{ 10 11 pair<K, V> _kv; 12 State _state = EMPTY; 13 }; 14 15 template < class K , class V > 16 class HashTable 17 { 18 private : 19 vector<HashData<K, V>> _tables; 20 size_t _n = 0 ; // 表中存储数据个数 21 };

要注意的是这⾥需要给每个存储值的位置加⼀个状态标识,否则删除⼀些值以后,会影响后⾯冲突的值的查找。如下图,我们删除30,会导致查找20失败,当我们给每个位置加⼀个状态标识

{EXIST,EMPTY,DELETE} ,删除30就可以不⽤删除值,⽽是把状态改为 DELETE ,那么查找20

时是遇到 EMPTY 才能,就可以找到20。

h(19) = 8 , h(30) = 8 , h(5) = 5 , h(36) = 3 , h(13) = 2 , h(20) = 9 , h(21) =

10 , h(12) = 1

扩容

这⾥我们哈希表负载因⼦控制在0.7,当负载因⼦到0.7以后我们就需要扩容了,我们还是按照2倍扩容,但是同时我们要保持哈希表⼤⼩是⼀个质数,第⼀个是质数,2倍后就不是质数了。那么如何解决

了,⼀种⽅案就是上⾯1.4.1除法散列中我们讲的Java HashMap的使⽤2的整数冥,但是计算时不能直接取模的改进⽅法。另外⼀种⽅案是sgi版本的哈希表使⽤的⽅法,给了⼀个近似2倍的质数表,每次去质数表获取扩容后的⼤⼩。

1 inline unsigned long __stl_next_prime( unsigned long n) 2 { 3 // Note: assumes long is at least 32 bits. 4 static const int __stl_num_primes = 28 ; 5 static const unsigned long __stl_prime_list[__stl_num_primes] = 6 { 7 53 , 97 , 193 , 389 , 769 , 8 1543 , 3079 , 6151 , 12289 , 24593 , 9 49157 , 98317 , 196613 , 393241 , 786433 , 10 1572869 , 3145739 , 6291469 , 12582917 , 25165843 , 11 50331653 , 100663319 , 201326611 , 402653189 , 805306457 , 12 1610612741 , 3221225473 , 4294967291 13 }; 14 15 const unsigned long * first = __stl_prime_list; 16 const unsigned long * last = __stl_prime_list + __stl_num_primes; 17 const unsigned long * pos = lower_bound (first, last, n); 18 return pos == last ? *(last - 1 ) : *pos; 19 }

key不能取模的问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。string做哈希表的key⾮常常⻅,所以我们可以考虑把string特化⼀下。

1 template < class K > 2 struct HashFunc 3 { 4 size_t operator ()( const K& key) 5 { 6 return ( size_t )key; 7 } 8 }; 9 10 // 特化 11 template <> 12 struct HashFunc <string> 13 { 14 // 字符串转换成整形,可以把字符 ascii 码相加即可 15 // 但是直接相加的话,类似 "abcd" "bcad" 这样的字符串计算出是相同的 16 // 这⾥我们使⽤ BKDR 哈希的思路,⽤上次的计算结果去乘以⼀个质数,这个质数⼀般去 31, 131 等效果会⽐较好 17 size_t operator ()( const string& key) 18 { 19 size_t hash = 0 ; 20 for ( auto e : key) 21 { 22 hash *= 131 ; 23 hash += e; 24 } 25 26 return hash; 27 } 28 }; 29 30 template < class K , class V , class Hash = HashFunc<K>> 31 class HashTable 32 { 33 public : 34 private : 35 vector<HashData<K, V>> _tables; 36 size_t _n = 0 ; // 表中存储数据个数 37 };

完整代码实现

1 namespace open_address 2 { 3 enum State 4 { 5 EXIST, 6 EMPTY, 7 DELETE 8 }; 9 10 template < class K , class V > 11 struct HashData 12 { 13 pair<K, V> _kv; 14 State _state = EMPTY; 15 }; 16 17 template < class K , class V , class Hash = HashFunc<K>> 18 class HashTable 19 { 20 public : 21 inline unsigned long __stl_next_prime( unsigned long n) 22 { 23 // Note: assumes long is at least 32 bits. 24 static const int __stl_num_primes = 28 ; 25 static const unsigned long __stl_prime_list[__stl_num_primes] = 26 { 27 53 , 97 , 193 , 389 , 769 , 28 1543 , 3079 , 6151 , 12289 , 24593 , 29 49157 , 98317 , 196613 , 393241 , 786433 , 30 1572869 , 3145739 , 6291469 , 12582917 , 25165843 , 31 50331653 , 100663319 , 201326611 , 402653189 , 805306457 , 32 1610612741 , 3221225473 , 4294967291 33 }; 34 35 const unsigned long * first = __stl_prime_list; 36 const unsigned long * last = __stl_prime_list + __stl_num_primes; 37 const unsigned long * pos = lower_bound (first, last, n); 38 return pos == last ? *(last - 1 ) : *pos; 39 } 40 41 HashTable () 42 { 43 _tables. resize (__stl_next_prime(_tables. size ())); 44 } 45 46 bool Insert ( const pair<K, V>& kv) 47 { 48 if ( Find (kv.first)) 49 return false ; 50 51 // 负载因⼦⼤于 0.7 就扩容 52 if (_n * 10 / _tables. size () >= 7 ) 53 { 54 // 这⾥利⽤类似深拷⻉现代写法的思想插⼊后交换解决 55 HashTable<K, V, Hash> newHT; 56 newHT._tables. resize (__stl_next_prime(_tables. size ())); 57 for ( size_t i = 0 ; i < _tables. size (); i++) 58 { 59 if (_tables[i]._state == EXIST) 60 { 61 newHT. Insert (_tables[i]._kv); 62 } 63 } 64 65 _tables. swap (newHT._tables); 66 } 67 68 Hash hs; 69 size_t hashi = hs (kv.first) % _tables. size (); 70 while (_tables[hashi]._state == EXIST) 71 { 72 ++hashi; 73 hashi %= _tables. size (); 74 } 75 76 _tables[hashi]._kv = kv; 77 _tables[hashi]._state = EXIST; 78 ++_n; 79 80 return true ; 81 } 82 83 HashData<K, V>* Find ( const K& key) 84 { 85 Hash hs; 86 size_t hashi = hs (key) % _tables. size (); 87 while (_tables[hashi]._state != EMPTY) 88 { 89 if (_tables[hashi]._state == EXIST 90 && _tables[hashi]._kv.first == key) 91 { 92 return &_tables[hashi]; 93 } 94 95 ++hashi; 96 hashi %= _tables. size (); 97 } 98 99 return nullptr ; 100 } 101 102 bool Erase ( const K& key) 103 { 104 HashData<K, V>* ret = Find (key); 105 if (ret == nullptr ) 106 { 107 return false ; 108 } 109 else 110 { 111 ret->_state = DELETE; 112 return true ; 113 } 114 } 115 private : 116 vector<HashData<K, V>> _tables; 117 size_t _n = 0 ; // 表中存储数据个数 118 }; 119 }

1.6.3链地址法

解决冲突的思路

开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。 下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中。

h(19) = 8 , h(30) = 8 , h(5) = 5 , h(36) = 3 , h(13) = 2 , h(20) = 9 , h(21) =

10 , h(12) = 1,h(24) = 2,h(96) = 88

扩容

开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。

极端场景如果极端场景下,某个桶特别⻓怎么办?其实我们可以考虑使⽤全域散列法,这样就不容易被针对了。但是假设不是被针对了,⽤了全域散列法,但是偶然情况下,某个桶很⻓,查找效率很低怎么办?这⾥在Java8的HashMap中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下, 不断扩容,单个桶很⻓的场景还是⽐较少的,下⾯我们实现就不搞这么复杂了,这个解决极端场景的思路,⼤家了解⼀下。

1.6.4链地址法代码实现

1 namespace hash_bucket 2 { 3 template < class K , class V > 4 struct HashNode 5 { 6 pair<K, V> _kv; 7 HashNode<K, V>* _next; 8 9 HashNode ( const pair<K, V>& kv) 10 :_kv(kv) 11 ,_next( nullptr ) 12 {} 13 }; 14 15 template < class K , class V , class Hash = HashFunc<K>> 16 class HashTable 17 { 18 typedef HashNode<K, V> Node; 19 20 inline unsigned long __stl_next_prime( unsigned long n) 21 { 22 23 static const int __stl_num_primes = 28 ; 24 static const unsigned long __stl_prime_list[__stl_num_primes] = 25 { 26 53 , 97 , 193 , 389 , 769 , 27 1543 , 3079 , 6151 , 12289 , 24593 , 28 49157 , 98317 , 196613 , 393241 , 786433 , 29 1572869 , 3145739 , 6291469 , 12582917 , 25165843 , 30 50331653 , 100663319 , 201326611 , 402653189 , 805306457 , 31 1610612741 , 3221225473 , 4294967291 32 }; 33 34 const unsigned long * first = __stl_prime_list; 35 const unsigned long * last = __stl_prime_list + __stl_num_primes; 36 const unsigned long * pos = lower_bound (first, last, n); 37 return pos == last ? *(last - 1 ) : *pos; 38 } 39 public : 40 HashTable () 41 { 42 _tables. resize (__stl_next_prime(_tables. size ()), nullptr ); 43 } 44 45 // 拷⻉构造和赋值拷⻉需要实现深拷⻉,有兴趣的同学可以⾃⾏实现 46 47 ~ HashTable () 48 { 49 // 依次把每个桶释放 50 for ( size_t i = 0 ; i < _tables. size (); i++) 51 { 52 Node* cur = _tables[i]; 53 while (cur) 54 { 55 Node* next = cur->_next; 56 delete cur; 57 cur = next; 58 } 59 _tables[i] = nullptr ; 60 } 61 } 62 63 bool Insert ( const pair<K, V>& kv) 64 { 65 Hash hs; 66 size_t hashi = hs (kv.first) % _tables. size (); 67 68 // 负载因⼦ ==1 扩容 69 if (_n == _tables. size ()) 70 { 71 /*HashTable<K, V> newHT; 72 newHT._tables.resize(_tables.size() * 2); 73 for (size_t i = 0; i < _tables.size(); i++) 74 { 75 Node* cur = _tables[i]; 76 while(cur) 77 { 78 newHT.Insert(cur->_kv); 79 cur = cur->_next; 80 } 81 } 82 _tables.swap(newHT._tables);*/ 83 84 // 这⾥如果使⽤上⾯的⽅法,扩容时创建新的结点,后⾯还要使⽤就结 点,浪费了 85 // 下⾯的⽅法,直接移动旧表的结点到新表,效率更好 86 vector<Node*> newtables (__stl_next_prime(_tables.size()), nullptr ); 87 for ( size_t i = 0 ; i < _tables. size (); i++) 88 { 89 Node* cur = _tables[i]; 90 while (cur) 91 { 92 Node* next = cur->_next; 93 94 // 旧表中节点,挪动新表重新映射的位置 95 size_t hashi = hs (cur->_kv.first) % newtables. size (); 96 // 头插到新表 97 cur->_next = newtables[hashi]; 98 newtables[hashi] = cur; 99 100 cur = next; 101 } 102 103 _tables[i] = nullptr ; 104 } 105 106 _tables. swap (newtables); 107 } 108 109 // 头插 110 Node* newnode = new Node (kv); 111 newnode->_next = _tables[hashi]; 112 _tables[hashi] = newnode; 113 ++_n; 114 115 return true ; 116 } 117 118 Node* Find ( const K& key) 119 { 120 Hash hs; 121 size_t hashi = hs (key) % _tables. size (); 122 Node* cur = _tables[hashi]; 123 while (cur) 124 { 125 if (cur->_kv.first == key) 126 { 127 return cur; 128 } 129 130 cur = cur->_next; 131 } 132 133 return nullptr ; 134 } 135 136 bool Erase ( const K& key) 137 { 138 Hash hs; 139 size_t hashi = hs (key) % _tables . size (); 140 Node* prev = nullptr ; 141 Node* cur = _tables[hashi]; 142 while (cur) 143 { 144 if (cur->_kv.first == key) 145 { 146 if (prev == nullptr ) 147 { 148 _tables[hashi] = cur->_next; 149 } 150 else 151 { 152 prev->_next = cur->_next; 153 } 154 155 delete cur; 156 --_n; 157 return true ; 158 } 159 160 prev = cur; 161 cur = cur->_next; 162 } 163 164 return false ; 165 } 166 167 private : 168 vector<Node*> _tables; // 指针数组 169 size_t _n = 0 ; // 表中存储数据个数 170 }; 171 }


结束语 本篇博客我们将哈希表有关知识进行总结,下片博客,就来就用哈希表模拟下unordered_map与unordered_set的实现 OK,本篇博客结束

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.哈希概念
    • 1.1直接定址法
    • 1.2哈希冲突
    • 1.3负载因子
    • 1.4将关键字转化为整数
    • 1.5哈希函数
      • 1.5.1除法散列法/除留余数法
      • 1.5.2乘法散列法
      • 1.5.3全域散列法
      • 1.5.4其他⽅法
    • 1.6处理哈希冲突
      • 1.6.1开放定址法
      • 1.6.2开放定址法代码实现
      • 1.6.3链地址法
      • 1.6.4链地址法代码实现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档