资讯专栏INFORMATION COLUMN

学习数据结构与算法之字典和散列表

Heier / 1146人阅读

摘要:小结实现了字典和哈希表感觉没有想象中那么困难,当然这还是开始。

本系列所有文章:
第一篇文章:学习数据结构与算法之栈与队列
第二篇文章:学习数据结构与算法之链表
第三篇文章:学习数据结构与算法之集合
第四篇文章:学习数据结构与算法之字典和散列表
第五篇文章:学习数据结构与算法之二叉搜索树

字典

不是新华字典哦,这里的字典也称作_映射_,是一种数据结构,跟set集合很相似的一种数据结构,都可以用来存储无序不重复的数据。不同的地方是集合以[值,值]的形式存储,而字典则是以[键,值]或者叫作{key: value}的形式。

用JavaScript实现字典

先实现一个构造函数:

function Dictionary () {
  var items = {}
}

字典需要实现以下方法:

set(key, value):向字典中添加新元素

remove(key):通过使用key来从字典中移除对应元素

has(key):通过key来判断字典中是否有该元素

get(key):通过key来找到特定的数值并返回

clear():清空字典

size():返回字典包含元素的数量

keys():返回字典所包含的所有键名,以数组形式返回

values():同上一个方法一样,只不过键名换成了数值,也是以数组形式返回

实现has

因为后面的方法都要用到has,所以先实现这个方法

this.has = function (key) {
  // 书上用的是in操作符来判断,但是in对于继承来的属性也会返回true,所以我换成了这个
  return items.hasOwnProperty(key)
}
实现set

没啥好说的,简单的赋值

this.set = function (key, value) {
  items[key] = value
}
实现remove

首先判断key是否属于该字典,然后再删除

this.remove = function (key) {
  if (this.has(key)) {
    delete items[key]
    return true
  }
  return false
}
实现values

返回由数值组成的数组

this.values = function () {
  var values = []
  for (var k in items) {
    if (items.has(k)) {
      values.push(items[k])
    }
  }
  return values
}
实现其他的方法

其他的比较简单,和集合的方法实现类似,我就直接贴源代码了

this.keys = function () {
  return Object.keys(items)
}

this.size = function () {
  return Object.keys(items).length
}

this.clear = function () {
  items = {}
}

this.getItems = function () {
  return items
}

this.get = function (key) {
  return this.has(key) ? items[key] : undefined 
}

源代码在此:

字典的实现-源代码

散列表

关于散列表的定义,这里摘抄一下维基百科的解释:

散列表Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表

可以理解为,数据中的键通过一个散列函数的计算后返回一个数据存放的地址,因此可以直接通过地址来访问它的值,这样的查找就非常快。

用JavaScript实现散列表

先看这个构造函数,注意:存储数据用的是数组,因为寻址访问元素最方便的还是数组了。

function HashTable () {
    var table = []
}

需要实现的方法:

put(key, value):向散列表增加一个新的项

remove(key):根据键值从散列表中移除值

get(key):返回根据键值检索到的特定的值

先实现散列函数

把键名的每个字母转成ASCII码再相加起来,最后和一个任意的数求余,得到数据存储的地址。

var loseloseHashCode = function (key) {
  var hash = 0
  for (var i = 0; i < key.length; i++) {
    hash += key.charCodeAt(i)
  }
  return hash % 37
}
简单实现

因为这里的方法比较简单,我就直接全贴上来了

this.put = function (key, value) {
  var position = loseloseHashCode(key)
  console.log(position + " - " + key)
  table[position] = value
}

this.remove = function (key) {
  table[loseloseHashCode(key)] = undefined
}

this.get = function (key) {
  return table[loseloseHashCode(key)]
}

// 用来输出整个散列表,查看结果用的
this.print = function () {
  for (var i = 0; i < table.length; i++) {
    if (table[i] !== undefined) {
      console.log(i + ": " + table[i])
    }
  }
}

稍等,还没完呢,现在看似很完美,我们调用一下刚才写的:

