1 前言
散列表(hash table)ADT,常常叫做散列(hashing),基本结构是数组+链表,是一种用于以常数平均时间
O
(
1
)
O(1)
O(1) 执行插入 ,删除和查找的技术,排序不会得到有效的支持,诸如findMin和findMax都是散列不支持的。
通多key --value 进行存储数据,key作为关键字,通过散列函数分布到数组,value则插入到关键字对应的位置的数据结构中。
2 散列函数
通过散列函数计算key的位置,即对应散列数组的下标。
- 散列要保证均匀分布
- 加载因子: 散列表元素个数/散列表长度,HashMap的是0.75,超过就要扩容。
一个简单的散列
// 使用 字符串的ASCII 或 Unicode 编码,如果碰到 大的表,不能保证 关键字均匀分配
public static int hash(String key,int tableSize){
int hashVal = 0;
for(int i=0;i<key.length();i++){
hashVal+=key.charAt(i);
}
System.out.println("hashVal is "+hashVal);
return hashVal%tableSize;
}
这是一个不好的,计算简单,但散列的数量有限,分布不均匀。
- 散列冲突: 当两个数散列到同一个位置,就产生了冲突,主要编程就是解决冲突。
- 散列表长度,即数组长度保持素数,散列效果好。
一个好的散列
// hk = ((k2)*37+k1)*37+k0
public static int goodHash(String key,int tableSize){
int hashVal = 0;
for(int i=0;i<key.length();i++){
hashVal = 37*hashVal+key.charAt(i);
}
hashVal %= tableSize;
if(hashVal<0){
hashVal+=tableSize;
}
return hashVal;
}
3 解决散列冲突
3.1 分离链接法
将散列到同一个位置的元素保留在一个表中,比如HashMap就是这样解决的。
- 基本结构: 数组+链表
插入的时候,通过计算hash码,找到存放数据的数组下标,将新元素插入到对应的链表头部。因为新插入的元素可能不久要被访问。
下面是实现
package 散列.分离链接法;
import java.util.LinkedList;
import java.util.List;
public class SeparateChainingHashTable <T>{
private static final int DEFAULT_TABLE_SIZE = 101; // 默认表长度
private List<T>[] theLists; // theLists 是数组,里边的元素是双向链表
private int currentSize; // 已经散列的元素个数
public SeparateChainingHashTable() {
this(DEFAULT_TABLE_SIZE);
}
public SeparateChainingHashTable(int size) {
theLists = new LinkedList[nextPrime(size)];
for (int i = 0; i < theLists.length; i++) {
theLists[i] = new LinkedList<>();
}
}
// 此处 另加载因子等于 1
public void insert(T x){
List<T> whichList = theLists[myHash(x)]; // myhash 函数的得出 插入元素 x 在 数组存放分下表,返回结果说就是 要插入的 链表
if(!whichList.contains(x)){ // 先检查 是是否存在,然后 添加
whichList.add(x);
if (++currentSize > theLists.length){
reHash();
}
}
}
public void remove(T x){
List<T> whichList = theLists[myHash(x)];
if(!whichList.contains(x)){
whichList.remove(x);
currentSize--;
}
}
public boolean contains(T x){
List<T> whichList = theLists[myHash(x)];
return whichList.contains(x);
}
public void makeEmpty(T x){
for (int i = 0; i < theLists.length; i++) {
theLists[i].clear();
currentSize = 0;
}
}
private void reHash(){
List<T>[] oldLists=theLists;//复制一下一会要用 theLists在又一次new一个
//对在散列的表同样进行素数修正
theLists=new LinkedList[nextPrime(2*theLists.length)];
for(int i=0;i<theLists.length;i++)
{
theLists[i]=new LinkedList<T>();
}
//把原来的元素拷贝到新的数组中 注意是把集合中的元素复制进去
for(int i=0;i<oldLists.length;i++)
{
for (T t : oldLists[i])
{
insert(t);
}
}
}
// 将 hashCode 返回的int,转成适当的数组下标
private int myHash(T x){
int hashVal = x.hashCode();
hashVal %= theLists.length;
if(hashVal<0){
hashVal+=theLists.length;
}
return hashVal;
}
// 求 下一个素数
private static int nextPrime(int n){
while(!isPrime(n))
{
n++;
}
return n;
}
// 判断是否是 素数
private static boolean isPrime(int n){
int i=1;
while((n%(i+1))!=0) {
i++;
}
if(i==n-1)
return true;
else
return false;
}
}
上面我们的加载因子 是 1,通过数学证明我们知道,散列表的大小对于效率影响不大,加载因子才是最重要的。
2.1 不用链表的(开放地址法)
分离链接法是使用一些链表,因为给新单元分配地址需要时间,导致算法速度减慢,可以采用探测散列表的方法,找出空的位置,这中思路要求 加载因子低于 0.5。
2.1.1 线性探测法
利用线性函数
f
(
i
)
=
i
f(i)=i
f(i)=i,逐个探测找到空的单元。
比如
散列函数是
h
a
s
h
v
a
l
u
e
hashvalue
hashvalue mod 10,
初始数组长度 10,下标 是 0到9,第一次插入 89,散列后是 9的位置,第二次插入18,放到 8的位置,第三次插入49,散列结果是9,因为9的位置已经有89了,所以需要找到一个空的单元,从第0个位置开始,发现空的位置,就插入,插入58 同理。
- 只要表足够大,总能找到一个位置,但花费时间也多,占据单元也会形成一些聚集区块。
2.1.2 平方探测法
和线性原理一样,使用的函数是 f ( i ) = i 2 f(i)=i^2 f(i)=i2。