散列是一种常用的数据存储技术,散列后的数据可以快速地插入或取用。散列使用的数据 结构叫做散列表。在散列表上插入、删除和取用数据都非常快。

下面的散列表是基于数组进行设计的,数组的长度是预先设定的,如有需要,可以随时增加。所有元素根据和该元素对应的键,保存在数组的特定位置。使用散列表存储数据时,通过一个散列函数将键映射为一个数字,这个数字的范围是0到散列表的长度。

散列函数会将每个键值映射为一个唯一的数组索引。然而,键的数量是无限的,数组的长度是有限的,一个更现实的目标是让散列 函数尽量将键均匀地映射到数组中。

即使使用一个高效的散列函数,仍然存在将两个键映射成同一个值的可能,这种现象称为碰撞(collision),当碰撞发生时,我们需要利用一定的方法去解决碰撞。

对数组大小常见的限制是:数组长度应该是一个质数。

HashTable类

使用 HashTable 类来表示散列表,该类包含计算散列值的方法、向散列中插入数据的方法、 从散列表中读取数据的方法、显示散列表中数据分布等方法。

function HashTable() {
  this.table = new Array(137);
  this.simpleHash = simpleHash;
  this.showDistro = showDistro;
  this.put = put;
  this.get = get;
  this.buildChains = buildChains;
}

散列函数

散列函数的选择依赖于键值的数据类型。如果键是整型,最简单的散列函数就是以数组的长度对键取余,这种散列方式称为除留余数法。

选择针对字符串类型的散列函数比较困难

简单的散列函数:字符串中每个字符的 ASCII 码值相加然后再除以数组长度,将得出的余数做为散列值。

function simpleHash(data) {
  var total = 0;
  for (var i = 0; i < data.length; ++i) {
    total += data.charCodeAt(i);
  }
  return total % this.table.length;
}

put() 和 showDistro(),一个用来将数据存入散列表, 一个用来显示散列表中的数据

function put(data) {
  var pos = this.simpleHash(data);
  this.table[pos] = data;
}
function showDistro() {
  var n = 0;
  for (var i = 0; i < this.table.length; ++i) {
    if (this.table[i] != undefined) {
      print(i + ": " + this.table[i]);
    }
  }
}

使用简答的散列函数 simpleHash() 时数据并不是均匀分布的,而是向数组的两端集中,并且数据很大概率将会产生碰撞而不会全部显示出来。

更好的散列函数:霍纳算法是一种比较好的散列函数算法,计算时仍然先计算字符串中各字符的 ASCII 码值,不过求和时每次要乘以一个质数。

为了避免碰撞,首先要确保散列表中用来存储数据的数组其大小是个质数。这一点和计算散列值时使用的取余运算有关。数组的长度应该在 100 以上,这是为了让数据在散列表中分布得更加均匀。

function betterHash(string, arr) {
  const H = 37; //质数
  var total = 0;
  for (var i = 0; i < string.length; ++i) {
    total += H * total + string.charCodeAt(i);
  }
  total = total % arr.length;
  return parseInt(total);
}

接受键和数据作为参数的put() 方法

function put(key, data) {
  var pos = this.betterHash(key); //使用更好的散列函数
  this.table[pos] = data;
}

get() 方法读取存储在散列表中的数据

function get(key) {
  return this.table[this.betterHash(key)];
}

碰撞处理

当散列函数对于不同的输入产生同样的散列值时,就产生了碰撞。下面是两种碰撞解决办法:开链法和线性探测法。

开链法:当碰撞发生时,仍然将键存储到通过散列算法产生的索引位置上,但实际上,每个数组元素又是一个新的数据结构,比如另一个数组,这样就能存储多个键了(即用二维数组实现)。

buildChains() 函数创建二维数组

function buildChains() {
  for (var i = 0; i < this.table.length; ++i) {
    this.table[i] = new Array();
  }
}

使用了开链法后,要重新定义 put() 和 get() 方法。

