前言
除了集合,我们还可以用字典和散列表来存储唯一值。
集合学习请见:
自定义集合和ES6集合http://t.csdn.cn/RcznA 在集合中,我们关注的是每个值本身。并将它作为主要元素。
而字典和散列表都是以[键:值]的形式来存储数据。
不同的是,字典的每一个键只能有一个值。
字典
字典和集合很相似。其存储的都是一组互不相同的元素。集合以[值:值]的形式存储元素。而字典以[键:值]的形式存储元素。字典也称为映射、符号表或关联数组。
在计算机科学中,字典常用来保存对象的引用地址。
字典类
ES6提供了一个Map类的实现。即上文所说的字典。
现在我们讲讲自行的实现:
export default class Dictionary {
constructor() {
this.table = {};
}
}
与我们自定义的Set类相似,我们都将在一个Object的实例而不是数组中存储字典中的元素。
在JS中,[]可以获取对象的属性。将属性名作为“位置”传入即可。所以对象也被称为关联数组。
规划化键名
在字典中,理想情况是用字符串作为键名。值可以是任何类型。所以我们需要确保key为字符串类型。于是我们需要先定义一个类型转换方法。确保所有key键都为字符串类型。
export default class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
}
除非用户自定义转化方法。一般的转化方法我们可以这么书写:
function defaultToString(item) {
if (item === null) {
return 'NULL';
} else if (item === undefined) {
return 'UNDEFINED';
} else if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return JSON.stringify(item);
}
检测键是否存在于字典中
hasKey(key) {
return Object.getOwnPropertyNames.call(this.toStrFn(key),this.table)
}
在字典中设置键
首先我们要搞清楚要以什么方式去储存键值。上面已经提过是[键名:键值]这种方式。键名为字符串。那么键值呢?
键名的字符串是经过转换的。而为了保存信息的需要,我们同样要保存原始的key。于是我们需要在键值Value里面记录原始的key和value。于是我们需要设置一个ValuePair类。用于存储原始数据。
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}
}
书写Set方法
set(key,value) {
if (key != null && value !=null && !this.hasKey(key)) {
const tableKey = this.toStrFn(key);
this.table[tableKey ] = new ValuePair(key, value)
return true
}
return false
}
在字典中移除一个键值对
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)];
return true;
}
return false;
}
在字典中检索键值对
get(key) {
const valuePair = this.table[this.toStrFn(key)];
return valuePair == null ? undefined : valuePair.value;
}
或者
get(key) {
if (this.hasKey(key)) {
return this.table[this.toStriFn(key)]
}else {
return undefined
}
}
但是第二种方法我们需要获取两次Key的字符串已经访问两次table对象。第一次的消耗明显更小
以数组的方式返回字典中所有的原始数据(包括key和value)
keysAndValues() {
return Object.values(this.table)
}
这使用了ES6的方法。我们也可以写成通用的方法:
keysAndValues() {
const values = [];
for (const k in this.table) {
if (this.hasKey(k)) {
values.push(this.table[k])
}
}
return values
}
为什么要加hasKey进行判断呢?
因为for…in循环是 遍历对象的每一个可枚举属性,包括原型链上面的可枚举属性
Object.keys()只是遍历自身的可枚举属性,不可以遍历原型链上的可枚可枚举属性Object.getOwnPropertyNames()则是遍历自身不包括原型链上面的所有属性(不论是否是可枚举的),
由此我们知道,for in循环可能枚举原型链上的属性。而且还有一个原因是可能是通过extend进行的类扩展,用for in可能枚举父类上的字段属性
keys方法返回原始数据键名
keys() {
return this.keysAndValues().map(valuePair => valuePair.key);
}
如果不支持ES6,可以用for循环替代map方法
values方法返回原始数据键值
values() {
return this.keysAndValues().map(valuePair => valuePair.value)
}
自定义字典的forEach迭代
我们需要创建一个能迭代这种数据结构中每个键值的方法。同时允许我们注入回调函数callbackFn并通过回调函数返回的结果中断迭代。
forEach(callbackFn) {
const valuePairs = this.keyAndValues();
for (let i = 0; i < valuePairs.length; i++) {
const result = callBackFn(valuePairs[i].key,valuePairs[i].value)
if (result === false) {
break;
}
}
}
size方法
返回字典中键的个数
size() {
const valuePairs = this.keyAndValues()
return valuePairs.length
}
clear方法
clear() {
this.table = {}
}
isEmpty方法
isEmpty() {
return this.size() === 0;
}
toString方法
toString() {
if (this.isEmpty()) {
return '';
}
const valuePairs = this.keyValues();
let objString = `${valuePairs[0].toString()}`;
for (let i = 1; i < valuePairs.length; i++) {
objString = `${objString},${valuePairs[i].toString()}`;
}
return objString;
}
散列表
HashTable类,也叫HashMap类。它是字典类的一种散列表实现方式。
作用
在字典里,如果要找到你想要的值(不是键,也不是健和值,而是值)。需要遍历整个数据结构来找到它。
散列算法与散列函数
狭义的算法常用的有排序、递归、动态规划、贪心算法、散列算法等等
数据结构常用的有数组、对象、堆栈、队列、链表、集合(Set 类)、字典(Map 类)、散列表(以及散列集合)、二叉树等等。
散列算法的作用是尽可能快地在数据结构中找到一个值
散列函数的作用是给定一个健值,然后返回值在表中的地址。
我们前面已经提到了,数组和链表的差别在于前者是检索很快,有一个下标就能快速定位到值,但是插入和删除项就没有后者强,而后者不需要像数组一样,改动一个项,其他的项在内存中的位置也会跟着变化,但是检索却更慢,因为要从头部或者尾部开始寻找。
那么如果像数组、对象或者是集合这些数据结构,配合散列算法(就是散列函数)使用的话,那么可以达到插入和检索的性能都很高的效果。
案例:
我们要维护一个数据结构,这个结构要存储以健为人名,以值为邮箱,业务需求是要不断往里面新增新的项,或者删除项,并且检索的频率也很高。
我们用最常见的散列函数lose lose散列函数进行存值。
lose lose散列函数:方法是简单地将每个健值中的每个字符的 ASCII 值相加。
最终的 key 的存储方式就是每个字符的 ASCII 值相加的结果。
散列函数代码:
loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}
hashCode(key) {
return this.loseloseHashCode(key);
}
这里为什么要取余数?为了得到比较小的hash值。我们会使用hash和一个任意数做除法的余数。这样可以规避操作数超过数值变量最大表示范围的风险
案例实现:
我们基于一个关联数组(对象)来表示我们的数据结构。
class HashTable {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
}
put方法 向散列表中增加一个新的项(此方法会更新散列表)
put(key,value) {
if (key !=null && value != null) {
const position = this.hashCode(key)
this.table[position] = new ValuePair(key,value);
return true
}
return false
}
get方法 从散列表中获取一个值
get(key) {
const valuePair = this.table[this.hashCode(key)]
return valuePair == null ? undefined : valuePair.value
}
HashTable和Dictionary类很相似。不同之处在于在字典类中,我们将valuePair保存在table的key属性中(在它呗转换为字符串之后)。而在HashTable中,我们将key生成一个数(键),并将valuerPair保存在值,从而形成一张新的hash表。
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return false;
}
我们也可以把散列表称为散列映射
以上为字典和散列表的基础介绍,如果需要了解散列表的进阶应用,请看下一篇推文《JavaScript散列表及其扩展》