散列表(HashMap)
李艳生
湖北师范大学
物理与电子科学学院
2020年春季
回顾
-
集合Set:元素不重复,元素=值:值
-
字典Map:元素不重复,元素=键:值
引入
请大家思考在n个无序元素的数组中查找一个元素需要多少个操作?有没有更高性能查找的数据结构呢?
内容
散列表(重点)
散列冲突(重点)
散列函数
任务
查字典
散列表
散列表
- 散列的作用是尽可能快地在数据结构中找到一个值
- 数组查找值要通遍历数组查找值
- 散列函数计算值的存储具体位置,从而快速找到值
- 应用:数据库索引,对象存储
创建散列表
class HashMap {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn; //转字符串方法
this.table = {};
}
put(key,value){} //添加
remove(key){} //移除
get(key){} //查找
hashCode(key){} //散列函数
toString(){} //转字符串
}
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 item.toString();
}
散列函数
hashCode(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;
}
添加元素
- 该方法接收key 和value 作为参数
- key计算散列函数值作为属性名
- 为了保存原始键值数据,原始的key 和value同时作为属性值
添加元素
put(key, value) {
if (key != null && value != null) {
//const tableKey = this.toStrFn(key);
//this.table[tableKey] = new ValuePair(key, value);
const position = this.hashCode(key);
this.table[position] = new ValuePair(key, value);
return true;
}
return false;
}
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}
toString() {
return `[#${this.key}: ${this.value}]`;
}
}
删除元素
- 要从HashMap 中移除一个值,计算散列函数找到值所在的位置
- 如果存在,就从散列表中移除,返回true,表示元素被移除;否则返回false
删除元素
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return false;
}
查找元素
- 如果元素在散列表中,返回元素值,否则返回undefined。
- 首先要计算散列函数用来查找某个元素
get(key) {
//const valuePair = this.table[this.toStrFn(key)];
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
转字符串
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[keys[i]].toString()}}`;
}
return objString;
}
散列表使用
const hash = new HashMap();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com');
hash.put('Tyrion', 'tyrion@email.com');
console.log(hash.hashCode('Gandalf') + ' - Gandalf'); //19 - Gandalf
console.log(hash.hashCode('John') + ' - John'); //29 - John
console.log(hash.hashCode('Tyrion') + ' - Tyrion'); //16 - Tyrion
console.log(hash.get('Gandalf')); // gandalf@email.com
console.log(hash.get('Loiane')); // undefined
hash.remove('Gandalf');
console.log(hash.get('Gandalf')); //undefined
散列表使用
问题
const hash = new HashMap();
hash.put('Ygritte', 'ygritte@email.com');
hash.put('Jonathan', 'jonathan@email.com');
hash.put('Jamie', 'jamie@email.com');
hash.put('Jack', 'jack@email.com');
console.log(hash.hashCode('Ygritte') + ' - Ygritte'); //4 - Ygritte
console.log(hash.hashCode('Jonathan') + ' - Jonathan'); //5 - Jonathan
console.log(hash.hashCode('Jamie') + ' - Jamie'); //5 - Jamie
console.log(hash.hashCode('Jack') + ' - Jack'); //7 - Jack
散列冲突
- 一些键会有相同的散列代码
- 不同的值在散列表中对应相同位置
拉链法
- 为散列表的每一个位置创建一个链表并将元素存储在里面
创建散列表
class HashMapWithChaining {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn; //转字符串方法
this.table = {};
}
put(key,value){} //添加
remove(key){} //移除
get(key){} //查找
hashCode(key){} //散列函数
}
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 item.toString();
}
添加元素
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new LinkedList();
}
this.table[position].push(new ValuePair(key, value));
return true;
}
return false;
}
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}
toString() {
return `[#${this.key}: ${this.value}]`;
}
}
查找元素
get(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.head;
while (current != null) {
if (current.element.key === key) {
return current.element.value;
}
current = current.next;
}
}
return undefined;
}
删除元素
remove(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.head;
while (current != null) {
if (current.element.key === key) {
linkedList.remove(current.element);
if (linkedList.isEmpty()) {
delete this.table[position];
}
return true;
}
current = current.next;
}
}
return false;
}
线性探测法
- 向表中某个位置添加一个新元素时,如果索引为position 的位置已经被占据,依次尝试position+1,position+2,...,直到找到空位置
创建散列表
class HashMapWithLinear {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn; //转字符串方法
this.table = {};
}
put(key,value){} //添加
remove(key){} //移除
get(key){} //查找
hashCode(key){} //散列函数
}
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 item.toString();
}
添加元素
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new ValuePair(key, value);
} else {
let index = position + 1;
while (this.table[index] != null) {
index++;
}
this.table[index] = new ValuePair(key, value);
}
return true;
}
return false;
}
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}
toString() {
return `[#${this.key}: ${this.value}]`;
}
}
查找元素
get(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
return this.table[position].value;
}
let index = position + 1;
while (this.table[index] != null && this.table[index].key !== key) {
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
return this.table[position].value;
}
}
return undefined;
}
删除元素
软删除
删除元素
硬删除
删除元素
remove(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
delete this.table[position];
this.verifyRemoveSideEffect(key, position);
return true;
}
let index = position + 1;
while (this.table[index] != null && this.table[index].key !== key ) {
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
delete this.table[index];
this.verifyRemoveSideEffect(key, index);
return true;
}
}
return false;
}
verifyRemoveSideEffect(key, removedPosition) {
const hash = this.hashCode(key);
let index = removedPosition + 1;
while (this.table[index] != null) {
const posHash = this.hashCode(this.table[index].key);
if (posHash <= hash || posHash <= removedPosition) {
this.table[removedPosition] = this.table[index];
delete this.table[index];
removedPosition = index;
}
index++;
}
}
## 直接定址法
$$ hashCode(key) = key $$
$$ hashCode(key) = a * key + b $$
## 除留余数法
$$ hashCode(key) = key \\% p $$
(p<=m,m为散列表长)
## 随机数法
$$ hashCode(key) = random(key) $$
(random为随机函数)
散列函数
hashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;
for (let i = 0; i < tableKey.length; i++) {
hash = (hash * 33) + tableKey.charCodeAt(i);
}
return hash % 1013;
}
## 装填因子
$$ \alpha = \frac {表中填入的元数个数} {表长} $$
## 性能
|散列冲突|查找成功|查找不成功|
|:-------|:------:|:--------:|
|线性探测法|$ \frac{1}{2}(1 + \frac{1}{1-\alpha}) $|$ \frac{1}{2}(1 + \frac{1}{(1-\alpha)^2}) $|
|拉链法| $ 1+ \frac {\alpha}{2} $ | $ \alpha + e^{-\alpha} $ |
## 比较
#### 平均情况时间复杂度
|数据结构|访问|查找|添加|删除|
|:--|:--:|:--:|:--:|:--:|
|数组|O(1)|O(n)|O(n)|O(n)|
|链表|O(n)|O(n)|O(1)|O(1)|
|散列表|N/A|O(1)|O(1)|O(1)|
## 比较
#### 最坏情况时间复杂度
|数据结构|访问|查找|添加|删除|
|:--|:--:|:--:|:--:|:--:|
|数组|O(1)|O(n)|O(n)|O(n)|
|链表|O(n)|O(n)|O(1)|O(1)|
|散列表|N/A|O(n)|O(n)|O(n)|
任务
分别向数组和散列表中存放1至100000共10万个元素,比较两种数据结构查找99999的性能。