• JS中数据结构之散列表


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

    下面的散列表是基于数组进行设计的,数组的长度是预先设定的,如有需要,可以随时增加。所有元素根据和该元素对应的键,保存在数组的特定位置。使用散列表存储数据时,通过一个散列函数将键映射为一个数字,这个数字的范围是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;
    }
  • 相关阅读:
    MyBatis框架——MyBatis update标签(转载)
    关“.NET研究”于代码规范 狼人:
    go并发写入文件的几种姿势
    go切片默认初始化len为0,既元素无法直接赋值为[0]=xx
    js对象
    go sync.WaitGroup
    go微服务学习
    修改 Kubeadm 源码中的 Kubernetes 证书过期时间为100年
    go的形参
    go 循环切片得到重复移除成别名
  • 原文地址:https://www.cnblogs.com/wenxuehai/p/10288785.html
Copyright © 2020-2023  润新知