新的put() 方法将键值散列,散列后的值对应数组中的一个位置,先尝试将数据放到该位置上的数组中的第一个单元格,如果该单元格里已经有数据了,put() 方法会搜索下一个位置,直到找到能放置数据的单元格,并把数据存储进去。

它既保存数据,也保存键值。该方法使用链中两个连续的单元格,第一个用来保存键值,第二个用来保存数据。

function put(key, data) {
var pos = this.betterHash(key);
var index = 0;
if (this.table[pos][index] == undefined) {
this.table[pos][index] = key;
this.table[pos][index + 1] = data;
} else {
while (this.table[pos][index] != undefined) {
++index;
}
this.table[pos][index] = key;
this.table[pos][index + 1] = data;
}
}

新的 get() 方法先对键值散列,根据散列后的值找到散列表中相应的位置,然后搜索该位置上的链,直到找到键值。如果找到,就将紧跟在键值后面的数据返回;如果没找到,就返回 undefined

function get(key) {
var index = 0;
var pos = this.betterHash(key);
if (this.table[pos][index] == key) {
return this.table[pos][index + 1];
} else {
while (this.table[pos][index] != key) {
index += 2;
}
return this.table[pos][index + 1];
}
return undefined;
}

线性探测法:线性探测法隶属于一种更一般化的散列技术:开放寻址散列。当发生碰撞时,线性探测法检查散列表中的下一个位置是否为空。如果为空, 就将数据存入该位置;如果不为空,则继续检查下一个位置,直到找到一个空的位置为止。

当存储数据使用的数组特别大时,选择线性探测法要比开链法好。如果数组的大小是待存储数据个数的 1.5 倍, 那就使用开链法;如果数组的大小是待存储数据的两倍及两倍以上时,那么使用线性探测法。

使用线性探测法需要为 HashTable 类增加一个新的数组,用来存储数据。数组 table 和 values 并行工作,当将一个键值保存到数组 table 中时,将数据存入数组 values 中相应的 位置上。即在 HashTable 的构造函数中加入下面一行代码: this.values = []

重写 put() 和 get() 方法。

function put(key, data) {
var pos = this.betterHash(key);
if (this.table[pos] == undefined) {
this.table[pos] = key;
this.values[pos] = data;
} else {
while (this.table[pos] != undefined) {
pos++;
}
this.table[pos] = key;
this.values[pos] = data;
}
} function get(key) {
var hash = this.betterHash(key);
for (var i = hash; this.table[hash] != undefined; i++) {
if (this.table[hash] == key) {
return this.values[hash];
}
}
return undefined;
}

