符号表(Map)概述
符号表在java中就是Map接口,说白了就是映射
我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息。
符号表有时也被称为字典。键就是单词,值就是单词对应的定义,发音和词源。
符号表有时又叫做索引。键就是术语,值就是书中该术语出现的所有页码。
符号表的应用:
····················································································································································································································
哈希表概述
如果所有的键都是小整数,我们可以用一个数组来实现的符号表,将键作为数组的索引,而数组中对应的位置存储键关联的值。
小:如果键值很大,就需要创建很大空间的数组
整数:不能是String类型或者Student类型,因为数组下标只能是整数。
哈希表是这种简单方法的扩展,并且能够处理更加复杂类型的键。我们需要用哈希函数将键转换成数组的索引。
哈希表的核心算法可以分为两步。
-
用哈希函数将键转换为数组中的一个索引。理想情况下不同的键都能转换成不同的索引值。当然这只是理想情况下,所以我们需要处理两个或者多个键都散列到相同索引值的情况 (哈希碰撞)。
-
处理碰撞冲突。
a. 开放地址法
线性探测法, 平方探测法, 再散列法…
b. 拉链法
哈希函数
一个优秀的 hash 算法,满足以下特点:
- 正向快速:给定明文和 hash 算法,在有限时间和有限资源内能计算出 hash 值。
正向快速是哈希函数本身的一个效率问题 - 逆向困难:给定(若干) hash 值,在有限时间内很难(基本不可能)逆推出明文。
- 输入敏感:原始输入信息修改一点信息(哪怕只有一比特不一样),产生的 hash 值看起来应该都有很大不同。
- 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。即对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
我们可以简单地把哈希函数理解为在模拟随机映射,然后从随机映射的角度去理解哈希函数。
经典的哈希函数
MD4: 128位,MD4已被证明不够安全。
MD5:MD5 已被证明不具备"强抗碰撞性"。
SHA1:SHA-1 已被证明不具"强抗碰撞性"。
SHA2:
SHA3: 相关算法已经被提出。
哈希算法的应用
- 指纹 (身份的标识)
a. 证书, 文件
b. 加密
假如你去柜台,让银行小姐姐给查看一下你的银行卡密码,她也查不到,为啥子,因为银行数据库里存的是密码是你的密码明文经过哈希映射后的哈希值,而哈希函数逆向困难。
但是假如你输入的是一个错误密码,但这个错误密码和你正确密码的哈希值正好相同了(概率极小),那你也能登陆成功。
也就是说,密码D1的哈希值是hash1,而密码D2的哈希值也是hash1,输入密码D2也能登陆成功,因为数据库中存的就是你的哈希值,而不是明文。
所以,怎么算把一个密码破解了呢?把一个哈希值对应的明文求出就算破解了,管你密码是D1还是D2,我求出一个就能登上你的账号了。
而如果黑客盗取一个系统的数据库,他得到的其实是用户名和用户密码对应的哈希值,而你拿着哈希值是无法登陆系统的。
由于逆向推出密码明文十分困难,于是,黑客就整一个很大的Map,比如书MD5加密的Map,键是哈希值,值是明文。他就拿哈希值去这个Map库中去找,有没有对应的明文,这样的话,如果是HashMap的话,就能做到O(1)的时间复杂度,找到密码明文就很快。这就叫撞库攻击。
那如何防御撞库攻击呢?
- 不要取简单的密码,最好是数字和英文大小写组合。因为撞库攻击中,黑客的Map库肯定不全,你不可能一个库中把世界上所有的密码都包含了,所以用户设计的密码越复杂,黑客的库拥有你的密码的哈希值的可能性越小,越不容易被盗。
- 加盐。服务器端得到密码后,在密码尾部或前端加上某个字符串如:“abc”,得到新的密码后,再通过哈希算法得到哈希值,存在数据库中。黑客就算盗走了哈希值,他撞库攻击后得到的密码明文也是在原始密码后加“abc”的密码,并不知道加盐算法是啥,也就无法得到原始密码。
…
- 散列
a. 集群, 分布式
b. 数据结构(哈希表中的哈希函数)
…
假如京东有十台服务器,而618也快到了,有的服务器特别忙,有的服务器特别闲,怎么做到负载均衡呢?
因为哈希函数可以简单的理解为随机映射,就可以将任务随机的分配到每个服务器
注意事项:其实把hash算法当成是一种加密算法,这是不准确的,我们知道加密总是相对于解密而言的,没有解密何谈加密呢,HASH的设计以无法解密为目的的。并且如果我们不附加一个随机的salt值,HASH口令是很容易被字典攻击入侵的。
····················································································································································································································
哈希函数在数据结构中的应用
在数据结构中,对速度比较重视,对抗碰撞性(有多个值的哈希值是一样的,没关系,只要保证哈希值的平均分布即可)不太看重。所以对哈希函数的要求没那么高,
只要满足下面两点就可以了。
- 计算速度快。
- Hash值平均分布。
处理碰撞冲突——拉链法
发生碰撞冲突的元素都存储在同一条链表中。
在哈希函数能保证平均分布的前提下,那么哈希表的性能就取决于链表的平均长度。
如果我们想在常数时间复杂度内, 完成哈希表的增删查操作,那么我们就得控制链表的平均长度不超过某个值。这个值我们称之为加载因子,也就是链表平均长度可以达到的最大值。
因此,当元素个数达到一定的数目的时候,我们就需要对数组进行扩容。
简化处理
-
键不能为null。如果键为null,我们会抛出 NullPointerException.
-
值不能为null。我们这么规定的原因是:当键不存在的时候,get()方法会返回null。这样做有个好处,我们可以调用get()方法,看其返回值是否为 null 来判断键是否存在哈希表中。
-
缓存hash值。如果散列值计算很耗时 (比如长字符串)。那么我么可以在结点中用一个 hash 变量来保存它计算的 hash 值。Java 中的 String 就是这样做的。
-
当数组达到最大值时,并且链表的平均长度达到了最大值。这种情况,我们就破坏加载因子的限制,直接添加元素。
自己实现的HashMap:
package com.cskaoyan;
import java.util.LinkedHashSet;
import java.util.Set;
import static com.sun.xml.internal.fastinfoset.util.KeyIntMap.indexFor;
/**
* @author shihao
* @create 2020-05-25 19:34
* <p>
* 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; //2^30
private static final float DEFAULT_LOAD_FACTOR = 0.75F;
//属性
private Entry[] table;
private int size;
private float loadFactor;
private int threshold; //阈值,当size达到threshold时就需要扩容了
private static class Entry {
Object key;
Object value;
int hash;
Entry next;
public Entry(Object key, Object value, int hash, Entry next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
@Override
public String toString() {
return key + "=" + value;
}
}
//构造方法
public MyHashMap() {
this(DEFAULT_ARRAY_SIZE, DEFAULT_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity <= 0 || initialCapacity > MAX_ARRAY_SIZE) {
throw new IllegalArgumentException("initialCapacity = " + initialCapacity);
}
if (loadFactor < 0) {
throw new IllegalArgumentException("loadFactor =" + loadFactor);
}
//求大于等于initialCapacity最小2的幂次方
int capacity = calculateCapacity(initialCapacity);
table = new Entry[capacity];
this.loadFactor = loadFactor;
this.threshold = (int) (table.length * loadFactor);
}
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;
}
//方法
/**
* 添加键值对,如果键已经存在,则更新它相关联的值
*
* @param key 键
* @param value 值
* @return 原来的值,如果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!");
}
int hash = hash(key);
int index = indexFor(hash, table.length);
//遍历链表,查找key
for (Entry e = table[index]; e != null; e = e.next) {
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
//更新值,并把原来的值返回
V retValue = (V) e.value;
e.value = value;
return retValue;
}
}
addEntry(key, value, hash, index);
return null;
}
private void addEntry(K key, V value, int hash, int index) {
//判断是否需要扩容
if (size >= threshold) {
//当数组达到最大值时,并且链表的平均长度达到了最大值。
// 这种情况,我们就破坏加载因子的限制,直接添加元素。
if (table.length == MAX_ARRAY_SIZE) {
//破坏加载因子的限制
threshold = Integer.MAX_VALUE;
} else {
//table长度扩大为原来的两倍
grow(table.length >> 1);
//重新计算索引值
index = indexFor(hash, table.length);
}
}
//添加结点(头插法)
Entry e = new Entry(key, value, hash, table[index]);
table[index] = e;
size++;
}
private void grow(int cap) {
Entry[] newTable = new Entry[cap];
//重新散列
for (Entry e : table) {
while (e != null) {
//保存e的next结点
Entry next = e.next;
//计算在新数组中的索引
int i = indexFor(e.hash, cap);
//在新数组中进行头插法
e.next = newTable[i];
newTable[i] = e;
//e指向原链表的下一个结点
e = next;
}
}
table = newTable;
//更新阈值
threshold = (int) (cap * loadFactor);
}
/**
* 通过键获取值
*
* @param key 键
* @return 键关联的值,如果键不存在返回null
*/
@SuppressWarnings("unchecked")
public V get(K key) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null!");
}
int hash = hash(key);
int index = indexFor(hash, table.length);
//遍历链表,找到key关联值
for (Entry e = table[index]; e != null; e = e.next) {
if (hash == e.hash && (key == e.key || key.equals(e.key))) {
return (V) e.value;
}
}
//没有这样的key
return null;
}
/**
* 找哈希值hash所在table中的索引位置
*
* @param hash 哈希值
* @param length table的长度
* @return 索引值
*/
private int indexFor(int hash, int length) {
return hash & (length - 1);
}
/**
* 自定义的哈希函数
*
* @param key key
* @return 哈希值
*/
private int hash(K key) {
int h = key.hashCode();
return (h << 16) ^ (h >> 16);
}
/**
* 判断key是否在哈希表中存在
*
* @param key 键
* @return 如果存在返回true,否则返回false
*/
public boolean contains(K key) {
return get(key) != null;
}
public V delete(K key) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null!");
}
int hash = hash(key);
int index = indexFor(hash, table.length);
//遍历链表找到key关联值
for (Entry e = table[index], parent = null; e != null; parent = e, e = e.next) {
if (hash == e.hash && (key == e.key || e.equals(e.key))) {
//删除结点
V retValue = (V) e.value;
if (parent == null) table[index] = e.next;
else parent.next = e.next;
size--;
return retValue;
}
}
return null;
}
/**
* 清空所有键值对
*/
public void clear() {
// for (Entry e : table) e = null;
//这行代码相当于下面三行代码,foreach循环并不能更改一个元素的值
// for (int i = 0; i < table.length; i++) {
// Entry e = table[i];
// e = null;
// }
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;
}
/**
* 获取键的集合
*
* @return 键的集合
*/
@SuppressWarnings("unchecked")
public Set<K> keys() {
//用LinkedHashSet是为了保证遍历顺序就是MyHashMap的输出顺序
Set<K> set = new LinkedHashSet<>();
for (Entry e : table) {
while (e != null) {
set.add((K) e.key);
e = e.next;
}
}
return set;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
for (Entry e : table) {
while (e != null) {
sb.append(e).append(", ");
e = e.next;
}
}
if (!this.isEmpty()) sb.delete(sb.length() - 2, sb.length());
return sb.append("}").toString();
}
public static void main(String[] args) {
// V put(K key, V value)
MyHashMap<Character, String> map = new MyHashMap<>();
// System.out.println(map);
map.put('A', "Allen");
map.put('B', "Beyonce");
map.put('C', "Catalina");
map.put('D', "Diana");
map.put('2', "弄什呢");
// System.out.println(map);
System.out.println(map.put('A', "Adele"));
System.out.println(map);
// boolean contains(K key), V get(K key)
/*System.out.println(map.get('B'));
System.out.println(map.get('X'));
System.out.println(map.contains('B'));
System.out.println(map.contains('X'));*/
// V delete(K key)
/*System.out.println(map.delete('C'));
System.out.println(map);
System.out.println(map.size());
System.out.println(map.delete('X'));
System.out.println(map);
System.out.println(map.size());*/
// int size(), void clear(), boolean isEmpty()
/* System.out.println(map);
System.out.println(map.size());
System.out.println(map.isEmpty());
map.clear();
System.out.println(map);
System.out.println(map.size());
System.out.println(map.isEmpty());*/
// Set<K> keys()
Set<Character> keys = map.keys();
//和System.out.println(map);的输出顺序一致
for (char key : keys) {
String value = map.get(key);
System.out.println(key + "=" + value);
}
}
}
····················································································································································································································
面试题:
并发场景
HashMap 类不同步,在并发场景中可能会出现许多问题。比较经典的一个问题是:查找的时候可能会出现死循环。(扩容方法中)
Q1: HashMap 和 Hashtable 的区别?
Q2: Java 给我们提供了哪些并发安全的 Map?我们应该选择哪一个?
-
Hashtable (看一下hashtable源码会发现hashtable有很多方法用的synchronized nchronized 方法,这样的话锁对象就是this,我们知道哈希表table的每个index下挂着一个链表,假如我在index=1的链表下进行增删查操作是不会影响index=2下的数据的,但由于hashtable的锁对象是this,所以,它不允许我们进行以上操作,尽管以上操作并不会发生线程安全,所以,hashtable的并发度是很低的 )
-
Collections.synchronizedMap(Map map) (Collections是一个工具类,并发度和hashtable一样)
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
返回指定有序映射支持的同步(线程安全的)有序映射。
3. ConcurrentHashMap(用它)
Hashtable 锁对象是 this对象
Collections.synchronizedMap(Map map) 锁对象是 mutex
JDK8 ConcurrentHashMap 锁对象是链表的头结点。 大大提高并发度。这样的话,关于起那么的index=1和index=2上两条链表的操作就可以同时进行了。
Q3: JDK8 对 HashMap 的改进。
如果某条链表的长度超过8, 把这条链表变成红黑树。(红黑树查询的时间复杂度是O(logN))
如果某条链表的长度低于6, 把红黑树变成链表。(链表太长不容易增删查)
·····················································································································································································································
接下来看一下Collections工具类的某些API:
-
static <T extends Comparable<? super T>>
void
sort(List list)
根据元素的自然顺序 对指定列表按升序进行排序。 -
static void
sort(List list, Comparator<? super T> c)
根据指定比较器产生的顺序对指定列表进行排序。 -
static int
binarySearch(List<? extends Comparable<? super T>> list, T key)
使用二分搜索法搜索指定列表,以获得指定对象。 -
static void reverse(List<?> list)
反转指定列表中元素的顺序。 -
static void shuffle(List<?> list)
使用默认随机源对指定列表进行置换。 -
static boolean disjoint(Collection<?> c1, Collection<?> c2)
如果两个指定 collection 中没有相同的元素,则返回 true。 -
static int frequency(Collection<?> c, Object o)
返回指定 collection 中等于指定对象的元素数。 -
static void
fill(List<? super T> list, T obj)
使用指定元素替换指定列表中的所有元素。 -
static void rotate(List<?> list, int distance)
根据指定的距离轮换指定列表中的元素。 -
static void swap(List<?> list, int i, int j)
在指定列表的指定位置处交换元素。
package com.Collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author shihao
* @create 2020-05-26 17:01
*
Collections:
<T extends Comparable<? super T>> void sort(List<T> list)
static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)
static void reverse(List<?> list)
static void shuffle(List<?> list)
static boolean disjoint(Collection<?> c1, Collection<?> c2)
static int frequency(Collection<?> c, Object o)
static <T> void fill(List<? super T> list, T obj)
static void rotate(List<?> list, int distance)
static void swap(List<?> list, int i, int j)
*/
public class Demo {
public static void main(String[] args) {
// List<String> list = new LinkedList<>();
List<String> list = new ArrayList<>();
list.add("Cindy");
list.add("Beyonce");
list.add("Adele");
list.add("Diana");
//System.out.println(list);
// 排序
Collections.sort(list);
// System.out.println(list);
// 二分查找:两个先决条件:1.数组 2.有序 如果不是数组,是链表的话效率不会有太大提升
// int index = Collections.binarySearch(list, "Beyonce");
//如果没有查找到,返回负数,根据这个负数,我们知道他是从哪个位置退出来的
//如返回-3,表示从index=2处退出,-(index+1)=-3
// int index = Collections.binarySearch(list, "Catalina");
// System.out.println(index);
// 逆序
// Collections.reverse(list);
// System.out.println(list);
// 乱序
// Collections.shuffle(list);
// System.out.println(list);
// 判断两个集合是否不相交(相交即两个集合有共同元素),不相交返回true
// static boolean disjoint(Collection<?> c1, Collection<?> c2)
/*Collection c = new ArrayList();
c.add("beijing");
c.add("wuhan");
System.out.println(Collections.disjoint(list, c));
c.add("Adele");
System.out.println(Collections.disjoint(c, list));*/
// static int frequency(Collection<?> c, Object o)
//o在集合c中出现的次数
/*System.out.println(Collections.frequency(list, "Beyonce"));
System.out.println(Collections.frequency(list, "HelloKitty"));
list.add("Beyonce");
System.out.println(Collections.frequency(list, "Beyonce"));*/
// static <T> void fill(List<? super T> list, T obj)
//指定用obj填充整个list
/*Collections.fill(list, "刘亦菲");
System.out.println(list);
System.out.println(list.size());*/
// static void rotate(List<?> list, int distance)
//旋转distance距离
/*System.out.println(list);
Collections.rotate(list, 1);
Collections.rotate(list, 4);
Collections.rotate(list, -1);
System.out.println(list);*/
// static void swap(List<?> list, int i, int j)
//交换list中索引为i和索引为j的元素
/*Collections.swap(list, 1, 2);
System.out.println(list);*/
}
}