5月阅读周·数据结构与算法JavaScript描述 | 散列,一种常用的数据存储技术

举报
叶一一 发表于 2024/05/28 09:32:08 2024/05/28
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读三个月。4月份的阅读计划有两本,《你不知道的JavaScrip》系列迎来收尾。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读四个月。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》

当前阅读周书籍《数据结构与算法JavaScript描述》

散列

散列概览

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

理想情况下,散列函数会将每个键值映射为一个唯一的数组索引。然而,键的数量是无限的,数组的长度是有限的(理论上,在JavaScript中是这样),一个更现实的目标是让散列函数尽量将键均匀地映射到数组中。

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

要确定的最后一个问题是:散列表中的数组究竟应该有多大?这是编写散列函数时必须要考虑的。对数组大小常见的限制是:数组长度应该是一个质数。

HashTable类

使用一个类来表示散列表,该类包含计算散列值的方法、向散列中插入数据的方法、从散列表中读取数据的方法、显示散列表中数据分布的方法,以及其他一些可能会用到的工具方法。

HashTable类的构造函数定义如下:

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

选择一个散列函数

散列函数的选择依赖于键值的数据类型。如果键是整型,最简单的散列函数就是以数组的长度对键取余。在一些情况下,比如数组的长度是10,而键值都是10的倍数时,就不推荐使用这种方式了。这也是数组的长度为什么要是质数的原因之一,就像我们在上个构造函数中,设定数组长度为137一样。如果键是随机的整数,则散列函数应该更均匀地分布这些键。这种散列方式称为除留余数法。

将字符串中每个字符的ASCII码值相加似乎是一个不错的散列函数。这样散列值就是ASCII码值的和除以数组长度的余数。该散列函数的定义如下:

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

给HashTable再增加两个方法:put()和showDistro(),一个用来将数据存入散列表,一个用来显示散列表中的数据,这样就初步实现了HashTable类,该类的完整定义如下:

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

function put(data) {
  var pos = this.simpleHash(data);
  this.table[pos] = data;
}

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

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()函数的工作原理:

load('HashTable.js');
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

碰撞处理

当散列函数对于多个输入产生同样的输出时,就产生了碰撞。散列算法的第二部分就将介绍如何解决碰撞,使所有的键都得以存储在散列表中。本节将讨论两种碰撞解决办法:开链法和线性探测法。

开链法

开链法是指实现散列表的底层数组中,每个数组元素又是一个新的数据结构,比如另一个数组,这样就能存储多个键了。使用这种技术,即使两个键散列后的值相同,依然被保存在同样的位置,只不过它们在第二个数组中的位置不一样罢了。

使用开链法避免碰撞:

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]);
    }
  }
}
load('HashTable.js');
var hTable = new HashTable();
hTable.buildChains();
var someNames = ['David', 'Jennifer', 'Donnie', 'Raymond', 'Cynthia', 'Mike', 'Clayton', 'Danny', 'Jonathan'];
for (var i = 0; i < someNames.length; ++i) {
  hTable.put(someNames[i]);
}
hTable.showDistro();

输出如下:

60: David
68: Jennifer
69: Mike
70: Donnie, Jonathan
78: Cynthia, Danny
88: Raymond, Clayton

线性探测法

线性探测法隶属于一种更一般化的散列技术:开放寻址散列。当发生碰撞时,线性探测法检查散列表中的下一个位置是否为空。如果为空,就将数据存入该位置;如果不为空,则继续检查下一个位置,直到找到一个空的位置为止。该技术是基于这样一个事实:每个散列表都会有很多空的单元格,可以使用它们来存储数据。

当存储数据使用的数组特别大时,选择线性探测法要比开链法好。这里有一个公式,常常可以帮助我们选择使用哪种碰撞解决办法:如果数组的大小是待存储数据个数的1.5倍,那么使用开链法;如果数组的大小是待存储数据的两倍及两倍以上时,那么使用线性探测法。

在put()方法中使用线性探测技术:

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;
}

总结

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


作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。