JS中数据结构之散列表的更多相关文章

  1. Python与数据结构[4] -> 散列表[0] -> 散列表与散列函数的 Python 实现

    散列表 / Hash Table 散列表与散列函数 散列表是一种将关键字映射到特定数组位置的一种数据结构,而将关键字映射到0至TableSize-1过程的函数,即为散列函数. Hash Table: ...

  2. Nginx数据结构之散列表

    1. 散列表(即哈希表概念) 散列表是根据元素的关键码值而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录, 以加快查找速度.这个映射函数 f 叫做散列方法,存放记录的数 ...

  3. 【PHP数据结构】散列表查找

    上篇文章的查找是不是有意犹未尽的感觉呢?因为我们是真真正正地接触到了时间复杂度的优化.从线性查找的 O(n) 直接优化到了折半查找的 O(logN) ,绝对是一个质的飞跃.但是,我们的折半查找最核心的 ...

  4. Python与数据结构[4] -> 散列表[2] -> 开放定址法与再散列的 Python 实现

     开放定址散列法和再散列 目录 开放定址法 再散列 代码实现 1 开放定址散列法 前面利用分离链接法解决了散列表插入冲突的问题,而除了分离链接法外,还可以使用开放定址法来解决散列表的冲突问题. 开放定 ...

  5. Python与数据结构[4] -> 散列表[1] -> 分离链接法的 Python 实现

    分离链接法 / Separate Chain Hashing 前面完成了一个基本散列表的实现,但是还存在一个问题,当散列表插入元素冲突时,散列表将返回异常,这一问题的解决方式之一为使用链表进行元素的存 ...

  6. jdk1.8HashMap底层数据结构:散列表+链表+红黑树,jdk1.8HashMap数据结构图解+源码说明

    一.前言 本文由jdk1.8源码整理而得,附自制jdk1.8底层数据结构图,并截取部分源码加以说明结构关系. 二.jdk1.8 HashMap底层数据结构图 三.源码 1.散列表(Hash table ...

  7. JS中数据结构之列表

    列表是一组有序的数据.每个列表中的数据项称为元素.在 JavaScript 中,列表中的元素可以是任意数据类型.列表中可以保存多少元素并没有事先限定并可以不断壮大,实际使用时元素的数量受到程序内存的限 ...

  8. JS中数据结构之链表

    1.链表的基本介绍 数组不总是组织数据的最佳数据结构,在很多编程语言中,数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难.在数组中,添加和删除元素也很麻烦,因为需要将数组中的 ...

  9. JS中数据结构之栈

    1.栈的基本介绍 栈是一种高效的数据结构,因为数据只能在栈顶添加或删除,所以这样的操作很快,而且容易实现. 栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶.栈被称为一种后入先出( ...

随机推荐

  1. JavaScript 表单验证正则表达式大全

    JavaScript 表单验证正则表达式大全[转载] 匹配中文字符的正则表达式: [u4e00-u9fa5] 评注:匹配中文还真是个头疼的事,有了这个表达式就好办了 匹配双字节字符(包括汉字在内):[ ...

  2. JavaScript 高级程序设计(第3版)第一章 (js简介)

    1.我比js早一年 2.web浏览器是ECMAScript实现的宿主环境之一. 其它实现ECMAScript的宿主环境包括Node和Adobe Flash 3.ECMAScript主要规定js的组成部 ...

  3. spring-第一篇之spring核心机制依赖注入(DI)/控制翻转(IoC)

    1.spring的核心机制:依赖注入(DI)/控制翻转(IoC) 什么是依赖:A对象需要调用B对象,所以A依赖于B. 什么是注入:A对象注入一个属性B对象. 什么是依赖注入(DI):A对象依赖于B对象 ...

  4. mysql 主从复制 (1)

    Mysql主从配置 大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据 ...

  5. BZOJ 3331 (Tarjan缩点+树上差分)

    题面 传送门 分析 用Tarjan求出割点,对点-双连通分量(v-DCC)进行缩点,图会变成一棵树 注意v-DCC的缩点和e-DCC不同,因为一个割点可能属于多个v-DCC 设图中共有p个割点和t个v ...

  6. 使用redis来存储session,不同框架对session的命名规则是不一样的

    今天做了一个测试,在同一个云服务器上,搭建了两个server,其中一个是用laravel框架写的,另外一个使用原生php开发的,为了提高访问的速度,使用云服务器中的redis来存储session数据, ...

  7. Linux服务器安全配置小结(转)

    众所周知,网络安全是一个非常重要的课题,而服务器是网络安全中最关键的环节.Linux被认为是一个比较安全的Internet服务器,作为一种开放源代码操作系统,一旦Linux系统中发现有安全漏洞,Int ...

  8. 【转】通俗理解Java序列化与反序列化

    一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化. 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存 ...

  9. 在vue中设计一个客户签名的功能

    直接贴代码: <template> <div class="hello"> <p>签字:</p> <canvas id=&qu ...

  10. 【问题解决方案】在某个文件夹下打开命令提示符或Git Bash

    参考链接: 百度知道:怎么在某个文件夹下打开命令提示符 问题: 当文件夹比较深时,一直cd进入文件夹内部就显得非常迟缓了. 解决: cmd:打开所需文件夹路径后,在上面的路径显示框中输入CMD,然后回 ...