哈希存取数据的原理与特性,字典与集合的特点,及其 js 代码的实现……
一 哈希表(Hash table)
通过上一章 数据结构之线性结构,我们知道数组的特点是查询快增删慢,链表的特点是查询慢增删快,那么有没有一种查询快增删也快的数据结构呢?这就是我们要讨论的哈希表。
哈希表,也叫散列表,是根据关键码值(key-value)直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
把关键码值映射到表中的方法,称为哈希(散列)函数。
存放记录的数组,称为哈希(散列)表,是一块连续的存储空间。
1. 哈希表的实现
哈希表最重要的是哈希函数,存储的过程也是计算哈希函数的过程。其实现思路是:
- 创建存储位置:建立一个数组来存放元素。
- 创建哈希函数:计算存储位置,这是哈希表最重要的一步。通常用来把 key 转换成一个整型数字,将该数字对数组长度进行取余,取余结果就当做数组下标,将 value 存储于此。
- 通过计算后的位置(
哈希(散列)地址)存储元素。
class HashMap {
constructor() {
// 存放元素的数组
this.list = [];
}
// 哈希函数
hashCode(key) {
return key.length % 10;
}
// 存储元素
put(key, value) {
this.list[this.hashCode(key)] = value;
}
// 获取元素
get(key) {
return this.list[this.hashCode(key)];
}
// 删除元素
remove(key) {
this.list[this.hashCode(key)] === undefined;
}
// 清空
clear() {
this.list = [];
}
}
// 初始化散列表
const hashMap = new HashMap();
// 添加元素
hashMap.put('animal', 'dog');
// 直接通过key获取元素
hashMap.get('animal');
2. 哈希冲突
哈希函数,除了上述的取余法外,还有其他常用的方法,如直接给定地址法、数字分析法、随机数法等,比较简单,不举例示意了。
如果数据很多,不同的 key 经过哈希函数换算后,难免会出现相同‘下标’的情况。
哈希冲突,是指两个 key 经过哈希函数处理后得到了一样的值。实际编码过程中,要尽量避免冲突的现象。以下是规避哈希冲突的常用方法。
链地址法
如果在添加元素的过程中发现要添加的位置已经存在元素,可以通过修改这个地址的结构为‘链表存储’来避免哈希冲突。
如:
可修改为:
多哈希法
使用多个不同的哈希函数,来减少冲突的几率。
开放地址法
每次发现冲突时,向后移动一个位置。
桶地址法
将表中的每个地址关联一个桶,如果桶满了,使用开发地址法处理。
3. 哈希表的特性及应用
把任意长度的输入,通过哈希函数,变换成固定长度的哈希值。这种转换是一种压缩映射,也就是,哈希值的空间通常远小于输入的空间,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
哈希表是基于数组的,以此为基础,查找、增删只需要接近常量的时间,即O(1)的复杂度。哈希表的空间复杂度为O(n), 是一种用空间换取时间的执行方式。
哈希主要用于信息安全领域中加密算法和海量数据的处理。比如把一些不同长度的信息转化成杂乱无章的128位的编码……
二 字典
字典,又称关联数组、映射,是一种以 键-值对 形式存储的数据结构。
字典是一种非线性存储结构,不像数组和链表那样有顺序性,分散在一个对象中。字典中的元素是可变的,不是必须的,键值是不重复的,如果重复则会覆盖,键值是可hash。javaScript 中的 Object(对象) 类就是用字典形式设计的。
代码实现
class Dictionary {
constructor() {
this.data = {};
this.count = 0;
}
// 添加元素
add(key, value) {
if(!this.data[key]) {
this.count ++;
}
this.data[key] = value;
return true;
},
// 删除元素
remove(key) {
if(this.data[key]) {
delete this.data[key];
this.count --;
}
return true;
}
// 查找
find(key) {
return this.data[key];
}
// 显示所有元素方法
showAll () {
Object.keys(this.data).forEach(key => console.log(`${key} --> ${this.data[key]}`))
}
// 总个数
total() {
return this.count;
}
// 清空
clear() {
this.data = {};
this.count = 0;
}
// 含有
has(key) {
return !!this.data[key];
}
// 排序
sort() {
let result = {};
Object.keys(this.data).sort().forEach( key => {
result[key] = this.data[key];
});
this.data = result;
}
}
Map 类
es6 内置了 Map 类。Map 与 Object 的区别在于,Object 只能使用字符串作 key,而 Map 可以用各种类型的值作为 key,并且键值可 hash。
let map = new Map()
let a = {}
let b = {}
map.set(a, 1)
map.set(b, 2)
console.log(map) // Map { {} => 1, {} => 2 }
最后的输出值有两个值,也就是说没有覆盖,map 中 a、b 的 key 值存储的是他们的哈希地址,并不是 {} 这个空对象,所以在 map 中才会有两个值存在。
三 集合
集合,是由一组无序且唯一的项组成的,集合中的元素成为成员,而且这些成员之间是没有关联的。ES6 中的 内置类 Set就是这种数据结构。
与数学中的概念相同,集合有着交集、并集、差集、子集、补集的概念。
集合的代码实现
class MockSet {
constructor() {
this.data = [];
}
// find index
findIndex(value) {
// 集合中成员唯一
return this.data.indexOf(value);
}
// add
add(value) {
if(!this.data.includes(value)) {
this.data.push(value);
}
}
// remove
remove(value) {
if(this.data.includes(value)) {
this.data.splice(this.findIndex(value), 1);
}
return true;
}
// 包含
has(value) {
return this.data.includes(value);
}
// 子集
isChild(newSet) {
return this.data.length !== newSet.data.length && newSet.data.every(item => this.data.includes(item));
}
// 并集
mergeSet(newSet) {
newSet.forEach(item => {
if(!this.data.has(item)) {
this.data.push(item)
}
})
return this.data;
}
// 差集
diffSet(newSet) {
return this.data.filter(item => !newSet.has(item));
}
// 交集
interSet(newSet) {
return this.data.filter(item => newSet.has(item));
}
// 补集
complementSet(newSet) {
if(!this.isChild(newSet)) {
return false
}
return this.diffSet(newSet);
}
// size
size() {
return this.data.length;
}
// clear
clear() {
this.data = [];
}
}