




已阅读5页,还剩19页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
本文根据他人博文整理而来,尊重原创。在前面的系列文章中,依次介绍了基于无序列表的顺序查找,基于有序数组的二分查找,平衡查找树,以及红黑树,下图是他们在平均以及最差情况下的时间复杂度:可以看到在时间复杂度上,红黑树在平均情况下插入,查找以及删除上都达到了lgN的时间复杂度。那么有没有查找效率更高的数据结构呢,答案就是本文接下来要介绍了散列表,也叫哈希表(Hash Table)什么是哈希表哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。使用哈希查找有两个步骤:1. 使用哈希函数将被查找的键转换为数组的索引。在理想的情况下,不同的键会被转换为不同的索引值,但是在有些情况下我们需要处理多个键被哈希到同一个索引值的情况。所以哈希查找的第二个步骤就是处理冲突2. 处理哈希碰撞冲突。有很多处理哈希碰撞冲突的方法,本文后面会介绍拉链法和线性探测法。哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。哈希函数哈希查找第一步就是使用哈希函数将键映射成索引。这种映射函数就是哈希函数。如果我们有一个保存0-M数组,那么我们就需要一个能够将任意键转换为该数组范围内的索引(0M-1)的哈希函数。哈希函数需要易于计算并且能够均匀分布所有键。比如举个简单的例子,使用手机号码后三位就比前三位作为key更好,因为前三位手机号码的重复率很高。再比如使用身份证号码出生年月位数要比使用前几位数要更好。在实际中,我们的键并不都是数字,有可能是字符串,还有可能是几个值的组合等,所以我们需要实现自己的哈希函数。1. 正整数获取正整数哈希值最常用的方法是使用除留余数法。即对于大小为素数M的数组,对于任意正整数k,计算k除以M的余数。M一般取素数。2. 字符串将字符串作为键的时候,我们也可以将他作为一个大的整数,采用保留除余法。我们可以将组成字符串的每一个字符取值然后进行哈希,比如public int GetHashCode(string str) char s = str.ToCharArray(); int hash = 0; for (int i = 0; i s.Length; i+) hash = si + (31 * hash); return hash;上面的哈希值是Horner计算字符串哈希值的方法,公式为:h = s0 31L1+ + sL 3 312+ sL 2 311+ sL 1 310举个例子,比如要获取”call”的哈希值,字符串c对应的unicode为99,a对应的unicode为97,L对应的unicode为108,所以字符串”call”的哈希值为 3045982 = 99313+ 97312+ 108311+ 108310= 108 + 31 (108 + 31 (97 + 31 (99)如果对每个字符去哈希值可能会比较耗时,所以可以通过间隔取N个字符来获取哈西值来节省时间,比如,可以 获取每8-9个字符来获取哈希值:public int GetHashCode(string str) char s = str.ToCharArray(); int hash = 0; int skip = Math.Max(1, s.Length / 8); for (int i = 0; i s.Length; i+=skip) hash = si + (31 * hash); return hash;但是,对于某些情况,不同的字符串会产生相同的哈希值,这就是前面说到的哈希冲突(Hash Collisions),比如下面的四个字符串:如果我们按照每8个字符取哈希的话,就会得到一样的哈希值。所以下面来讲解如何解决哈希碰撞:避免哈希冲突拉链法 (Separate chaining with linked lists)通过哈希函数,我们可以将键转换为数组的索引(0-M-1),但是对于两个或者多个键具有相同索引值的情况,我们需要有一种方法来处理这种冲突。一种比较直接的办法就是,将大小为M 的数组的每一个元素指向一个条链表,链表中的每一个节点都存储散列值为该索引的键值对,这就是拉链法。下图很清楚的描述了什么是拉链法。图中,”John Smith”和”Sandra Dee” 通过哈希函数都指向了152 这个索引,该索引又指向了一个链表, 在链表中依次存储了这两个字符串。该方法的基本思想就是选择足够大的M,使得所有的链表都尽可能的短小,以保证查找的效率。对采用拉链法的哈希实现的查找分为两步,首先是根据散列值找到等一应的链表,然后沿着链表顺序找到相应的键。 我们现在使用我们之前介绍符号表中的使用无序链表实现的查找表SequentSearchSymbolTable来实现我们这里的哈希表。当然,您也可以使用.NET里面内置的LinkList。首先我们需要定义一个链表的总数,在内部我们定义一个SequentSearchSymbolTable的数组。然后每一个映射到索引的地方保存一个这样的数组。public class SeperateChainingHashSet : SymbolTables where TKey : IComparable, IEquatable private int M;/散列表大小 private SequentSearchSymbolTable st;/ public SeperateChainingHashSet() : this(997) public SeperateChainingHashSet(int m) this.M = m; st = new SequentSearchSymbolTablem; for (int i = 0; i m; i+) sti = new SequentSearchSymbolTable(); private int hash(TKey key) return (key.GetHashCode() & 0x7fffffff) % M; public override TValue Get(TKey key) return sthash(key).Get(key); public override void Put(TKey key, TValue value) sthash(key).Put(key, value); 可以看到,该实现中使用 Get方法来获取指定key的Value值,我们首先通过hash方法来找到key对应的索引值,即找到SequentSearchSymbolTable数组中存储该元素的查找表,然后调用查找表的Get方法,根据key找到对应的Value。 Put方法用来存储键值对,首先通过hash方法找到改key对应的哈希值,然后找到SequentSearchSymbolTable数组中存储该元素的查找表,然后调用查找表的Put方法,将键值对存储起来。 hash方法来计算key的哈希值, 这里首先通过取与&操作,将符号位去除,然后采用除留余数法将key应到到0-M-1的范围,这也是我们的查找表数组索引的范围。实现基于拉链表的散列表,目标是选择适当的数组大小M,使得既不会因为空链表而浪费内存空间,也不会因为链表太而在查找上浪费太多时间。拉链表的优点在于,这种数组大小M的选择不是关键性的,如果存入的键多于预期,那么查找的时间只会比选择更大的数组稍长,另外,我们也可以使用更高效的结构来代替链表存储。如果存入的键少于预期,索然有些浪费空间,但是查找速度就会很快。所以当内存不紧张时,我们可以选择足够大的M,可以使得查找时间变为常数,如果内存紧张时,选择尽量大的M仍能够将性能提高M倍。线性探测法线性探测法是开放寻址法解决哈希冲突的一种方法,基本原理为,使用大小为M的数组来保存N个键值对,其中MN,我们需要使用数组中的空位解决碰撞冲突。如下图所示:对照前面的拉链法,在该图中,”Ted Baker” 是有唯一的哈希值153的,但是由于153被”Sandra Dee”占用了。而原先”Snadra Dee”和”John Smith”的哈希值都是152的,但是在对”Sandra Dee”进行哈希的时候发现152已经被占用了,所以往下找发现153没有被占用,所以存放在153上,然后”Ted Baker”哈希到153上,发现已经被占用了,所以往下找,发现154没有被占用,所以值存到了154上。开放寻址法中最简单的是线性探测法:当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中的下一个位置即将索引值加1,这样的线性探测会出现三种结果:1. 命中,该位置的键和被查找的键相同2. 未命中,键为空3. 继续查找,该位置和键被查找的键不同。实现线性探测法也很简单,我们只需要两个大小相同的数组分别记录key和value。public class LinearProbingHashSet : SymbolTables where TKey : IComparable, IEquatable private int N;/符号表中键值对的总数 private int M = 16;/线性探测表的大小 private TKey keys; private TValue values; public LinearProbingHashSet() keys = new TKeyM; values = new TValueM; private int hash(TKey key) return (key.GetHashCode() & 0xFFFFFFF) % M; public override TValue Get(TKey key) for (int i = hash(key); keysi != null; i = (i + 1) % M) if (key.Equals(keysi) return valuesi; return default(TValue); public override void Put(TKey key, TValue value) int hashCode = hash(key); for (int i = hashCode; keysi != null; i = (i + 1) % M) if (keysi.Equals(key)/如果和已有的key相等,则用新值覆盖 valuesi = value; return; /插入 keysi = key; valuesi = value; 线性探查(Linear Probing)方式虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在。性能分析我们可以看到,哈希表存储和查找数据的时候分为两步,第一步为将键通过哈希函数映射为数组中的索引, 这个过程可以认为是只需要常数时间的。第二步是,如果出现哈希值冲突,如何解决,前面介绍了拉链法和线性探测法下面就这两种方法进行讨论:对于拉链法,查找的效率在于链表的长度,一般的我们应该保证长度在M/8M/2之间,如果链表的长度大于M/2,我们可以扩充链表长度。如果长度在0M/8时,我们可以缩小链表。对于线性探测法,也是如此,但是动态调整数组的大小需要对所有的值从新进行重新散列并插入新的表中。不管是拉链法还是散列法,这种动态调整链表或者数组的大小以提高查询效率的同时,还应该考虑动态改变链表或者数组大小的成本。散列表长度加倍的插入需要进行大量的探测, 这种均摊成本在很多时候需要考虑。哈希碰撞攻击我们知道如果哈希函数选择不当会使得大量的键都会映射到相同的索引上,不管是采用拉链法还是开放寻址法解决冲突,在后面查找的时候都需要进行多次探测或者查找, 在很多时候会使得哈希表的查找效率退化,而不再是常数时间。下图清楚的描述了退化后的哈希表:哈希表攻击就是通过精心构造哈希函数,使得所有的键经过哈希函数后都映射到同一个或者几个索引上,将哈希表退化为了一个单链表,这样哈希表的各种操作,比如插入,查找都从O(1)退化到了链表的查找操作,这样就会消耗大量的CPU资源,导致系统无法响应,从而达到拒绝服务供给(Denial of Service, Dos)的目的。之前由于多种编程语言的哈希算法的“非随机”而出现了Hash碰撞的DoS安全漏洞,在ASP.NET中也曾出现过这一问题。在.NET中String的哈希值内部实现中,通过使用哈希值随机化来对这种问题进行了限制,通过对碰撞次数设置阈值,超过该阈值就对哈希函数进行随机化,这也是防止哈希表退化的一种做法。下面是BCL中string类型的GetHashCode方法的实现,可以看到,当碰撞超过一定次数的时候,就会开启条件编译,对哈希函数进行随机化。ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecuritySafeCritical, _DynamicallyInvokablepublic override unsafe int GetHashCode() if (HashHelpers.s_UseRandomizedStringHashing) return InternalMarvin32HashString(this, this.Length, 0L); fixed (char* str = (char*) this) char* chPtr = str; int num = 0x15051505; int num2 = num; int* numPtr = (int*) chPtr; int length = this.Length; while (length 2) num = (num 0x1b) numPtr0; num2 = (num2 0x1b) numPtr1; numPtr += 2; length -= 4; if (length 0) num = (num 0x1b) numPtr0; return (num + (num2 * 0x5d588b65); .NET中哈希的实现我们可以通过在线源码查看.NET 中Dictionary,类型的实现,我们知道任何作为key的值添加到Dictionary中时,首先会获取key的hashcode,然后将其映射到不同的bucket中去:public Dictionary(int capacity, IEqualityComparer comparer) if (capacity 0) Initialize(capacity); parer = comparer ? EqualityComparer.Default;在Dictionary初始化的时候,会如果传入了大小,会初始化bucket 就是调用Initialize方法:private void Initialize(int capacity) int size = HashHelpers.GetPrime(capacity); buckets = new intsize; for (int i = 0; i = 0; i = entriesi.next) if (entriesi.hashCode = hashCode & comparer.Equals(entriesi.key, key) if (add) ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate); entriesi.value = value; version+; return; #if FEATURE_RANDOMIZED_STRING_HASHING collisionCount+;#endif int index; if (freeCount 0) index = freeList; freeList = entriesindex.next; freeCount-; else if (count = entries.Length) Resize(); targetBucket = hashCode % buckets.Length; index = count; count+; entriesindex.hashCode = hashCode; entriesindex.next = bucketstargetBucket; entriesindex.key = key; entriesindex.value = value; bucketstargetBucket = index; version+; #if FEATURE_RANDOMIZED_STRING_HASHING if(collisionCount HashHelpers.HashCollisionThreshold & HashHelpers.IsWellKnownEqualityComparer(comparer) comparer = (IEqualityComparer) HashHelpers.GetRandomizedEqualityComparer(comparer); Resize(entries.Length, true); #endif 首先,根据key获取其hashcode,然后将hashcode除以backet的大小取余映射到目标backet中,然后遍历该bucket存储的链表,如果找到和key相同的值,如果不允许后添加的键与存在的键相同替换值(add),则抛出异常,如果允许,则替换之前的值,然后返回。如果没有找到,则将新添加的值放到新的bucket中,当空余空间不足的时候,会进行扩容操作(Resize),然后重新hash到目标bucket。这里面需要注意的是Resize操作比较消耗资源。总结前面几篇文章先后介绍了基于无序列表的顺序查找,基于有序数组的二分查找,平衡查找树,以及红黑树,本篇文章最后介绍了查找算法中的最后一类即符号表又称哈希表,并介绍了哈希函数以及处理哈希冲突的两种方法:拉链法和线性探测法。各种查找算法的最坏和平均条件下各种操作的时间复杂度如下图:在实际编写代码中,如何选择合适的数据结构需要根据具体的数据规模,查找效率要求,时间和空间局限来做出合适的选择。希望本文以及前面的几篇文章对您有所帮助。说明:本程序建立的哈希表示意图:哈希函数为对哈希表长取余源代码:cppview plaincopy1. /*2. *哈希表算法实现3. *(c)copyright2013,jdh4. *AllRightReserved5. *文件名:main.c6. *程序员:jdh7. */8. 9. #include10. #include11. 12. /*13. *宏定义14. */15. 16. /*17. *数据类型重定义18. */19. 20. #defineuint8_tunsignedchar21. #defineuint16_tunsignedshort22. #defineuint32_tunsignedlong23. 24. /*25. *哈希表长度26. */27. 28. #defineHASH_TABLE_LEN10029. 30. /*31. *数据结构32. */33. /链表节点34. typedefstruct_Link_Node35. 36. uint16_tid;37. uint16_tdata;38. struct_Link_Node*next;39. Link_Node,*Link_Node_Ptr;40. 41. /哈希表头42. typedefstruct_Hash_Header43. 44. struct_Link_Node*next;45. Hash_Header,*Hash_Header_Ptr;46. 47. /*48. *全局变量49. */50. 51. /哈希表52. Hash_Header_PtrHash_TableHASH_TABLE_LEN;53. 54. /*55. *函数56. */57. 58. /*59. *哈希表函数60. *说明:61. *1.用哈希函数生成id对应的哈希表中的位置62. 输入:id63. 返回:位置64. */65. 66. uint8_thash_func(uint16_tid)67. 68. uint8_tpos=0;69. 70. pos=id%HASH_TABLE_LEN;71. 72. returnpos;73. 74. 75. /*76. *初始化节点77. *返回:结点指针78. */79. 80. Link_Node_Ptrinit_link_node(void)81. 82. Link_Node_Ptrnode;83. 84. /申请节点85. node=(Link_Node_Ptr)malloc(sizeof(Link_Node);86. /初始化长度为087. node-next=NULL;88. 89. returnnode;90. 91. 92. /*93. *初始化哈希表头结点94. *返回哈希表头结点指针95. */96. 97. Hash_Header_Ptrinit_hash_header_node(void)98. 99. Hash_Header_Ptrnode;100. 101. /申请节点102. node=(Hash_Header_Ptr)malloc(sizeof(Hash_Header);103. /初始化长度为0104. node-next=NULL;105. 106. returnnode;107. 108. 109. 110. /*111. *哈希表初始化112. *说明:113. *1.初始化哈希表Hash_Table114. *2.哈希表长度最大不能超过256115. */116. 117. voidinit_hash_table(void)118. 119. uint8_ti=0;120. 121. for(i=0;inext=NULL;125. 126. 127. 128. /*129. *在哈希表增加节点130. *说明:131. *1.在哈希表的某个链表末增加数据132. 输入:new_node:新节点133. */134. 135. voidappend_link_node(Link_Node_Ptrnew_node)136. 137. Link_Node_Ptrnode;138. uint8_tpos=0;139. 140. /新节点下一个指向为空141. new_node-next=NULL;142. 143. /用哈希函数获得位置144. pos=hash_func(new_node-id);145. 146. /判断是否为空链表147. if(Hash_Tablepos-next=NULL)148. 149. /空链表150. Hash_Tablepos-next=new_node;151. 152. else153. 154. /不是空链表155. /获取根节点156. node=Hash_Tablepos-next;157. 158. /遍历159. while(node-next!=NULL)160. 161. node=node-next;162. 163. 164. /插入165. node-next=new_node;166. 167. 168. 169. /*170. *在哈希表查询节点171. *说明:172. *1.知道在哈希表某处的单链表中,并开始遍历.173. *2.返回的是查询节点的前一个节点指针.这么做是为了做删除操作.174. 输入:pos:哈希表数组位置,从0开始计数175. id:所需要查询节点的id176. root:如果是根节点,则*root=1,否则为0177. 返回:所需查询的节点的前一个节点指针,如果是根节点则返回根节点,失败返回0178. */179. 180. Link_Node_Ptrsearch_link_node(uint16_tid,uint8_t*root)181. 182. Link_Node_Ptrnode;183. uint8_tpos=0;184. 185. /用哈希函数获得位置186. pos=hash_func(id);187. 188. /获取根节点189. node=Hash_Tablepos-next;190. 191. /判断单链表是否存在192. if(node=NULL)193. 194. return0;195. 196. 197. /判断是否是根节点198. if(node-id=id)199. 200. /是根节点201. *root=1;202. returnnode;203. 204. else205. 206. /不是根节点207. *root=0;208. /遍历209. while(node-next!=NULL)210. 211. if(node-next-id=id)212. 213. returnnode;214. 215. else216. 217. node=node-n
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 考研数学真题解析:判题思路抓重点
- 北京地区中端婚礼服市场的多维剖析与发展前瞻
- 分形方法在旋转机械系统故障特征提取中的应用与探索
- 农业蔬菜大棚承包与农业生态旅游合作合同
- 锅炉(承压)设备焊工协同作业考核试卷及答案
- 厨具产品的营销方案设计
- 店铺促销活动宣传方案策划
- 增加客户粘性活动方案策划
- 实体门店帮扶咨询方案
- 建筑方案设计怎么评职称
- 2024版高中同步学案优化设计思想政治必修4人教版-第三单元测评
- 2026届新高三开学考试英语模拟试卷|新高考I卷(含答案解析)
- 数字化设计与制造技术专业教学标准(高等职业教育专科)2025修订
- 2025至2030年中国中试基地行业市场全景调查及发展趋向研判报告
- 承兑汇票转让协议书
- 业主直接参与物业管理区域的物业管理
- 《运动医学与康复》课件
- 2025年自建房施工合同书 (包工不包料 C款)
- 军事心理战试题及答案
- 2025年北京市第一次普通高中学业水平合格性考试历史试题(含答案)
- 二年级上册数学《观察物体》教学设计
评论
0/150
提交评论