哈希表/散列表
哈希表的定义
使用散列函数,就知道值的具体位置,能够快速检索到该值。散列函数的作用是给定一个键值, 然后返回值在表中的地址。
哈希表的作用
数组虽然查找和修改数据数据的值效率很快,但是删除和新增数组的操作很消耗性能
1. 而哈希表可以提供快速的插入-删除-查找操作
2. 无论多少数据,哈希表的插入与删除操作需要的时间都是O(1)。
3. 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素
哈希表的缺点
哈希表中的数据是没有顺序的,所以不能以一种固定的顺序去遍历所有的元素
通常情况下,哈希表的键key是不允许重复的,不能用相同的key去存不同的value。
创建更好的哈希算法
一个表现良好的散列函数是由几个方面构成的:插人和检索元素的时间(即性能),当然也包括较低的冲突可能性。我们可以在网上找到一些不同的实现方法,或者也可以实现自己的散列函数。
1. 好的哈希函数应该尽可能让计算的过程变得简单,提供计算的效率
1.1 哈希表的主要优点就是它的速度快,所以哈希函数速度很慢,那么将毫无意义
1.2 提高速度的一个方法就是让哈希函数中尽可能的少出现乘法和除法,因为这两种操作性能比较低
2. 哈希函数看重
2.1 速度快慢
2.2 值分布是否均匀
// 这并不是最好的散列函数,但这是最被社区推荐的散列函数之一 。
const djb2HashCode = function (key) {
let hash = 5381;
for (let i = 0; i < key.length; i++) {
hash = hash * 33 + key.charCodeAt(i);
}
return hash % 1013;
};
基于数组实现
const HashTable = function() {
this.table = [];
//散列函数,计算传进来的值的ASCLL码的之和
const hashFunction = function(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
// 向散列表中添加元素
HashTable.prototype.put = function(key, value) {
const hash = hashFunction(key);
this.table[hash] = value;
};
// 从散列表中查找一个值
HashTable.prototype.get = function(key) {
return this.table[hashFunction(key)];
};
// 清空散列表
HashTable.prototype.clear = function() {
this.table = [];
};
// 移除散列表中的一个元素
HashTable.prototype.remove = function(key) {
this.table[hashFunction(key)] = undefined;
};
// 输出散列表中所有的元素
HashTable.prototype.print = function() {
for (let i = 0; i < this.table.length; i++) {
/*在创建稀疏数组时,会存在一些空白单元JavaScript会将这些空白单元隐式的赋值为undefined(但这与将其显式赋值为 undefined是有所区别的)这个过程并且会影响length的值*/
if (this.table[i] !== undefined) {
console.log(i + ': ' + this.table[i]);
}
}
}
}
什么是哈希冲突
也就是哈希算法可能会为两个不同的值生成相同的索引,从而导致值会被覆盖的问题,造成数据丢失,见下发演示代码和运行截图发现传入的数据和被保存的数据数量不一致。
const HashTable = function() {
this.table = [];
//散列函数,计算传进来的值的ASCLL码的之和
const hashFunction = function(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
// 向散列表中添加元素
HashTable.prototype.put = function(key, value) {
const hash = hashFunction(key);
this.table[hash] = value;
};
// 从散列表中查找一个值
HashTable.prototype.get = function(key) {
return this.table[hashFunction(key)];
};
// 清空散列表
HashTable.prototype.clear = function() {
this.table = [];
};
// 移除散列表中的一个元素
HashTable.prototype.remove = function(key) {
this.table[hashFunction(key)] = undefined;
};
// 输出散列表中所有的元素
HashTable.prototype.print = function() {
for (let i = 0; i < table.length; i++) {
/*在创建稀疏数组时,会存在一些空白单元JavaScript会将这些空白单元隐式的赋值为undefined(但这与将其显式赋值为 undefined是有所区别的)这个过程并且会影响length的值*/
if (this.table[i] !== undefined) {
console.log(i + ': ' + this.table[i]);
}
}
}
}
var hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@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');
console.log('————————————————————————');
hash.print();
解决哈希冲突–分离链接法
分离链接法包括为散列表的每一个位置创建一个链表并将元索存储在里面。它是解决冲突的
最简单的方法,但是它在HashTable实例之外还需要额外的存储空间。
链表相关部分的代码
// 单向链表
export default function() {
this.head = null;
this.length = 0;
// 辅助类,创建节点
const Node = function(element) {
this.element = element;
this.next = null; //因为next用来存放对象,null是一个空的对象,因此这里使用空很合适
};
//在链表末尾添加元素
this.__proto__.append = function(element) {
const node = new Node(element);
if (this.head === null) {
this.head = node;
} else {
// 拿一个变量接收找到的最后一个节点,将添加的元素添加到最后一个节点的next属性
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
this.length++;
};
// 获取链表的头
this.__proto__.getHead = function() {
return this.head;
};
//得到链表的长度
this.__proto__.size = function() {
return this.length;
};
// 往链表指定位置插入元素(下标位置position从0开始)
this.__proto__.insert = function(position, element) {
let node = new Node(element);
// 插入位置不能越界
if (position > -1 && position <= this.length) {
// 在开头插入元素,相当于交换(插入的node和head两个数)
if (position == 0) {
let current = this.head;
this.head = node;
this.head.next = current;
} else if (position == this.length) {
//在末尾的位置插入
this.append(element);
// 在append方法中新建了一个node,所以将这个作用域中的node赋值为null,释放内存
node = null;
// append方法中执行了this.length++
this.length--;
} else {
// previous插入坐标前一个节点 current当前插入坐标的节点
let previous = null;
let current = this.head;
let index = 0;
while (index < position) {
previous = current;
current = current.next;
index++;
}
previous.next = node;
node.next = current;
}
this.length++;
return true;
} else {
// return new Error("插入的下标位置越界了,不能这样");
return false;
}
};
//检查链表是否为空
this.__proto__.isEmpty = function() {
return this.length === 0;
};
//传入要删除某个位置下标的元素
this.__proto__.removeAt = function(position) {
// 越界问题
if (position > -1 && position < this.length) {
let current = this.head;
//删除链表第一个元素
if (position === 0) {
this.head = current.next;
} else {
//while循环退出条件是,查找到链表的index下标位置为position时,进行删除操作
let index = 0;
let previous = null;
// 查询
while (index < position) {
previous = current;
current = current.next;
index++;
}
// 当index == position时,删除
previous.next = current.next;
}
this.length--;
return current;
} else {
return false;
}
};
//获取指定element的下标position
this.__proto__.indexOf = function(element) {
let index = 0;
let current = this.head;
while (current) {
if (current.element == element) {
return index;
}
current = current.next;
index++;
}
return -1;
};
// 删除指定元素
this.__proto__.remove = function(element) {
return this.removeAt(this.indexOf(element));
};
// 更新指定position下标的节点
this.__proto__.update = function(position, element) {
if (this.get(position)) {
this.get(position).element = element;
return true;
}
return false;
};
// 获取指定position下标的节点
this.__proto__.get = function(position) {
// 越界判断
if (position > -1 && position < this.length) {
let current = this.head;
let index = 0;
while (index < position) {
current = current.next;
index++;
}
return current;
} else {
return false;
}
};
// 删除链表最后一个节点
this.__proto__.delete = function() {
this.removeAt(this.length - 1);
};
};
分离链接法
// 使用链表加数组来解决哈希冲突哈希表
export default function() {
this.table = [];
//散列函数,计算传进来的值的ASCLL码的之和
const hashFunction = function(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
// 向散列表中添加元素
this.__proto__.put = function(key, value) {
const hash = hashFunction(key);
if (this.table[hash] === undefined) {
this.table[hash] = new LikedList();
}
this.table[hash].append(value);
};
// 从散列表中查找一个值(根据键名)
this.__proto__.get = function(key) {
const hash = hashFunction(key);
if (this.table[hash] === undefined) {
return false;
} else {
// 应该得到整个链表,后续在链表中加入返回所有值的方法
return this.table[hash];
}
};
// 清空散列表
this.__proto__.clear = function() {
this.table = [];
};
// 移除散列表中的一个元素(根据键名)
this.__proto__.remove = function(key) {
this.table[hashFunction(key)] = undefined;
};
// 输出散列表中所有的元素
this.__proto__.print = function() {
for (let i = 0; i < this.table.length; i++) {
/*在创建稀疏数组时,会存在一些空白单元JavaScript会将这些空白单元隐式的赋值为undefined(但这与将其显式赋值为 undefined是有所区别的)这个过程并且会影响length的值*/
if (this.table[i] !== undefined) {
console.log(i + ": " + this.table[i].getHead());
}
}
};
};
解决哈希冲突–线性探查
当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被 占据了,就尝试index+2的位置,以此类推。在js中,我们不需要像其它语言一样担心数组的长度超出可用范围,需要手动扩容。因为js的数组会自动扩容,但是会影响性能,所以需要好好设计一下,数组的初始长度给多大,一次扩容多少,这些问题较为复杂,目前还没有水平去深入-__-
export default function() {
this.table = [];
//散列函数,计算传进来的值的ASCLL码的之和
const hashFunction = function(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37;
};
// 向散列表中添加元素
this.__proto__.put = function(key, value) {
let hash = hashFunction(key);
while (this.table[hash] !== undefined) {
hash++;
}
this.table[hash] = value;
};
// 从散列表中查找一个值(根据键名)
this.__proto__.get = function(key) {
const hash = hashFunction(key);
if (this.table[hash] === undefined) {
return false;
} else {
return this.table[hash];
}
};
// 清空散列表
this.__proto__.clear = function() {
this.table = [];
};
// 移除散列表中的一个元素(根据键名)
this.__proto__.remove = function(key) {
this.table[hashFunction(key)] = undefined;
};
// 输出散列表中所有的元素
this.__proto__.print = function() {
for (let i = 0; i < this.table.length; i++) {
/*在创建稀疏数组时,会存在一些空白单元JavaScript会将这些空白单元隐式的赋值为undefined(但这与将其显式赋值为 undefined是有所区别的)这个过程并且会影响length的值*/
if (this.table[i] !== undefined) {
console.log(i + ": " + this.table[i]);
}
}
};
};