散列是一种常用的数据存储技术,散列后的数据可以快速的插入或取用。
散列使用的数据结构叫做 散列表。
在散列表上插入、删除和取用数据都非常的快,但是对于查找操作来说却效率低下,比如查找一组数据中的最大值和最小值。这些操作得求助于其他的数据结构,二叉查找树就是一个很好的选择。
1、散列概览
我们的散列表是基于数组进行设计的。数组的长度是预先设定的。如果需要,可以随时增加。所有的元素根据和该元素对应的键,保存在数组的特定位置。
使用散列表存储数据时,通过一个散列函数将键映射为一个数字,这个数字的范围是0到散列表的长度。
理想情况下,散列函数会将每个键值映射为一个唯一的数组索引。然而,键的数量是无限 的,数组的长度是有限的(理论上,在 JavaScript 中是这样),一个更现实的目标是让散列 函数尽量将键均匀地映射到数组中。
即使使用一个高效的散列函数,仍然存在将两个键映射成同一个值的可能,这种现象称为 碰撞(collision),当碰撞发生时,我们需要有方案去解决。
对数组大小常见的限制是:数组长度应该是一个质数。在实现各种散列函数时我们将讨论为什么要求数组长度为质数。
以一个小型电话号码簿为例,阐释了散列的概念。
2、 HashTable类
我们使用一个类来表示散列表,该类包含计算散列值的方法、向散列中插入数据的方法、 从散列表中读取数据的方法、显示散列表中数据分布的方法,以及其他一些可能会用到的 工具方法。
HashTable 类的构造函数定义如下:
//构造函数定义
function HashTable() {
this.table = new Array(137);
this.simpeHash = simpeHash;
this.showDistro = showDistro;
this.put = put;
// this.get = get
}
get() 方法暂时被注释掉,稍后将描述该方法的定义。
2.1 一个简单的散列函数
散列函数的选择依赖于键值的数据类型。如果键是整型,最简单的散列函数就是以数组的 长度对键取余。在一些情况下,比如数组的长度是 10,而键值都是 10 的倍数时,就不推 荐使用这种方式了。这也是数组的长度为什么要是质数的原因之一,就像我们在上个构造 函数中,设定数组长度为 137 一样。如果键是随机的整数,则散列函数应该更均匀地分布这些键。这种散列方式称为除留余数法。
在很多应用中,键是字符串类型。事实证明,选择针对字符串类型的散列函数是很难的,选择时必须加倍小心。
乍一看,将字符串中每个字符的 ASCII 码值相加似乎是一个不错的散列函数。这样散列值就是 ASCII 码值的和除以数组长度的余数。该散列函数的定义如下:
//构造函数定义
function HashTable() {
this.table = new Array(137);
this.simpeHash = simpeHash;
this.showDistro = showDistro;
this.put = put;
// this.get = get
}
function simpeHash(data) { // 选择一个散列函数
var total = 0;
for (var i = 0; i < data.length; ++i) {
total += data.charCodeAt(i) //charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。
}
return total % this.table.length;
}
function put(data) { //将数据存入散列表
var pos = this.simpeHash(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])
}
}
}
var someNames = ["David", "Jennifer", "Donnie", "Raymond", "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
var hTable = new HashTable();
for (var i = 0; i < someNames.length; ++i) {
hTable.put(someNames[i]);
}
hTable.showDistro();
// 35: Cynthia
// 45: Clayton
// 57: Donnie
// 77: David
// 95: Danny
// 116: Mike
// 132: Jennifer
// 134: Jonathan
simpleHash() 函数通过使用 JavaScript 的 charCodeAt() 函数,返回每个字符的 ASCII 码值, 然后再将它们相加得到散列值。put() 方法通过调用 simpleHash() 函数得到数组的索引, 然后将数据存储到该索引对应的位置上。你会发现,数据并不是均匀分布的,人名向数组 的两端集中。
比起这种不均匀的分布,还有一个更严重的问题。如果你仔细观察输出,会发现初始数组 中的人名并没有全部显示。给 simpleHash() 函数加入一条 print() 语句,来仔细分析一下 这个问题:
再次运行程序,得到的输出如下:
Hash value: David -> 488 Hash value: Jennifer -> 817 Hash value: Donnie -> 605 Hash value: Raymond -> 730 Hash value: Cynthia -> 720 Hash value: Mike -> 390 Hash value: Clayton -> 730 Hash value: Danny -> 506 Hash value: Jonathan -> 819 现在真相大白了:字符串 "Clayton" 和 "Raymond" 的散列值是一样的!一样的散列值引发 了碰撞,因为碰撞,只有 "Clayton" 存入了散列表。可以通过改善散列函数来避免碰撞,
2.2、 一个更好的散列函数
为了避免碰撞,首先要确保散列表中用来存储数据的数组其大小是个质数。这一点很关 键,这和计算散列值时使用的取余运算有关。数组的长度应该在 100 以上,这是为了让数 据在散列表中分布得更加均匀。通过试验我们发现,比 100 大且不会让例 8-1 中的数据产 生碰撞的第一个质数是 137。使用其他更接近 100 的质数,在该数据集上依然会产生碰撞。
为了避免碰撞,在给散列表一个合适的大小后,接下来要有一个计算散列值的更好方法。 霍纳算法很好地解决了这个问题。本书不会过多深入该算法的数学细节,在此算法中,新 的散列函数仍然先计算字符串中各字符的 ASCII 码值,不过求和时每次要乘以一个质数。 大多数算法书建议使用一个较小的质数,比如 31,但是对于我们的数据集,31 不起作用, 我们使用 37,这样刚好不会产生碰撞。
//现在我们有了一个使用霍纳算法的更好的散列函数:
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);
}
拥有更好散列函数的betterHash()的hashTable类
//构造函数定义
function HashTable() {
this.table = new Array(137);
this.simpeHash = simpeHash;
this.betterHash = betterHash;
this.showDistro = showDistro;
this.put = put;
// this.get = get
}
// 使用霍纳算法的散列函数
function betterHash(string) {
const H = 37;
var total = 0;
for (var i = 0; i < string.length; ++i) {
total += H * total + string.charCodeAt(i);
}
total = total % this.table.length;
if (total < 0) {
total += this.table.length - 1;
}
return parseInt(total);
}
function simpeHash(data) { // 一个简单的选择散列函数
var total = 0;
for (var i = 0; i < data.length; ++i) {
total += data.charCodeAt(i) //charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。
}
return total % this.table.length;
}
function put(data) { //将数据存入散列表
var pos = this.betterHash(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])
}
}
}
var someNames = ["David", "Jennifer", "Donnie", "Raymond", "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
var hTable = new HashTable();
for (var i = 0; i < someNames.length; ++i) {
hTable.put(someNames[i]);
}
hTable.showDistro();
// 12: Jennifer
// 22: Raymond
// 55: Donnie
// 58: Clayton
// 80: Jonathan
// 82: Mike
// 103: Cynthia
// 110: Danny
2.4 对散列表排序、从散列表中取值
修改 put() 方法,使得该方法同时接受键和数据作为参数,对键值散列后,将数据存储到散列 表中。重新定义的 put() 方法如下:
function put(key, data) {
var pos = this.betterHash(key);
this.table[pos] = data;
}
put() 方法将键值散列化后,将数据存储到散列化后的键值对应在数组中的位置上。
完整的
//构造函数定义
function HashTable() {
this.table = new Array(137);
this.simpeHash = simpeHash;
this.betterHash = betterHash;
this.showDistro = showDistro;
this.put = put;
this.get = get
}
// 使用霍纳算法的散列函数
function betterHash(string) {
const H = 37;
var total = 0;
for (var i = 0; i < string.length; ++i) {
total += H * total + string.charCodeAt(i);
}
total = total % this.table.length;
if (total < 0) {
total += this.table.length - 1;
}
return parseInt(total);
}
// 一个简单的选择散列函数
function simpeHash(data) {
var total = 0;
for (var i = 0; i < data.length; ++i) {
total += data.charCodeAt(i) //charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。
}
return total % this.table.length;
}
//将数据存入散列表
function put(key, data) {
var pos = this.betterHash(key);
this.table[pos] = data;
}
// 读取存储在散列表中的数据
function get(key) {
return this.table[this.betterHash(key)];
}
// 显示散列表中的数据
function showDistro() {
var n = 0;
for (var i = 0; i < this.table.length; ++i) {
if (this.table[i] != undefined) {
print(i + ":" + this.table[i])
}
}
}
var pnumbers = new HashTable();
var name, number;
pnumbers.put('wang', 18866666666)
pnumbers.put('hong', 19966666666)
pnumbers.put('pu', 10066666666)
print(pnumbers.get('hong')) /// 19966666666
pnumbers.showDistro()
// 27: 18866666666
// 120: 19966666666
// 126: 10066666666
3、 碰撞处理
当散列函数对于多个输入产生同样的输出时,就产生了碰撞。散列算法的第二部分就将介 绍如何解决碰撞,使所有的键都得以存储在散列表中。本节将讨论两种碰撞解决办法:开 链法和线性探测法。
1 开链法
开链法是指实现散列表的底层数组中,每个数组元素有事一个新的数据结构,比如另一个数组,这样就能够存储多个键了。
使用这样的技术,即使两个键散列后的值相同,依然呗保存在同样的位置,只不过他们在第二个数组中的位置不一样罢了
实现开链法的方法是:在创建存储散列过的键值的数组时,通过调用一个函数创建一个新的空数组,然后将该数组赋给散列表里的每个数组元素。这样就穿件了一个二维数组。
定义一个函数 buildChains()用来创建第二组数组,我们也称这个数组为链。
// 创建链
function buildChains(){
for(var i =0;i<this.table.length;++i){
this.table[i] = new Array()
}
}
考虑到散列表现在使用多维数组存储数据,为了更好地显示使用了开链法后键值的分布,
// 显示散列表中的数据
function showDistro() {
var n = 0;
for (var i = 0; i < this.table.length; ++i) {
if (this.table[i][0] != undefined) {
print(i + ": " + this.table[i]);
}
}
}
使用了开链法后,要重新定义 put() 和 get() 方法。
put() 方法将键值散列,散列后的值对 应数组中的一个位置,先尝试将数据放到该位置上的数组中的第一个单元格,如果该单元 格里已经有数据了,put() 方法会搜索下一个位置,直到找到能放置数据的单元格,并把 数据存储进去。实现 put() 方法的代码如下:
function put(key, data) {
var pos = this.betterHash(key);
var index = 0;
if (this.table[pos][index] == undefined) {
this.table[pos][index+1] = data;
}
++index;
else {
while (this.table[pos][index] != undefined) { ++index;
}
this.table[pos][index+1] = data; }
}
前面的例子只保存数据,新的 put() 方法则不同,它既保存数据,也保存键值。该方法使
用链中两个连续的单元格,第一个用来保存键值,第二个用来保存数据。
get() 方法先对键值散列,根据散列后的值找到散列表中相应的位置,然后搜索该位置上 的链,直到找到键值。如果找到,就将紧跟在键值后面的数据返回;如果没找到,就返回 undefined。代码如下:
// 读取存储在散列表中的数据
function get(key) {
var index = 0;
var hash = this.betterHash(key);
if (this.table[hash][index] = key) {
return this.table[hash][index + 1];
}
index += 2;
else {
while (this.table[hash][index] != key) {
index += 2;
}
return this.table[hash][index + 1];
}
return undefined;
// return this.table[this.betterHash(key)];
}
我这个两个例子 没跑通 回头重新定义下 get() 和put()
3.2 线性探测法
第二种处理碰撞的方法是线性探测法。线性探测法隶属于一种更一般化的散列技术:开放 寻址散列。当发生碰撞时,线性探测法检查散列表中的下一个位置是否为空。如果为空, 就将数据存入该位置;如果不为空,则继续检查下一个位置,直到找到一个空的位置为 止。该技术是基于这样一个事实:每个散列表都会有很多空的单元格,可以使用它们来存 储数据。
当存储数据使用的数组特别大时,选择线性探测法要比开链法好。这里有一个公式,常常 可以帮助我们选择使用哪种碰撞解决办法:如果数组的大小是待存储数据个数的 1.5 倍, 那么使用开链法;如果数组的大小是待存储数据的两倍及两倍以上时,那么使用线性探 测法。
为了说明线性探测法的工作原理,可以重写 put() 和 get() 方法。为了实现一个真实的 数据存取系统,需要为 HashTable 类增加一个新的数组,用来存储数据。数组 table 和 values 并行工作,当将一个键值保存到数组 table 中时,将数据存入数组 values 中相应的 位置上。
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;
}
}
get() 方法先搜索键在散列表中的位置,如果找到,则返回数组 values 中对应位置上的数 据;如果没有找到,则循环搜索,直到找到对应的键或者数组中的单元为 undefined 时, 后者表示该键没有被存入散列表。代码如下:
function get(key) {
var hash = -1;
hash = this.betterHash(key);
if (hash > -1) {
for (var i = hash; this.table[hash] != undefined; i++) {
if (this.table[hash] == key) {
return this.values[hash];
}
}
}
return undefined;
}
emmmmm 先整理这 两个方法好像都有坑 没有跑通。。。。 线下班回家