var hash = new HashTable()
hash.put("Gandalf", "gandalf@email.com")
hash.put("John", "johnsnow@email.com")
hash.put("Tyrion", "tyrion@email.com")

// 输出结果
// 19 - Gandalf
// 29 - John
// 16 - Tyrion

好像没什么不对,但是仔细想想:如果在某些情况下,散列函数根据传入的键计算得到的地址是一样的会怎么样呢?

看看下面这个例子:

var hash = new HashTable()

hash.put("Gandalf", "gandalf@email.com")
hash.put("John", "john@email.com")
hash.put("Tyrion", "tyrion@email.com")
hash.put("Aaron", "aaron@email.com")
hash.put("Donnie", "donnie@email.com")
hash.put("Ana", "ana@email.com")
hash.put("Jonathan", "jonathan@email.com")
hash.put("Jamie", "jamie@email.com")
hash.put("Sue", "sue@email.com")
hash.put("Mindy", "mindy@email.com")
hash.put("Paul", "paul@email.com")
hash.put("Nathan", "nathan@email.com")

// 输出结果
// 19 - Gandalf
// 29 - John
// 16 - Tyrion
// 16 - Aaron
// 13 - Donnie
// 13 - Ana
// 5 - Jonathan
// 5 - Jamie
// 5 - Sue
// 32 - Mindy
// 32 - Paul
// 10 - Nathan

这种情况就比较复杂了:Tyrion和Aaron的值都是16,Donnie和Ana都是13,还有其他很多重复的值,这时散列表表中是什么情况呢

hash.print()

// 输出结果
// 5: sue@email.com
// 10: nathan@email.com
// 13: ana@email.com
// 16: aaron@email.com
// 19: gandalf@email.com
// 29: john@email.com
// 32: paul@email.com

很明显,后面的元素会覆盖前面的元素,但这样是不行的,要想办法解决冲突

解决冲突

目前主流的方法主要是两种:分离链接法和线性探查法,这里就简单介绍一下分离链接法。

分离链接法

思路很简单:你不是重复了吗,那我就在同一个位置里面放一个链表,重复的数据全都放在链表里面,你要找数据就在链表里面找。

如果不理解,可以参见下图:

(图片来源谷歌搜索,侵删)

根据这个思路,我们重新实现一下三个方法,不过在这之前,我们需要一个对象来保存键值对

var ValuePair = function (key, value) {
  this.key = key
  this.value = value
  
  // 重写toString主要是方便输出查看
  this.toString = function () {
    return "[" + this.key + " - " + this.value + "]"
  }
}

接下来就是重写方法了

this.put = function (key, value) {
  var position = loseloseHashCode(key)
  // 如果发现该位置还没有元素,就先放一个链表,再用append加进去
  if (table[position] === undefined) {
    // 因为使用node执行文件,这里LinkedList是我在顶部用require引入的LinkedList.js
    table[position] = new LinkedList()
  }
  table[position].append(new ValuePair(key, value))
}

this.get = function (key) {
  var position = loseloseHashCode(key)

  if (table[position] !== undefined) {
    var current = table[position].getHead()

    // 遍历链表查找值
    while (current.next) {
      if (current.element.key === key) {
        return current.element.value
      }
      current = current.next
    }

    // 检查元素如果是最后一个的情况
    if (current.element.key === key) {
      return current.element.value
    }
  }

  return undefined
}

this.remove = function (key) {
  var position = loseloseHashCode(key)

  if (table[position] !== undefined) {
    var current = table[position].getHead()

    // 遍历查找值
    while (current.next) {
      if (current.element.key === key) {
        // 使用链表的remove方法
        table[position].remove(current.element)
        // 当链表为空了,就把散列表该位置设为undefined
        if (table[position].isEmpty()) {
          table[position] = undefined
        }
        return true
      }
      current = current.next
    }

    if (current.element.key === key) {
      table[position].remove(current.element)
      if (table[position].isEmpty()) {
        table[position] = undefined
      }
      return true
    }
  }

  return false
}

以上就是用分离链接法重写的哈希表。

