文章目录
1 符号表
- 我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息
- 符号表有时也被称为字典。键就是单词,值就是单词对应的定义,发音和词源
- 符号表有时又叫做索引。键就是术语,值就是书中该术语出现的所有页码
- 无序符号表—HashMap
- 有序符号表—TreeMap
符号表的应用
2 哈希表
2.1 概述
- 如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。哈希表是这种简单方法的扩展,并且能够处理更加复杂类型的键。我们需要用哈希函数将键转换成数组的索引
- 哈希表的核心算法可以分为两步
1.用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
2.处理碰撞冲突:
a. 开放地址法
b. 拉链法
c. 再散列法
哈希函数
一个优秀的 hash 算法,满足以下特点:
- 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值
- 逆向困难:给定(若干) hash值,在有限时间内很难(基本不可能)逆推出明文。
- 输入敏感:原始输入信息修改一点信息,产生的 hash 值看起来应该都有很大不同
- 冲突避免:很难找到两段内容不同的明文,使得它们的 hash值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
我们可以简单地把哈希函数理解为在模拟随机映射,然后从随机映射的角度去理解哈希函数
经典的哈希函数
- MD4: 128位,MD4已被证明不够安全
- MD5:MD5 已被证明不具备"强抗碰撞性"
- SHA1:SHA-1 已被证明不具"强抗碰撞性"
- SHA2: SHA3: 相关算法已经被提出
哈希算法的应用
- 指纹 (身份的标识)
a. 证书, 文件
b. 加密 - 散列
a. 集群, 分布式
b. 数据结构
注意事项:其实把hash算法当成是一种加密算法,这是不准确的,我们知道加密总是相对于解密而言的,没有解密何谈加密呢,HASH的设计以无法解密为目的的。并且如果我们不附加一个随机的salt值,HASH口令是很容易被字典攻击入侵的
2.2 碰撞处理:拉链法
- 发生碰撞冲突的元素都存储在同一条链表中
- 在哈希函数能保证平均分布的前提下,那么哈希表的性能就取决于链表的平均长度
- 如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表得平均长度不超过某个值。这个值我们称之为加载因子,也就是链表平均长度可以达到的最大值
- 因此,当元素个数达到一定的数目的时候,我们就需要对数组进行扩容
3 MyHashMap
简化处理
- 键不能为null。如果键为null,我们会抛出 NullPointerException
- 值不能为null。我们这么规定的原因是:当键不存在的时候,get()方法会返回null。这样做有个好处,我们可以调用get()方法,看其返回值是否为 null 来判断键是否存在哈希表中
- 缓存hash值。如果散列值计算很耗时 (比如长字符串)。那么我么可以在结点中用一个 hash 变量来保存它计算的 hash 值。Java 中的 String 就是这样做的
- 注意!
HashMap 类不同步,在并发场景中可能会出现许多问题。比较经典的一个问题是:查找的时候可能会出现死循环
3.1 构造方法及内部类
package com.cskaoyan.hashmap;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;
/*
API:
void put(K key, V value)
V get(K key)
void delete(K key)
void clear()
boolean contains(K key)
boolean isEmpty()
int size()
Set<K> keys()
*/
public class MyHashMap<K, V> {
private static final int DEFAULT_ARRAY_SIZE = 16;
private static final int MAX_ARRAY_SIZE = 1 << 30;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 属性
private Entry[] table;
private int size;
private float loadFactor;//加载因子
private int threshold; // 阈值
private static class Entry {//私有内部类
Object key;
Object val;
int hash;
Entry next;//链表
public Entry(Object key, Object val, int hash, Entry next) {
this.key = key;
this.val = val;
this.hash = hash;
this.next = next;
}
@Override
public String toString() {
return key + "=" + val;
}
}
// 构造方法
默认构造方法
public MyHashMap() {
this(DEFAULT_ARRAY_SIZE, DEFAULT_LOAD_FACTOR);
}
给定容量时构造方法
public MyHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
给定容量和加载因子的构造方法
public MyHashMap(int initialCapacity, float loadFactor) {
// initialCapacity 初始数组大小
if (initialCapacity <= 0 || initialCapacity > MAX_ARRAY_SIZE) {
throw new IllegalArgumentException("initialCapacity=" + initialCapacity);
}
if (loadFactor <= 0) {
throw new IllegalArgumentException("loadFactor=" + loadFactor);
}
this.loadFactor = loadFactor;
计算大于等于initialCapacity的最小的2的幂
int capacity = calculateCapacity(initialCapacity);
table = new Entry[capacity];
threshold = (int) (capacity * loadFactor);
}
JDK中提供非常巧妙找到一个数最小二次幂上限的方法
private int calculateCapacity(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return n + 1;
}
重写tostring
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
for (Entry e : table) {
while (e != null) {
sb.append(e).append(", ");
e = e.next;
}
}
if (size > 0) sb.delete(sb.length() - 2, sb.length());
return sb.append("}").toString();
}
}
JDK中提供非常巧妙找到一个数最小二次幂上限的方法
3.2 Put方法
/**
* 添加键值对, 如果key已经存在, 则修改key关联的值
*
* @param key 键
* @param value 值
* @return key原来关联的值, 如果key不存在, 则返回null
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
if (key == null || value == null) {
throw new IllegalArgumentException("Key or value cannot be null");
}
// 计算key的hash值
int hash = hash(key);
// 计算key应该位于数组的哪条链表中
int idx = indexFor(hash, table.length);
// 遍历链表, 查看key是否已经存在
for (Entry e = table[idx]; e != null; e = e.next) {
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
// 找到key,并更新value
V oldValue = (V) e.val;
e.val = value;
return oldValue;
}
}
// 没找到,添加结点
addEntry(key, value, hash, idx);
return null;
}
private void addEntry(K key, V value, int hash, int idx) {
// 判断table是否需要扩容
if (size >= threshold) {
if (table.length == MAX_ARRAY_SIZE) {//数组达到最大长度
// 破坏加载因子的限制,达到最大上限
threshold = Integer.MAX_VALUE;
} else {//没达到最大长度,数组*2
grow(table.length << 1);
// 重新计算键值对应该插入的位置
idx = indexFor(hash, table.length);
}
}
// 添加键值对(头插法)
Entry entry = new Entry(key, value, hash, table[idx]);
//创建Entry对象指向Entry数组对应索引的头节点
table[idx] = entry;
//然后再把头节点赋值给创建的Entry
size++;
}
private void grow(int newCapacity) {
Entry[] newTable = new Entry[newCapacity];
// 把所有的键值对添加到newTable
for (Entry e : table) {//遍历old数组
while (e != null) {//当old数组某一索引位置上仍有元素,继续替换
Entry next = e.next;//先保存old数组头节点的next
int idx = indexFor(e.hash, newCapacity);//计算在新数组上的索引
// 头插法
e.next = newTable[idx];//old数组上的头节点指向new数组对应索引的头节点
newTable[idx] = e;//要插入的目标Entry赋值给new数组的头节点
e = next;//老数组的next上来顶替被换的节点
}
}
//修改table和threshold
table = newTable;
threshold = (int) (newCapacity * loadFactor);
}
//计算索引位置
private int indexFor(int hash, int length) {
return hash & (length - 1);
}
//哈希函数
private int hash(K key) {
int h = key.hashCode();
return (h >> 16) ^ (h << 16);
}
3.2 Gget方法
/**
* 获取key关联的值
*
* @param key 键
* @return key关联的值
*/
@SuppressWarnings("unchecked")
public V get(K key) {
if (key == null) throw new IllegalArgumentException("Key cannot be null");
int hash = hash(key);
int idx = indexFor(hash, table.length);
// 遍历链表
for (Entry e = table[idx]; e != null; e = e.next) {
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
return (V) e.val;
}
}
return null;
}
3.4 delete方法
/**
* 删除键等于key键值对
*
* @param key 键
* @return key关联的值
*/
@SuppressWarnings("unchecked")
public V delete(K key) {
if (key == null) throw new IllegalArgumentException("Key cannot be null");
int hash = hash(key);
int idx = indexFor(hash, table.length);
for (Entry p = null, e = table[idx]; e != null; e = e.next) {//p节点用来存储遍历节点的上一个节点,用于删除
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
// 删除键值对
V deleteValue = (V) e.val;
if (p == null) table[idx] = e.next;
else p.next = e.next;
size--;
return deleteValue;
}
p = e;
}
return null;
}
3.5 获取键的集合
/**
* 获取键的集合
*
* @return 键的集合
*/
@SuppressWarnings("unchecked")
public Set<K> keys() {
Set<K> set = new LinkedHashSet<>();
for (Entry e : table) {
while (e != null) {
set.add((K) e.key);
e = e.next;
}
}
return set;
}
3.6 其他小方法
/**
* 清空所有键值对
*/
public void clear() {
for (int i = 0; i < table.length; i++) {
table[i] = null;
}
size = 0;
}
/**
* 获取哈希表中键值对的个数
*
* @return 键值对的个数
*/
public int size() {
return size;
}
/**
* 判断哈希表是否为空
*
* @return 如果哈希表为空返回true, 否则返回false
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 判断键是否存在
*
* @param key 键
* @return 如果键存在返回true, 否则返回false
*/
public boolean contains(K key) {
return get(key) != null;
}