JavaScript------散列学习

散列是一种常用的数据存储技术,散列后的数据可以快速的插入或取用。

散列使用的数据结构叫做 散列表。

在散列表上插入、删除和取用数据都非常的快,但是对于查找操作来说却效率低下,比如查找一组数据中的最大值和最小值。这些操作得求助于其他的数据结构,二叉查找树就是一个很好的选择。

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 先整理这  两个方法好像都有坑 没有跑通。。。。  线下班回家

猜你喜欢

转载自blog.csdn.net/wanghongpu9305/article/details/110372051
今日推荐