1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
| // 哈希表 js中并没有哈希表 但是更够实现 理论知识多 代码少 // 基于数组实现 快速插入删除查找 哈希表的速度比树更快 基本可以瞬间查找到元素 // 相对于树来说编码容易些 // 数组进行插入操作时 效率比较低 // 查找时:如果基于下标查找效率高 如果基于内容查找效率很低 // 数组进行删除操作 效率低 各种位移操作 // 数组进行修改和查找差不多
// 哈希表相对于数组的不足 // 数据没有顺序 不能以一种固定的从小到大或者从大到小的顺序遍历元素 // key值不允许重复 不能使用相同的key保存不同的值
// 哈希表结构是数组 特别的地方是对下标值的变换 称为哈希函数 // 通过哈希函数可以获取到HashCode
// 公司员工存储 // 数组结构存储一个个员工信息 下标效率较高 定义员工编号查找 // 链表结构 插入和删除效率比较高 从头到尾查找效率低
// 联系人和电话存储 // 存储单词
// 将字符串转换为下标值方案 字母文字转数字 // 字符编码 ASCII编码 a是97 b是98,,,122是z GBXXXX GBK GB18030十万中文文字 // 单独设置本地区域独特文字的编码一旦上了国际就不行 因此Unicode万国码出来了utf-8 utf-32 // 设计自己的编码 方便理解 // 方案1:吧单词每个字符的编码求和 cats是3+1+20+19=43 那么43就作为cats单词的下标存在于数组中 // 名称key不会重复 但是编码值相加下标值可能会重复 后来的数据必然会覆盖 下标值存储很多值不合理 // 方案2:幂的连乘 7654 = 7*10的3次方 + 6*10的2次方 + 5*10的1次方 + 4 // 基本可以保持数据的唯一性 不会和别的单词重复 // cats = 3 * 26(自己编码的26个字符)的3次幂 + 1 * 26的2次幂 + 20 * 27 + 17 = 60337 // 但是如果是zzzzzzzz的话那么下标值就是70000000000.。 创建大的下标值的数组没有意义 // 会存在很多下标值是空的位置 虽然保证了数字的唯一性 但是对空间造成了很大的浪费性 // 改进方案2 hash化 哈希化 把幂的连乘方案中得到的巨大的整数范围压缩到可接受的范围内 // 0-70000000000压缩到0-100000 使用取余操作符 也会存在一定的重复性 后续解决 // 哈希化 将大数字压缩成规定范围下标值 // 哈希函数 将字母文字单词转换为大数字的函数 // 哈希表 将最终得到的数字插入数组 整体结构的封装称之为哈希表 // 什么事冲突 下标值相同 已存在 即使概率小 也要考虑解决方案 // 常见两种解决方式 // 1:链地址法 / 拉链法 // 2:开放地址法 // 1链地址法 // 将相同下标值的元素存放在链表(或者数组)中,每个数组单元存储的不再是单个数据 而是一个链表 // 如果插入前端 使用链表 插入后端使用数组 // 2:开放地址法 寻找空白的单元格添加重复的数据 // 探索空白位置有三种方法 查找空位厕所 找坑填坑 // 线性检测法 如果单元格存在数据 index + 1依次向后查找 删除元素后 不能将下表设置为null // 会影响之后查询的其他操作 所以通常删除一个数据后 会进行特殊处理(将下标值设置为-1) // 线性检测有问题 // 聚集 一连串的相同下标值向后查找填充后续多个单元单元就是聚集 影响hash表的性能 查询/删除/插入 // 二次检测法 优化的是探测的步长 x + 1 x + 3相隔多个步数探测 一次性探测较远距离 避免聚集影响 // 问题:也会存在累加步数到达相同位置的元素 还是会影响效率 因此要设置每个人的步长都不一样
// 再哈希法 // 为了消除线性检测和二次检测中步长的问题 // 需要一种方法产生一种依赖关键字的探测序列 而不是每个关键字都一样 不同的关键字即使映射到 // 相同的数组下标 也可以使用不同的探测序列 // 把关键字用另外一个函数再做一个哈希化 用这次哈希化的结果作为步长 对于指定的关键字 // 步长在整个探测中是不变的 不过不同的关键字使用不同的时长
// 特点 // 和第一个哈希函数不相同 不能输出为0 否则将没有步长 每次探测都是原地踏步 算法就进入死循环 // 计算机专家已经设计出了工作很好的哈希函数 // stepSize = constant - (key % constant) // 其中constant是质数 且小于数组的容量 // 例如stepSize = 5 - (key % 5)满足需求 那么结果不可能为0
// 哈希化的效率 // 哈希化执行插入和搜索效率是非常高的 如果没有冲突效率更高 如果存在冲突 那么查询事件就依赖后来的探测长度 // 平均探测长度以及平均存取时间 取决于装填因子 随着装填因子变大 探测长度也越长 // 随着装填因子变大 效率下降 在不同开发地址法方案中比链地址法更严重
// 装填因子 表示当前哈希表中已经包含的数据项和整个哈希表长度的比值 // 装填因子 = 总数据项 / 哈希表长度 // 开放地址法的最大装填因子是1 必须找到空白单元格才能放入 后续效率也会变低 // 链地址法的装填因子可以大于1 因为拉链法可以无线延伸下去 但是后续效率会变低
// 优秀的哈希函数 // 哈希表的优点在于速度 提高速度的一个方法就是让哈希函数中尽量少有乘法和除法因为他们性能低 // 快速获取hashCode值 无论是链地址法还是开放地址法 当多个元素映射到同一个位置都会影响效率 // 所以 优秀的哈希函数尽量会把元素映射到不同的位置 让元素在哈希表中均匀的分布
// 3*2的10次方 + 3*2的9次方 + 3*2的8次方 + 3*2的7次方 。。。 效率低下 // 优化:霍纳法则(秦九韶算法) // 大O表示法从O(N的2次方)变成了O(N) // 大O表示法:(N的2次方 + n) / 2 随着数据增加查找数据的时间的变化趋势 // 在需要使用常量的地方尽量使用质数 // 质数使用的位置 // 哈希表的长度 N次幂的底数
// 设计哈希函数 // 1:将字符转换为比较大的数字hashCode // 2:将大的数字压缩到数组范围内 function hashFunc(str,size){ // 1:定义hashCode变量 var hashCode = 0 // 2:霍纳算法计算hashCode值 for(var i = 0;i < str.length;i++){ // 获取该字符unicode // str.charCodeAt(i) hashCode = 37 * hashCode + str.charCodeAt(i) } // 取余操作 var index = hashCode % size console.log('index',index) } hashFunc('cats',7) hashFunc('catss',7) hashFunc('catsss612',7)
// 采用链地址法实现hash表格 function hashTable(){ // 属性 this.storage = [] //存放相关元素 this.count = 0 //表示已经存在多长的数据 this.limit = 7 //标记数组中一共可以存多少数据 // 装填因子loadFactor 如果超过0.75就需要对数组进行扩容 小于0.25还需要变小压缩 // 方法 // 哈希函数 hashTable.prototype.hashFunc = function (str,size){ // 1:定义hashCode变量 var hashCode = 0 // 2:霍纳算法计算hashCode值 for(var i = 0;i < str.length;i++){ // 获取该字符unicode // str.charCodeAt(i) hashCode = 37 * hashCode + str.charCodeAt(i) } // 取余操作 var index = hashCode % size return index } // 插入修改 hashTable.prototype.put = function(key,value){ // 1:根据key获取索引值 将数据插入对应位置 var index = this.hashFunc(key,this.limit) // 2:根据索引值取出桶 判断桶是否存在 不存在就创造桶,放在该位置 var bucket = this.storage[index] if(bucket == null){ bucket = [] this.storage[index] = bucket } // 3:一定有桶以后判断是修改还是添加操作 如果有值了那么就修改值 如果没有执行添加操作 // 修改操作 for(var i = 0; i < bucket.length; i++){ // 从数组中线性查找 var tuple = bucket[i]//tuple是桶中的每一个数组 if(tuple[0] == key){ tuple[1] = value return } } // 新增操作 bucket.push([key,value]) this.count += 1 // 判断是否需要扩容操作 if(this.count > this.limit * 0.75){ // 判断扩容完成是否是质数 var newSize = this.limit * 2 var newZhishu = this.getZhishu(newSize) this.resize(newSize) }
} // 查找获取 hashTable.prototype.get = function(key){ // 1:根据key获取索引值 var index = this.hashFunc(key,this.limit) // 2:根据index获取对应桶bucket 判断bucket是否为空 var bucket = this.storage[index] if(bucket == null){ return null } // 循环遍历桶中每一个数值的key是否和传入的key相同 for(var i = 0; i < bucket.length; i++){ var tuple = bucket[i] if(tuple[0] == key){ // 找到对应key返回value return tuple[1] } } // 还没找到 return null } // 删除操作 hashTable.prototype.remove = function(key){ // 1:根据key获取索引值 var index = this.hashFunc(key,this.limit) // 2:根据index获取对应桶bucket 判断bucket是否为空 var bucket = this.storage[index] // 不存在直接返回null if(bucket == null){ return null } // 3:循环遍历桶中每一个数值的key是否和传入的key相同 for(var i = 0; i < bucket.length; i++){ var tuple = bucket[i] if(tuple[0] == key){ // 找到对应key删除 tuple.splice(i,1) this.count-- return tuple[1] // 如果storage过长 内部数据太少 那么就需要缩小容量 此处应该不会执行 放置位置有错误 if(this.limt > 7 && this.count < this.limit * 0.25){ // 缩小为质数 var newSize = Math.floor(this.limit / 2) var newZhishu = this.getZhishu(newSize) this.resize(newSize) } } } // 依然没有找到 return null } // 其他操作 // 是否为空 hashTable.prototype.isEmpty = function(){ return this.count == 0 } // 获取hash表中元素的个数 hashTable.prototype.size = function(){ return this.count } // 哈希表扩容思想 // 因为使用的是链地址法 填充因子可以大于1 所以这个hash表可以无线填充新数据 // 但是随着数据量的增多 每一个index对应的bucket就会越来越长 也就会造成效率的降低 // 所以要在适当的时候对数组进行扩容 比如扩容两倍 // 但是这种情况下 所有的数据项一定要同时进行修改 重新调用hash函数获取不同位置 虽然耗时 但是如果扩容这是必须的 // 填充因子>0.75时扩充 // 实现扩容 取出所有桶和元素 插入新的storage中通过put插入 // 容量质数 容量最好是质数 虽然链地址法中没有开放地址法长度是质数重要 但是还是有好处的 // 如何判断一个数是质数?(面试题)质数特点:也称为素数 质数表示大于1的自然数中 只能被1和自己整除 // function isZhishu(num){ // 这样的写法效率低 // for(var i = 2; i < num; i++){ // if(num % i == 0){ // return false 不是质数 // } // } // return true 是质数 // 更高效的方式 不需要从2判断到num-1 判断开平方根 面试选这个 // var temp = parseInt(Math.sqrt(num))//求平方根 // for(var i = 2; i <= temp; i++){ // if(num % i == 0){ // return false // } // } // return true // } // 扩容操作 还需实现扩容大小limit恒为质数 hashTable.prototype.resize = function(newlimit){ // 1:保存旧storage let oldstorage = this.storage // 2:重置所有属性 this.count = 0 this.limit = newlimit // 3:遍历oldStorage中所有的bucket for(var i = 0; i < oldstorage.length; i++){ // 3.1:取出所有桶 var bucket = oldstorage[i] if(bucket == null){ continue } // 3.2buckey有数据重新插入 for(var j = 0; j < bucket.length; j++){ var tuple = bucket[j] // 4:插入新的storage this.put(tuple[0],tuple[1]) } } } // 判断是否是质数 hashTable.prototype.isZhishu = function(){ var temp = parseInt(Math.sqrt(num))//求平方根 for(var i = 2; i <= temp; i++){ if(num % i == 0){ return false } } return true } // 获取质数方法 hashTable.prototype.getZhishu = function(num){ // 判断num是否是质数 如果不是向后查找 while(!this.isZhishu(num)){ num++ } return num } } let table = new hashTable() // 最后的形式 [[[key,value],[key,value],[key,value]],[[key,value],[key,value]],[]] // 测试hash表 table.put('abc','321') table.put('nba','521') table.put('mba','1') table.put('aaa','13') table.put('bba','12') console.log('获取',table.get('abc')) console.log('删除',table.remove('bba')) console.log('为空',table.isEmpty()) console.log('size长度',table.size()) console.log(table)
|