算法(广义)的时间是很奇妙的,将复杂的业务逻辑,可以通过算法转换成一个程序,如此,我们就可以不用为复杂的业务逻辑写一大堆命令式代码,而只需封装一个模块,提供一个接口就能解决复杂的业务逻辑。只需要我们学习足够多的算法和数据结构,在需要的时候,可以想到它们。今天我们来学习一种数据结构:散列表。
狭义的算法常用的有排序、递归、动态规划、贪心算法、散列算法等等,数据结构常用的有数组、对象、堆栈、队列、链表、集合(Set 类)、字典(Map 类)、散列表(以及散列集合)、二叉树等等。
散列算法的作用是尽可能快地在数据结构中找到一个值。散列函数的作用是给定一个健值,然后返回值在表中的地址。你是否知道数组和链表的差别呢?前者是检索很快,有一个下标就能快速定位到值,但是插入和删除项就没有后者强,而后者不需要像数组一样,改动一个项,其他的项在内存中的位置也会跟着变化,但是检索却更慢,因为要从头部或者尾部开始寻找。
那么如果像数组、对象或者是集合这些数据结构,配合散列算法(就是散列函数)使用的话,那么可以达到插入和检索的性能都很高的效果。
一个具体的场景:我们要维护一个数据结构,这个结构要存储以健为人名,以值为邮箱,业务需求是要不断往里面新增新的项,或者删除项,并且检索的频率也很高。先了解下 "lose lose" 散列函数,方法是简单地将每个健值中的每个字符的 ASCII 值相加。
如图所示,最终的 key 的存储方式就是每个字符的 ASCII 值相加的结果:
![d9a9cd547333d8d71a7b385cab845743.png](https://i-blog.csdnimg.cn/blog_migrate/2d03a0deeb2d342039c3710d1d07c27d.jpeg)
//loss loss 散列函数
let loseloseHashCode = function(key) {
let hash = 0;
for(let i=0; i<key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % 37; //取模意味着这个数据结构最多存放 36 个元素
};
//开创创建一个散列表吧
function HashTable() {
let items = [];
this.put = function(key, value) {
let position = loseloseHashCode(key);
console.log(`${position} --- ${key}`);
items[position] = value;
};
this.remove = function(key) {
let position = loseloseHashCode(key);
items[position] = undefined;
};
this.get = function(key) {
let position = loseloseHashCode(key);
return items[position];
};
};
let hash = new HashTable();
hash.put('Gandalf', 'gandalf@emai.com');
hash.put('John', 'johnsnow@email.com');
hash.put('Tyrion', 'tyrion@email.com');
console.log(hash.get('Gandalf'));
hash.remove('John');
console.log(hash.get('John'));
但是你会发现散列表是有冲突的,比如如果加入的人名很多时,会造成人名得到的 ASCII 值相加的结果是一样的,比如 Tyrion 和 Aaron 就有相同的散列值(都为 16),Donnie 和 Ana 有相同的散列值(都为 13),那么上面的散列表就会发生冲突,后面加入的项会覆盖前面的项,也就是说前面的项在数据结构中消失了,发生这种情况时,就要想办法去解决它,处理冲突的几种方式:分离链接、线性探查和双散列法。
//对 HashTable 类进行改写,分离链接法,即每个值是一个单链表的数据结构
function HashTable() {
let items = [];
let valuePair = function(key, value) {
this.key = key;
this.value = value;
this.toString = function() {
console.log(`${key} --- ${value}`);
};
};
this.put = function(key, value) {
let position = loseloseHashCode(key);
console.log(`${position} --- ${key}`);
if(table[position] === undefined) table[position] = new LinkedList();
table[position].append(new valuePair(key, value));
};
this.remove = function(key) {
let position = loseloseHashCode(key);
if(table[position] !== undefined) {
let cur = table[postion].getHead();
while(cur.next) {
if(cur.element.key === key) {
table[position].remove(cur.element);
if(table[postion].isEmpty) {
table[position] = undefined;
}
return true;
}
cur = cur.next;
}
//为链表的第一项或者最后一项
if(cur.element.key === key) {
table[position].remove(cur.element);
if(table[postion].isEmpty) {
table[position] = undefined;
}
return true;
}
}
return false;
};
this.get = function(key) {
let position = loseloseHashCode(key);
if(table[position] !== undefined) {
let cur = table[position].getHead();
while(cur.next) {
if(cur.element.key === key) {
return cur.element.value;
}
cur = cur.next;
}
if(cur.element.key === key) {
return cur.element.value;
}
}
return undefined;
};
};
//线性查找
function HashTable2() {
let table = [];
let ValuePair = function(key, value) {
this.key = key;
this.value = value;
this.toString = function() {
console.log(`${key} ---- ${value}`);
}
};
this.put = function(key, value) {
let position = loseloseHashCode(key);
if(table[position] === undefined) table[position] = new ValuePair(key, value);
let index = ++position;
while(table[index] !== undefined) {
index++;
}
table[index] = new ValuePair(key, value);
};
this.remove = function(key) {
let position = loseloseHashCode(key);
if(table[position] !== undefined) {
let index = ++position;
while((table[index] === undefined || table[index].key !== key) && index < 36) {
index++;
}
if(table[index] && table[index].key === key) {
table[index] = undefined;
return true;
}
}
return false;
};
this.get = function(key) {
let position = loseloseHashCode(key);
if(table[position] !== undefined) {
//这个位置上的值是否就是我们要查找的值,如果不是就往下一个位置查找
if(table[position].key ===key) {
return table[position].value;
}else {
let index = ++position;
while((table[index] === undefined || table[index].key !== key) && index < 36) {
index++;
}
if(table[index] && table[index].key === key) {
return table[index].value;
}
}
}
return undefined;
};
}
//创建更好的散列函数,djb2
let djb2HashCode = function(key) {
let hash = 5381;
for(let i=0; i<key.length; i++) {
hash = hash * 33 + key.charCodeAt(i);
}
return hash % 1013; //如果最大限度是 1000 的话,就要比 1000 大
};
使用 djb2 散列函数比 lose lose 散列函数发生的冲突要低很多,这是一个表现良好的散列函数,插入和检索元素的时间更好,也包括更低的冲突可能性。当然这并不是最好的散列函数,但是这是最受社区推崇的散列函数之一。
所以下次面试时,如果有人问题你,数组和链表的差别,你回答完之后,如果又问你,如果检索和插入以及删除的频率都高的话,怎么办呢?现在知道该怎么回答了吧!