本文参考文献:https://www.cnblogs.com/AhuntSun-blog/p/12636718.html
配套视频教程:https://www.bilibili.com/video/BV1r7411n7Pw?p=1&spm_id_from=pageDriver
目录
数据结构-二
哈希表
简介
哈希表通常是基于数组实现的,但是相对于数组,它存在更多优势:
- 哈希表可以提供非常快速的插入-删除-查找操作;
- 无论多少数据,插入和删除值都只需要非常短的时间,即**O(1)**的时间级。实际上,只需要几个机器指令即可完成;
- 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。但是相对于树来说编码要简单得多。
哈希表同样存在不足之处:
- 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。
- 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
- 从上述两个缺点可以知道,为什么一般采用哈希表来实现集合、字典
哈希表是什么?
- 哈希表并不好理解,不像数组、链表和树等可通过图形的形式表示其结构和原理。
- 哈希表的结构就是数组,但它神奇之处在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取HashCode。
通过以下案例了解哈希表:
案例一:公司想要存储1000个人的信息,每一个工号对应一个员工的信息。若使用数组,增删数据时比较麻烦;使用链表,获取数据时比较麻烦。有没有一种数据结构,能把某一员工的姓名转换为它对应的工号,再根据工号查找该员工的完整信息呢?没错此时就可以使用哈希表的哈希函数来实现。
案例二:存储联系人和对应的电话号码:当要查找张三(比如)的号码时,若使用数组:由于不知道存储张三数据对象的下标值,所以查找起来十分麻烦,使用链表时也同样麻烦。而使用哈希表就能通过哈希函数把张三这个名称转换为它对应的下标值,再通过下标值查找效率就非常高了。
总结:哈希表最后还是基于数据来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值,建立字符串和下标值的对应关系。即:哈希表就是一个数组,但是其索引可以和每个元素的部分数据构建对应关系,使得每当要查询数据时,可以根据要查数据的值来找到其在哈希表中的索引,从而实现快速的查找。
哈希化的方法
常见的字符集编码方式有 ASCII、GB2312、GBK、UTF-8等
为了把字符串转化为对应的下标值,需要有一套编码系统,为了方便理解我们创建这样一套编码系统:比如a为1,b为2,c为3,以此类推z为26,空格为27(不考虑大写情况)。
有了编码系统后,将字母转化为数字也有很多种方式:(下面两个方式基于字母替换为相应数字)
-
方式一:数字相加。例如cats转化为数字:3+1+20+19=43,那么就把43作为cats单词的下标值储存在数组中;
但是这种方式会存在这样的问题:很多的单词按照该方式转化为数字后都是43,比如was。而在数组中一个下标值只能储存一个数据,即不同单词编码后的数字结果冲突,所以该方式不合理。
-
方式二:幂的连乘。我们平时使用的大于10的数字,就是用幂的连乘来表示它的唯一性的。比如:
6543=6 * 10^3 + 5 * 10^2 + 4 * 10 + 3
;这样单词也可以用该种方式来表示:cats = 3 * 27^3 + 1 * 27^2 + 20 * 27 + 17 =60337
;虽然该方式可以保证字符的唯一性,但是如果是较长的字符(如aaaaaaaaaa)所表示的数字就非常大,此时要求很大容量的数组,然而其中却有许多下标值指向的是无效的数据(比如不存在zxcvvv这样的单词),造成了数组空间的浪费。
两种方案总结:
- 第一种方案(让数字相加求和)产生的数组下标太少;
- 第二种方案(与27的幂相乘求和)产生的数组下标又太多;
现在需要一种压缩方法,把幂的连乘方案系统中得到的巨大整数范围压缩到可接受的数组范围中。可以通过取余操作来实现。虽然取余操作得到的结构也有可能重复,但是可以通过其他方式解决。
哈希表的一些概念:
- 哈希化:将大数字转化成数组范围内下标的过程,称之为哈希化;(即把代表一个值的大数字转换为小数字,小数字的范围是在一个能覆盖全部值的数组内)
- 哈希函数:我们通常会将单词转化成大数字,把大数字再进行哈希化的代码实现放在一个函数中,该函数就称为哈希函数;
- 哈希表:对最终数据插入的数组进行整个结构的封装,得到的就是哈希表。
解决哈希化时的冲突
什么冲突?哈希化过后的下标依然可能重复,这会导致冲突。
两种解决方案:
-
方案一:链地址法(拉链法)
如下图所示,我们将每一个数字都对其进行除10取余的操作,则其余数的范围是0~9
,0~9
作为数组的下标值。按照一般方法,我们在数组每一个下标值对应的位置存储哈希化后的元素,但是如果出现冲突时,这个方法就不再可用了。
此时可以采用链地址法,将我们的数组每一个下标值对应的位置存储由经过取余操作后得到相同余数的元素组成的数组或链表。一旦发现有重复的冲突,就将重复的元素插入到对应位置的链表或数组的开头或结尾。
当需要查询元素时,就对相应位置进行线性查找,即一个一个元素依次查找。
总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。 -
方案二:开放地址法
开放地址法的主要工作方式是:如果遇到冲突,就把元素放到其他空白的位置。
根据空白位置的探测方式不同,可以细分为三个方法:- 线性探测法
- 二次探测法
- 再哈希化法
开放地址法的三种探测方式
线性探测法:
- 当插入一个数
13
时:
13
在经过哈希化(除十取余)后,得到的下标值Index=3,但是从上图可知 位置3已经放置了数据33,此时需要找一个新的空白位置来放置13,线性探测就是从index + 1
的位置开始,向后一个个依次查找空白位置,找到空位之后就把13放置在上面,这就是线性探测。 - 当需要查询
13
时:- 首先13经过哈希化后得到index=3,随后去index 3的位置查找,如果是13则返回
- 如果该索引位置存放的不是13,则线性查找,从
index + 1
即index=4的位置开始依次向下查找13; - 查询过程中有一限定规则:如果查询到空位置就停止,因为依据线性探测法,不可能出现
13
在空位置之后(插入13时不会跳过空位置去插入下面其他的位置)
- 当删除
13
时:- 也是同查询一样进行查找,查找到该13元素后,不可以将该位置的内容设置为
null
,因为这样会影响上文查询步骤的第三点(如果查别的元素,遇到这个被删除内容的位置是空,就不会继续查询下去了) - 所以在删除一个位置的数据内容时,需要对其进行特殊处理,可以将内容设置为
-1
,这样就可以避免查到null
时会终止查找。
- 也是同查询一样进行查找,查找到该13元素后,不可以将该位置的内容设置为
线性探测法存在的问题:
- 线性探测法有一个严重的问题,就是聚集:
- 如在哈希表中插入了
23, 24, 25, 26, 27
五个位置连续的元素时,这种一连串的填充单元就被称为聚集。 - 聚集会影响哈希表的性能,当使用线性探测法 插入、查询和删除时,由于聚集区域的存在,会导致降低性能。
- 如:插入数
13
时,由于连续的单元3~7
均不允许插入数据,所以要依次遍历跳过这块区域,这就会降低插入速度,其他操作也同理 - 因为这个问题,衍生出了新的探测方式二次探测法
图:聚集现象
- 如在哈希表中插入了
二次探测法:
线性探测法存在的问题是,当数据连续插入导致聚集情况时,新插入一个数据可能需要很长的探测距离,二次探测就是在线性探测法的基础上进行了优化:
- 线性探测法的探测方式:步长为1进行探测,如从下标值为x 的位置开始探测,则顺序就是x+1, x+2, x+3…依次探测下去;
- 二次探测法的探测方式:对步长进行优化,如从下标值为x 的位置开始探测,则探测顺序就是x+12、x+22、x+33…,这样一次性探测的步长较大,可以跳过很多位置,避免了数据聚集带来的影响
- 二次探测法的问题:当待插入数据有较多重复冲突时,比如:
13-63-213-3-453
,下标值为3的位置上冲突较多,这也是一种聚集则二次探测法也会不能避免性能下降,但是这种冲突较多的情况不常见。
再哈希法:
开放地址法中探测空位置的最佳方法是再哈希化法:
- 二次探测的步长也是固定的,
1, 4, 9, 16
以此类推,所以还是会出现性能下降问题 - 再哈希化法可以将步长与关键字(数据)绑定,每个数据的探测步长都不一致
- 这样的话,就算不同的关键字其哈希化后的下标值一致,也不会出现二次探测法中的问题,其探测步长是不同的
- 再哈希法的处理方式:把关键字(数据)用另一个哈希函数,再进行一次哈希化,用这次哈希化的结果作为该关键字的步长。
第二次哈希化需要满足的条件:
- 哈希函数不得与第一次哈希的哈希函数相同,不然哈希化后的结果还是原来的位置
- 哈希化后的结果不能为0,为0就是在原地探测的死循环了
已有的步长哈希函数:
- stepSize = constant - (key % constant)
- 其中constant是质数,且小于哈希数组的容量
- 例如:stepSize = 5 - (key % 5),因为key对5取余的结果肯定小于5,所以步长不会等于0
哈希化的效率
在哈希表中执行插入和搜索操作的效率是非常高的。
- 如果没有发生冲突,效率会更高
- 如果发生了冲突,效率就取决于空位置探测长度
- 平均探测长度以及平均存取时间取决于填装因子,随着填装因子的变大,探测长度也越大
什么是填装因子:
- 填装因子表示为当前哈希表中已经包含的数据项和整个哈希表长度的比值;
- 装填因子 = 总数据项 / 哈希表长度;
- 开放地址法的装填因子最大为1,因为每个单元位置只能放一个元素
- 链地址法的装填因子可以无限大,因为每个单元可以放任意冲突元素
不同探测方式性能的比较
线性探测:
可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。
二次探测和再哈希化的性能:
二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。
链地址法的性能:
可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如Java中的HashMap中使用的就是链地址法。
总结: 综上所述,我们可得知在实际的开发中,使用 链地址法(拉链法)的情况较多。
优秀的哈希函数
哈希表的优势在于它的速度,所以哈希函数不能采用消耗性能较高的复杂算法。提高速度的一个方法是在哈希函数中尽量减少乘法和除法运算。
性能高的哈希函数应具备以下两个优点:
- 快速的计算:减少计算hashCode的耗时;
- 均匀的分布:减少查找元素的耗时;
快速计算
原本,我们是通过幂的连乘方式来得到每个元素的hashCode值的,而幂的连乘需要的乘除法运算较多,运算速度较低,会影响哈希的效率。
计算公式:(例如:cats = 3 * 273 + 1 * 272 + 20 * 271 + 17 * 270 =60337)
此时,需要做的运算次数是:
- 乘法次数:n(n + 1) / 2 次
- 加法次数:n 次
为了改变这个计算方法带来的效率降低,选择采用下面的方法。
霍纳法则:在中国霍纳法则也叫做秦久韶算法,具体算法为:
求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求n次多项式f(x)的值就转化为求n个一次多项式的值。
改进后,需要做的运算次数是:
- 乘法次数:n 次
- 加法次数:n 次
时间复杂度上,从改进前的O(n^2)
降到了O(n)
。
均匀分布:
为了保证数据在哈希表中均匀分布,当我们需要使用常量的地方,尽量使用质数;比如:哈希表的长度、N次幂的底数等。
Java中的HashMap采用的是链地址法,哈希化采用的是公式为:index = HashCode(key)&(Length-1)
即将数据转化为二进制进行与运算,而不是取余运算。这样计算机直接运算二进制数据,效率更高。但是JavaScript在进行叫大数据的与运算时会出现问题,所以以下使用JavaScript实现哈希化时还是采用取余运算。
封装和实现
哈希表的常见操作为:
put(key,value)
:插入或修改操作;get(key)
:获取哈希表中特定位置的元素;remove(key)
:删除哈希表中特定位置的元素;isEmpty()
:如果哈希表中不包含任何元素,返回trun,如果哈希表长度大于0则返回false;size()
:返回哈希表包含的元素个数;resize(value)
:对哈希表进行扩容操作;
哈希表基本封装及哈希函数的实现
哈希函数的步骤:
- 将字符串str转换为比较大的数字:hashCode,采用霍纳法则
- 将hashCode压缩到数组范围(size)内,得到索引值,采用取余计算
哈希表的实现思路:
- 本实现采用链地址法解决哈希化冲突,其中每个位置用数组存放元素(即表中一个位置放置一个数组bucket)
[[bucket], [bucket], [bucket],...]
- bucket中,每个元素采用数组进行信息存储
[key, value]
,数组第一个位置为key,即元素值,第二个位置存放元素相关信息value - 综上,此处完整的哈希表为三维数组
class hashTable {
constructor() {
this.storage = [] // 用于存储数据的数组
this.limit = 7 // 数组的初始长度
this.count = 0 // 已有数据的个数,可用于计算填装因子(loadFactor)
}
// 哈希函数,根据待存字符,得到表中的位置索引
hashFunc(str, size) {
// str:待哈希化的字符串,size:哈希表的长度
// 初始化哈希值
let hashCode = 0
// 对要哈希化的str进行迭代,实现 秦久韶算法(霍纳法则)
for (let i = 0; i < str.length; i++) {
// 此处相当于霍纳法则中的一个 一次多项式,公式中为: an * x + a (下标n-1)
hashCode = hashCode * 37 + str.charCodeAt(i);
}
// 对hashCode进行取余操作,得到在哈希表中的位置索引
let index = hashCode % size
return index
}
}
put()方法 插入、修改元素
哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个<key,value>时,如果原来不存在该key,那么就是插入操作,如果原来已经存在该key,那么就是修改操作。
实现思路:
- 首先,根据key获取索引值index,目的是将数据插入到storage的对应位置;
- 然后,根据索引值取出bucket,如果bucket不存在,先创建bucket,随后将这个bucket放置在该索引值的位置上;
- 接着,判断bucket中是否存在key,即是要新增还是修改原来的值。如果已经有值了,就修改该值,并返回函数;如果没有,就执行第4步操作。
- 此时可以确定是要新增数据,所以进行新增数据的操作。
put(key, value) {
// 利用哈希函数对key进行哈希化,得到对应index
let index = this.hashFunc(key, this.limit);
// 通过index取出哈希表对应位置的bucket(链条,此处为数组)
let bucket = this.storage[index]
// 如果bucket为空,证明此处没有数据,为这个位置创建一个bucket
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
// 判断bucket中是否存在key,存在则进行值的修改,并结束代码
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
return // 无需返回值
}
}
// 不存在key,新增元素
bucket.push([key, value])
// 哈希表已有数据个数加一
this.count++
}
get()方法 获取元素值
实现思路:
- 首先,根据key通过哈希函数获取它在storage中对应的索引值index;
- 然后,根据索引值获取对应的bucket;
- 接着,判断获取到的bucket是否为null,如果为null,直接返回null;
- 随后,线性遍历bucket中每一个key是否等于传入的key。如果等于,直接返回对应的value;
- 最后,遍历完bucket后,仍然没有找到对应的key,直接return null即可。
get(key) {
// 利用哈希函数对key进行哈希化,得到对应index
let index = this.hashFunc(key, this.limit);
// 通过index取出哈希表对应位置的bucket
let bucket = this.storage[index];
// 如果bucket为空,证明没有数据,也就不存在这个key了,直接返回null
if (bucket == null) {
return null
}
// 判断bucket中是否存在key,存在则返回值value
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
return bucket[i][1]
}
}
// 不存在key,返回null
return null
}
remove()方法 删除元素实现思路:
实现思路
- 首先,根据key通过哈希函数获取它在storage中对应的索引值index;
- 然后,根据索引值获取对应的bucket;
- 接着,判断获取到的bucket是否为null,如果为null,直接返回null;
- 随后,线性查找bucket,寻找对应的数据,并且删除;
- 最后,依然没有找到,返回null;
remove(key) {
// 利用哈希函数对key进行哈希化,得到对应index
let index = this.hashFunc(key, this.limit);
// 通过index取出哈希表对应位置的bucket
let bucket = this.storage[index];
// 如果bucket为空,证明没有数据,也就不存在这个key了,直接返回null
if (bucket == null) {
return null
}
// 判断bucket中是否存在key,存在则返回值value
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
// 相等则删除这个元素
bucket.splice(i, 1)
this.count--;
// 返回被删除元素的value
return tuple[1]
}
}
return null
}
isEmpty() & size()
isEmpty() {
return this.count == 0
}
size() {
return this.count
}
哈希表的扩容
为什么要扩容?
- 前面我们在哈希表中使用的是长度为7的数组,由于使用的是链地址法,装填因子(loadFactor)可以大于1,所以这个哈希表可以无限制地插入新数据。
- 但是,随着数据量的增多,storage中每一个index对应的bucket数组(链表)就会越来越长,这就会造成哈希表效率的降低
什么情况下需要扩容?
- 常见的情况是loadFactor > 0.75的时候进行扩容;
如何进行扩容?
- 简单的扩容可以直接扩大两倍(关于质数问题,在之后讨论);
- 扩容之后所有的数据项都要进行同步修改,因为哈希函数的一个参数size变化了;
实现思路:
- 首先,定义一个变量,比如oldStorage指向原来的storage;
- 然后,创建一个新的容量更大的数组,让this.storage指向它;
- 最后,将oldStorage中的每一个bucket中的每一个数据取出来依次添加到this.storage指向的新数组中;
代码实现:
resize(newLimit) {
// 保存旧的哈希表
let oldStorage = this.storage
// 新的哈希表置空,各参数恢复默认
this.storage = []
this.limit = newLimit
this.count = 0
// 循环旧哈希表的每个bucket
for (let i = 0; i < oldStorage.length; i++) {
const bucket = oldStorage[i]
// 如果是空的bucket则跳过本次循环
if (bucket == null) {
continue
}
// 循环取出bucket中的数据并再次哈希化存到新表中
for (let j = 0; j < bucket.length; j++) {
let tuple = bucket[j]
this.put(tuple[0], tuple[1])
}
}
}
上述定义的哈希表的resize方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩。
装填因子 = 哈希表中数据 / 哈希表长度,即 loadFactor = this.count / this.length。
- 通常情况下当装填因子
loadFactor > 0.75
时,对哈希表进行扩容。在哈希表中的添加方法(push方法)中添加如下代码,判断是否需要调用扩容函数进行扩容:if (this.count > this.limit * 0.75) { this.resize(this.limit * 2) }
- 当装填因子
loadFactor < 0.25
时,对哈希表容量进行压缩。在哈希表中的删除方法(remove方法)中添加如下代码,判断是否需要调用扩容函数进行压缩:if (this.count < this.limit * 0.25) { this.resize(Math.floor(this.limit / 2)) }
质数问题及解决方案
在哈希化的过程中,我们需要保证哈希表的容量(长度)始终为质数,从而使其均匀分布,本节解决的问题是如何保证在扩容时表长度始终为质数。
什么是质数?
本身大于一,且只能被1和自身整除的数是质数。
基本实现:
function isPrime(num){
if(num <= 1 ){
return false
}
for(let i = 2; i <= num - 1; i++){
if(num % i ==0){
return false
}
}
return true
}
上述方法可以实现质数的判断,但是根据数学规律可以知道,只需要该数的平方根以下的数
不能整除该数,就可以判断该数为质数了。下面代码为提高算法效率的写法:
isPrime(val) {
if (val <= 1) return false
for (let i = 2; i <= Math.sqrt(val); i++) {
if (val % i === 0) {
return false
}
}
return true
}
上文我们实现了质数判断,接下来就要实现在哈希表扩容或缩容时,始终保持哈希表长度为质数的功能。
实现思路:
- 2倍扩容之后,通过循环调用
isPrime()
判断得到的容量是否为质数,不是则该数+1
,直到是为止。 - 比如原长度:7,2倍扩容后长度为14,14不是质数,14 + 1 = 15不是质数,15 + 1 = 16不是质数,16 + 1 = 17是质数,停止循环,由此得到质数17。
实现代码:
// 传入一个数,判断该数是否为质数,如不是,则返回一个比他大且离他最近的质数
getPrime(val) {
while (!this.isPrime(val)) {
val++
}
return val
}
// 在 resize() 方法的开头增加以下语句
resize(newLimit) {
newLimit = this.getPrime(newLimit)
......
哈希表的完整实现代码
class hashTable {
constructor() {
this.storage = [] // 用于存储数据的数组
this.limit = 7 // 数组的初始长度
this.count = 0 // 已有数据的个数,可用于计算填装因子(loadFactor)
}
hashFunc(str, size) {
// str:待哈希化的字符串,size:哈希表的长度
// 初始化哈希值
let hashCode = 0
// 对要哈希化的str进行迭代,实现 秦久韶算法(霍纳法则)
for (let i = 0; i < str.length; i++) {
// 此处相当于霍纳法则中的一个 一次多项式,公式中为: an * x + a (下标n-1)
hashCode = hashCode * 37 + str.charCodeAt(i);
}
// 对hashCode进行取余操作,得到在哈希表中的位置索引
let index = hashCode % size
return index
}
put(key, value) {
// 利用哈希函数对key进行哈希化,得到对应index
let index = this.hashFunc(key, this.limit);
// 通过index取出哈希表对应位置的框(链条,此处为数组)
let bucket = this.storage[index]
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
tuple[1] = value
return // 无需返回值
}
}
bucket.push([key, value])
this.count++
if (this.count > this.limit * 0.75) {
this.resize(this.limit * 2)
}
}
get(key) {
let index = this.hashFunc(key, this.limit);
let bucket = this.storage[index];
if (bucket == null) {
return null
}
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
return bucket[i][1]
}
}
return null
}
remove(key) {
// 利用哈希函数对key进行哈希化,得到对应index
let index = this.hashFunc(key, this.limit);
// 通过index取出哈希表对应位置的bucket
let bucket = this.storage[index];
// 如果bucket为空,证明没有数据,也就不存在这个key了,直接返回null
if (bucket == null) {
return null
}
// 判断bucket中是否存在key,存在则返回值value
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--;
if (this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
return tuple[1]
}
}
return null
}
isEmpty() {
return this.count == 0
}
size() {
return this.count
}
resize(newLimit) {
newLimit = this.getPrime(newLimit)
let oldStorage = this.storage
this.storage = []
this.limit = newLimit
this.count = 0
for (let i = 0; i < oldStorage.length; i++) {
const bucket = oldStorage[i]
if (bucket == null) {
continue
}
for (let j = 0; j < bucket.length; j++) {
let tuple = bucket[j]
this.put(tuple[0], tuple[1])
}
}
}
isPrime(val) {
if (val <= 1) return false
for (let i = 2; i <= Math.sqrt(val); i++) {
if (val % i === 0) {
return false
}
}
return true
}
getPrime(val) {
while (!this.isPrime(val)) {
val++
}
return val
}
}
var h = new hashTable();
h.put('qwe', 'aaa')
h.put('asd', 'bbb')
h.put('zxc', 'ccc')