java.util.Map 接口
可以使用三个具体的类来创建一个映射表: HashMap 、LinkedHashMap 、TreeMap.
java.util.HashMap 使用散列实现
java.util.LinkedHashMap 使用LinkedList
java.util.TreeMap 使用红黑树。
1 散列的基本概念
-
回顾一下映射表(map) :
键Key - 值Value
。又称为字典( dictionary)、散列表、( hash table) 或者关联数组
(associate array) 。 -
散列非常高效。
-
使用散列将耗费O(1)时间来查找、插入以及删除一个元素。类比之前的学过的数组,数组通过索引获得元素,所以此处考虑:把键映射到一个索引上
-
散列使用一个
散列函数
,将一个键
映射到一个索引
上。 -
存储了值Value的数组称为
散列表(hash table)
。 -
将键映射到散列表中的索引上的函数称为散列函数(hash function) 。
散列函数从一个键Key获得索引,并使用索引来获取该键的值。 -
散列( hashing) 是一种无须执行搜索,即可通过从键得到的索引来获取值的技术。
2 散列函数、散列码
典型的散列函数首先将搜索键转换成一个称为散列码的整数值,然后将散列码压缩为散列表中的索引。
Java 的根类 Object
具有 hashCode 方法
,该方法返回一个整数的散列码。默认的,该方法返回一个该对象的内存地址。hashCode 方法的一般约定如下:
- 当equals 方法被重写时,应该重写hashCode 方法,从而保证两个相等的对象返回同样的散列码。
- 程序执行中,如果对象的数据没有被修改,则多次调用hashCode 将返回同样的整数。
- 两个不相等的对象可能具有同样的散列码,但是应该在实现hashCode 方法时避免太多这样的情形出现。
byte 、short 、int 、char 类型:对这几个类型的搜索键而言,简单地将它们转型为int 。因此,这些类型中的任何一个的不同搜索键将有不同的散列码。
float:对于float 类型的搜索键,使用 Float.floatToIntBits(key)
作为散列码。注意,floatToIntBits(float f) 返回一个int 值,该值的比特表示和浮点数f 的比特表示相同。因此,两个不同的float 类型的搜索键将具有不同的散列码。
long:不能简单地将其类型转换为int ,因为所有前32 比特不同的键将具有相同的散列码。考虑到前32 比特,将64 比特分为两部分,并执行 异或操作
将两部分结合。这个过程称为 折叠
int hashCode = (int)(key ^ (key >> 32)); >>为右移操作
字符串String类型的散列码:
一个比较直观的方法是将所有字符的Unicode 求和作为字符串的散列码,如果搜索键包含同样字母,将产生许多冲突。
一个更好的方法是 考虑字符的位置
,然后产生散列码。称为多项式散列码。
=
实验显示, b 的较好的取值为31 , 33 , 37 , 39 和41。String 类中,hashCode 采用b 值为31 的多项式散列码计算被重写。
3 压缩散列码
键的散列码可能是一个很大的整数,超过了散列表索引的范围,因此需要将它缩小到适合索引的范围。假设散列表的索引处于0 到N-1 之间。将一个整数缩小到0 到N-1 之间的最通常的做法是使用 h(hashCode) = hashCode % N
保证索引均匀扩展,选择N 为大于2 的素数。
理想的,应该为N 选择一个素数。然而,选择一个大的素数将很耗时。Java API 为 java.util.HashMap 的实现中, N 设置为一个2 的幂值。这样的选择具有合理性。当N 为2 的幂值时,上式与这个一样: h(hashCode) = hashCode & ( N - 1 ) .
&操作符比%操作符执行快许多。
为了保证散列码是均匀分布的, java.util.HashMap 的实现中采用了 补充的散列函数与主散列函数一起使用
。该函数定义为:
private static int supplementalHash(int h){
h ^= ( h >>> 20 ) ^ (h >>> 12);
return h ^ (h >>>7 ) ^ (h >>> 4);
^ 和>>>是比特的异或和无符号右移操作
完整的散列函数如下定义:
h(hashCode) = supplementalHash(hashCode) % N
这个与以下式子一样:
h(hashCode) = supplementalHash(hashCode) & (N - 1)
4. 地址冲突
当两个键映射到散列表中的同一个索引上,会冲突发生!通常,有两种方法处理冲突:开放地址法、链地址法。
4.1 开放地址法
开放地址法( open addressing) 是在冲突发生时,在散列表中找到一个开放位置的过程。
开放地址法有几个变体:线性探测、二次探测和再哈希法。
****
线性探测
当插入一个条目到散列表中发生冲突时,线性探测法( linear probing) 按顺序找到下一个可用的位置。例如,如果冲突发生在hashTable[k % N] ,则检查hashTabl e [(k+1) % N]是否可用。如果不可用,则检查hashTable[(k+2) %N],以此类推,直到一个可用单元被找到。
(当探测到表的终点时,则返回表的起点。因此,散列表被当成是循环的。)
散列表中的每个单元具有三个可能的状态:被占的、标记的或者空的。
线性探测法容易导致散列表中连续的单元组被占用。每个组称为一个簇(cluster ) 。每个簇实际上成为在获取、添加以及删除一个条目时必须查找的探测序列。当簇的大小增加时,它们可能合并为更大的簇,从而更加放慢查找的时间。这是线性探测法的一个较大的缺点。
二次探测法
二次探测法( quadratic probing) 可以避免线性探测法产生的成簇的问题。二次探测法则从索引为 (k + j^2) %N
位置的单元开始审查,其中 j >= 0 。即 k%N , ( k + 1) %N , (k+4 ) %N, (k+9) %N, 以此类推.。。。
二次探测法避免了线性探测法的成簇问题,但是有自己本身的成簇问题,称为二次成簇( seconda叩clustering); 即在一个被占据的条目处产生冲突的条目将采用同样的探测序列。
线性探测法可以保证只要表不是满的, 一个可用的单元总是可以被找到用于插入新的元素。然而, 二次探测法不能保证这个。
再哈希法
另外一个避免成簇问题的开放地址模式称为再哈希法( double hasbing ) 。
从初始索引k开始,线性探测法和二次探测法都对k 增加一个值来定义一个搜索序列。对于线性探测法来说增量为1 ,对于二次探测法来说增量为f。这些增量都独立于键。
再哈希法在键上应用一个 二次散列函数h' (key)
来确定增量, 从而避免成簇问题。
具体来说,再哈希法审查索引为 (k+ j*h' (key)) %N
处的单元,其中 j >= 0 ,即 k%N , (k+h’ (key)) %N , (k+2* h’ (key)) %N,
(k+3* h’ (key)) %N, 以此类推。
例如,让一个大小为11 的散列表的主散列函数h 和二次散列函数h’ 如下定义:
h(key) = key % 11 ;
h’(key) = 7 - key % 7;
4.2 链地址法
链地址法将具有同样的散列索引的条目都放在一个位置,而不是寻找一个新的位置。链地址法的每个位置使用一个桶来放置多个条目。可以使用数组, ArrayList 或者LinkedList 来实现一个桶。
5. 装填因子
装填因子( load factor) 衡量一个散列表有多满。如果装填因子溢出,则增加散列表的大小,并重新装载条目到一个新的更大的散列表中。这称为再散列。
当λ 增加时,冲突的可能性增大。研究表明,对于开放地址法而言,需要维持装填因子在0.5 以下,而对于链地址法而言,维持在0.9 以下。
将装填因子保持在一定的阔值下对于散列的性能是非常重要的。Java APl 中java.util.HashMap类的实现中,采用了阔值0.75 。一旦装填因子超过阈值,则需要增加散列表的大小,并将映射表中所有条目再散列( rehash) 到一个更大的散列表中。注意需要修改散列函数,因为散列表的大小被改变了。由于再散列代价比较大,为了减少州现再散列的可能性应该至少将散列表的大小翻倍。即使需要周期性的再散列,对于映射表来说散列依然是一种高效的实现。
6. 手写HashMap
(使用 链地址法 来实现映射表)
首先,参照java.util.Map 设计自定义的Map接口,接口命名MyMap,集体类命名MyHashMap。
package ReWrite;
public interface MyMap<K,V> {
public void clear();
public boolean containsKey(K key);
public boolean contansVaule(V value);
public java.util.Set<Entry<K,V>> entrySet();
public V get(K key);
public boolean isEmpty();
public java.util.Set<K> keySet();
public V put(K key,V value);
public void remove(K key);
public int size();
public java.util.Set<V> values();
public static class Entry<K,V>{
K key;
V value;
public Entry(K key,V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
@Override
public String toString() {
return "[" + key + "," + value + "]";
}
}
}
接下来是主体部分。
首先要确定哈希表的大小,装载因子。再添加HashMap中的常用方法们
package ReWrite;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
public class MyHashMap<K,V> implements MyMap<K,V> {
private static int DEFAULT_INITIAL_CAPACITY = 4; // 默认哈希表的大小,必须是2的幂值
private static int MAXIMUN_CAPACITY = 1 << 30; // 定义最大的哈希表大小 = 2^30
private int capacity; // 当前哈希表的容量
private static float DEFAULT_MAX_LOAD_FACTOR = 0.75f; // 加载因子
private float loadFactorThreshold; // 指定加载因子
LinkedList<MyMap.Entry<K,V>>[] table; // 哈希表是一个数组,每个单元格都是一个链表
private int size = 0;
public MyHashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_MAX_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity) {
this(initialCapacity,DEFAULT_MAX_LOAD_FACTOR);
}
public MyHashMap(int initialCapacity,float loadFactorThreshold) {
if ( initialCapacity > MAXIMUN_CAPACITY )
this.capacity = MAXIMUN_CAPACITY;
else
this.capacity = trimToPowerOf2(initialCapacity);
this.loadFactorThreshold = loadFactorThreshold;
table = new LinkedList[capacity];
}
/**
* 全部清除
*/
@Override
public void clear() {
size = 0;
removeEntries();
}
/**
* 检测指定的键是否在映射表中,通过调用get方法,耗费 O(1)时间
*/
@Override
public boolean containsKey(K key) {
if(get(key) != null )
return true;
else
return false;
}
/**
* 检测某值是否存在于映射表中。O (capacity + size) 时间
*/
@Override
public boolean contansVaule(V value) {
for(int i = 0; i < capacity; i++) {
if( table[i] != null) {
LinkedList<Entry<K,V>> bucket = table[i];
for(Entry<K,V> entry: bucket)
if(entry.getValue().equals(value))
return true;
}
}
return false;
}
/**
* 返回一个包含映射表中所有条目的集合. O(capacity) 时间
*/
@Override
public Set<MyMap.Entry<K,V>> entrySet() {
java.util.Set<Entry<K,V>> set = new java.util.HashSet<>();
for(int i = 0 ; i < capacity; i++) {
if( table[i] != null ) {
LinkedList<Entry<K,V>> bucket = table[i];
for(Entry<K,V> entry: bucket)
set.add(entry);
}
}
return set;
}
/**
* 返回具有指定键的第一个条目的值,需要0(1)时间
*/
@Override
public V get(K key) {
int bucketIndex = hash(key.hashCode());
if(table[bucketIndex] != null ) {
LinkedList<Entry<K,V>> bucket = table[bucketIndex];
for(Entry<K,V> entry: bucket)
if(entry.getKey().equals(key))
return entry.getValue();
}
return null;
}
/**
* 在映射表为空的情况下简单地返回true,需要0(1)时间
*/
@Override
public boolean isEmpty() {
return size==0;
}
/**
* 返回一个包含映射表中所有键的集合,该方法从每个桶中寻找到键并将它们加入一个集合中。该方法需要O(capacity) 时间
*/
@Override
public Set<K> keySet() {
Set<K> set = new java.util.HashSet<K>();
for(int i = 0 ; i < capacity; i++) {
if(table[i] != null ) {
LinkedList<Entry<K,V>> bucket = table[i];
for(Entry<K,V> entry: bucket)
set.add(entry.getKey());
}
}
return set;
}
/**
* 添加一个新的条目到映射表中。
* 首先测试该键是否已经在映射表中,如果是--定位该条目,并将该键所在的条目的旧值替换成新值,并返回旧值
* 如果键不在映射表中,则在映射表中产生一个新的条目
*/
@Override
public V put(K key, V value) {
if(get(key) != null ) {
int bucketIndex = hash(key.hashCode());
LinkedList<Entry<K,V>> bucket = table[bucketIndex];
for(Entry<K,V> entry: bucket)
if(entry.getKey().equals(key)) {
V oldValue = entry.getValue();
entry.value = value;
return oldValue;
}
}
if(size >= capacity * loadFactorThreshold) {
if( capacity == MAXIMUN_CAPACITY)
throw new RuntimeException("Exceeding maximun capacity");
rehash();
}
int bucketIndex = hash(key.hashCode());
if(table[bucketIndex] == null)
table[bucketIndex] = new LinkedList<Entry<K,V>>();
table[bucketIndex].add(new MyMap.Entry<K,V>(key, value));
size++;
return value;
}
/**
* 删除映射表中指定键的条目,花费 O(1) 时间
*/
@Override
public void remove(K key) {
int bucketIndex = hash(key.hashCode());
if(table[bucketIndex] != null ) {
LinkedList<Entry<K,V>> bucket = table[bucketIndex];
for(Entry<K,V> entry: bucket)
if(entry.getKey().equals(key)) {
bucket.remove(entry);
size--;
break;
}
}
}
@Override
public int size() {
return size;
}
/**
* 返回映射表中所有的值。从所有的桶中检测每个条目,然后添加值到一个集合中。花费O(capacity) 时间。
*/
@Override
public Set<V> values() {
Set<V> set = new HashSet<>();
for(int i = 0 ; i < capacity; i++) {
if(table[i] != null ) {
LinkedList<Entry<K,V>> bucket = table[i];
for(Entry<K,V> entry: bucket)
set.add(entry.getKey());
}
}
return set;
}
/**
* 调用suppler问ntalHash 方法来确保为散列表生成索引的散列是均匀分布的,该方法花费0(1) 时间。
* @param hashCode
* @return
*/
private int hash(int hashCode) {
return supplementalHash(hashCode) & (capacity -1 );
}
/**
* 哈希code计算
* @param h
* @return
*/
private static int supplementalHash(int h) {
h ^= ( h >>> 20) ^ ( h >>> 12) ;
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 确保数为2的幂值
* @param initialCapacity
* @return
*/
private int trimToPowerOf2(int initialCapacity) {
int capacity = 1;
while(capacity < initialCapacity) {
capacity <<=1;
}
return capacity;
}
private void removeEntries() {
for(int i = 0; i < capacity; i++) {
if(table[i] != null)
table[i].clear();
}
}
private void rehash() {
Set<Entry<K,V>> set = new HashSet<>();
capacity <<= 1; // capacity = capacity * 2;
table = new LinkedList[capacity];
size = 0;
for(Entry<K,V> entry:set) {
put(entry.getKey(),entry.getValue());
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[");
for(int i = 0 ; i < capacity; i++) {
if(table[i] != null && table[i].size() > 0)
for(Entry<K,V> entry:table[i])
builder.append(entry);
}
builder.append("]");
return builder.toString();
}
}