线性探查法

第二种办法思路更粗暴:你不是占了这个位置嘛,那我就占下一个,下个位置还被占了?那就占再下一个~

具体的实现我就不贴出来了,读者可以自行思考并实现,然后对照代码看看。

这里先把源代码放出来了

哈希表的实现-源代码

创建更好的散列函数

以上是哈希表的两个冲突解决办法,但实际上应用哈希表的时候能避免冲突就尽量避免冲突,一开始的散列函数不是一个好的函数,因为冲突太多了,这里就贴书上的一个不错的散列函数(社区),原理大致是:相加所得的hash数要够大,且尽量为质数,用hash与另一个大于散列表大小的质数做求余,这样得到的地址也能尽量不重复。

var djb2HashCode = function (key) {
  var hash = 5381
  for (var i = 0; i < key.length; i++) {
    hash = hash * 33 + key.charCodeAt(i)
  }
  return hash % 1013
}
小结

实现了字典和哈希表感觉没有想象中那么困难,当然这还是开始。

不过,这几天自己不断地研究数据结构,也让自己对于JS的理解进一步加深了。继续努力吧~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87227.html

相关文章

  • 《Javascript数据结构算法》笔记-「字典和散列表

    摘要:我经常在业务代码中把数据处理成这种字典的数据结构获取的方法哈希表在学习了类之后,我们会学习散列表,也就是哈希表。 《Javascript数据结构和算法》笔记-「字典和散列表」 集合、字典、散列表存储的都是「不重复」的数据结构 集合:我们更关注每一个元素的值,并把其作为主要元素 字典:我们用[键,值]的形式来存储数据 散列表: 跟字典类似,也会是用[键,值]的形式来存储数据 但是「字...

    wenyiweb 评论0 收藏0
  • 《JavaScript数据结构算法》笔记——第7章 字典和散列表

    摘要:在字典中,存储的是键,值,集合可以看作值,值的形式存储元素,字典也称为映射方法描述备注向字典中添加新元素通过某个键值从字典中移除对应的数据值判断某个键值是存在于这个字典中通过键值获取对应的数据值返回字典所有元素的数量删除字典中所有元素将字典 在字典中,存储的是[键,值],集合可以看作[值,值]的形式存储元素,字典也称为映射 方法 描述 备注 set(key,...

    zorro 评论0 收藏0
  • 每周一练 数据结构算法(Dictionary 和 HashTable)

    摘要:什么是散列表和散列函数哈希表,也叫散列表,是根据关键码值而直接进行访问的数据结构。根据键值从散列表中移除值。请实现散列表将和存在一个对象中即可定义一个包含和属性的类并分配到散列表。 showImg(https://segmentfault.com/img/remote/1460000019005270); 这是第五周的练习题,上周忘记发啦,这周是复习 Dictionary 和 Hash...

    eternalshallow 评论0 收藏0
  • 每周一练 数据结构算法(Dictionary 和 HashTable)

    摘要:什么是散列表和散列函数哈希表,也叫散列表,是根据关键码值而直接进行访问的数据结构。将字典的所有键名以数组的形式返回。根据键值从散列表中移除值。这是第五周的练习题,上周忘记发啦,这周是复习 Dictionary 和 HashTable。 下面是之前分享的链接: 1.每周一练 之 数据结构与算法(Stack) 2.每周一练 之 数据结构与算法(LinkedList) 3.每周一练 之 数据结构...

    ingood 评论0 收藏0
  • CSS技巧

    摘要:技巧使你的更加专业这是上关于技巧的一篇译文,另外你也可以在本项目看到原文。列举了一些很实用的技巧,比如给空内容的标签添加内容,逗号分隔列表等等。排序算法看源码,把它背下来吧排序算法的封装。主要帮助初学者更好的掌握排序算法的实现。 成为专业程序员路上用到的各种优秀资料、神器及框架 成为一名专业程序员的道路上,需要坚持练习、学习与积累,技术方面既要有一定的广度,更要有自己的深度。 Java...

    DangoSky 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<