1、为何要用哈希表java
树的操做一般须要O(N)的时间级,而哈希表中不管存有多少数据,它的插入和查找(有时包括删除)只须要接近常量级的时间,即O(1)的时间级。算法
可是哈希表也有必定的缺点:它是基于数组的,数组建立后难以扩展。而某些哈希表在基本填满时,性能降低明显,因此事先必须清楚哈希表中将要存储多少数据。并且目前没有一种简便的方法能够对哈希表进行有序(从大到小或者从小到大)的遍历,除非哈希表自己是有序的,但事实上这是违背哈希原则的。数组
综合以上:当不须要有序遍历数据,并且能够提早预测须要存储的数据项的数目,使用哈希表的结构是十分方便的。数据结构
2、哈希化dom
把巨大的整数(关键字)范围压缩到一个可接受的数组范围内,便于存储和查找。一般来讲,咱们要存储5000个数据,但数据的关键字范围多是0-200000。咱们不可能去开辟200000的数组去存储这5000个数据,这就须要一个函数把关键字和数组下标对应起来。这就是哈希函数。一般的作法是取余操做。i=N%size;i为下标,N为关键字,size为数组大小。不过一般来讲,size设为要存储数据项数目的两倍。函数
若是哈希表存满时,须要扩展哈希表。咱们须要新建一个更大的数组来存储数据,而后把原表中数据一一取出放入新表中。须要注意的是数据放入新表时须要从新用哈希函数计算哈希值,不能直接进行数组的复制,由于哈希函数的size已经变了。性能
一般而言咱们把哈希数组的容量设为一个质数。首先来讲假如关键字是随机分布的,那么无所谓必定要模质数。但在实际中每每关键字有某种规律,例如大量的等差数列,那么公差和模数不互质的时候发生碰撞的几率会变大,而用质数就能够很大程度上回避这个问题。对于除法哈希表h(k)=k mod m,注意二进制数对取余就是该二进制数最后r位数。这样一来,Hash函数就和键值(用二进制表示)的前几位数无关了,这样咱们就没有彻底用到键值的信息,这种选择m的方法是很差的。因此好的方法就是用质数来表示m,使得这些质数,不太接近2的幂或者10的幂。spa
3、解决冲突设计
首先通常哈希表是不容许重复的关键字,不然查找函数只能返回最早查到的关键字,没法找到全部的对应数据项。若是重写查找函数让它能够找到全部的对应数据项,这又会使得不管是不是重复关键字,查找操做都要搜索整个表,很是耗时。code
存储过程当中可能出现存储的数据项关键字不一样,但计算出来的哈希值是相同的,这就是冲突。
一般采用如下两种方法来解决冲突。
一、开放地址法
直接在哈希表中找到一个空位,把冲突的数据项存进去。
二、链地址法
把哈希表中存储的数据格式设为链表,这样能够把冲突的数据放入对应位置的链表中便可。
4、开放地址法的java实现
根据在查找下一个空位置时采用的方法,能够把开放地址法分为三种:线性探测、二次探测和再哈希法。
一、线性探测法
线性探测就是根据数组下标一个挨着一个去检测,直到找到一个空位置。
java实现:
DataItem类定义了哈希表存储的数据内容和关键字。
package hash;
class DataItem //hash表中存放的数据格式
{
private int iData; // 设为关键字
//--------------------------------------------------------------
public DataItem(int ii) // 构造器
{ iData = ii; }
//--------------------------------------------------------------
public int getKey() //获取关键字
{ return iData; }
//--------------------------------------------------------------
} // end class DataItem
////////////////////////////////////////////////////////////////
package hash;
class HashTable //定义哈希表
{
private DataItem[] hashArray; // 数组形式
private int arraySize; //哈希表的大小
private DataItem nonItem; // 删除数据时,将被删除的数据设为nonItem
//-------------------------------------------------------------
public HashTable(int size) //构造器,指定哈希表的大小
{
arraySize = size;
hashArray = new DataItem[arraySize];
nonItem = new DataItem(-1); // 把nonItem的关键字设为-1
}
//-------------------------------------------------------------
public void displayTable() //显示哈希表
{
System.out.print("Table: ");
for(int j=0; j<arraySize; j++)
{
if(hashArray[j] != null)
System.out.print(hashArray[j].getKey() + " ");
else
System.out.print("** "); //该位置没有存数据
}
System.out.println("");
}
//-------------------------------------------------------------
public int hashFunc(int key)
{
return key % arraySize; // 哈希函数
}
//-------------------------------------------------------------
public void insert(DataItem item) // 插入数据
// 默认表未满,事实上哈希表是不容许存满的,哈希表的大小比实际存储的数据数要大。
{
int key = item.getKey(); // 获取数据项的关键字,用于计算哈希值
int hashVal = hashFunc(key); // 计算哈希值
// 当前位置存有数据而且该数据未被删除
while(hashArray[hashVal] != null &&
hashArray[hashVal].getKey() != -1)
{
++hashVal; // 查找下一个位置
hashVal %= arraySize; // 到达表的末尾时,hashVal值变成1,。构成循环,从而能够查找整个表
}
hashArray[hashVal] = item; // 找到位置
} // end insert()
//-------------------------------------------------------------
public DataItem delete(int key) // 根据关键字删除数据
{
int hashVal = hashFunc(key); // 根据关键字计算哈希值
while(hashArray[hashVal] != null) // 该位置存有数据
{ // 二者的关键字是否相同
if(hashArray[hashVal].getKey() == key)
{
DataItem temp = hashArray[hashVal]; // 保存删除的数据项,用于返回
hashArray[hashVal] = nonItem; // 删除
return temp; // 返回删除的数据项
}
++hashVal; // 关键字不相同,继续查找下一个
hashVal %= arraySize; //循环
}
return null; // 未找到
} // end delete()
//-------------------------------------------------------------
public DataItem find(int key) // 表中是否存在该关键字的数据项
{
int hashVal = hashFunc(key);
while(hashArray[hashVal] != null)
{
if(hashArray[hashVal].getKey() == key)
return hashArray[hashVal];
++hashVal;
hashVal %= arraySize;
}
return null;
}
//-------------------------------------------------------------
} // end class HashTable
////////////////////////////////////////////////////////////////
package hash;
import java.io.*;
class HashTableApp
{
public static void main(String[] args) throws IOException
{
DataItem aDataItem;
int aKey, size, n, keysPerCell;
System.out.print("Enter size of hash table: ");
size = getInt();//从控制台获取一个整数做为哈希表的大小
System.out.print("Enter initial number of items: ");
n = getInt(); //随机生成n个数做为数据存入哈希表
keysPerCell = 10;//随机生成函数的因子
HashTable theHashTable = new HashTable(size);//初始化
for(int j=0; j<n; j++) // 生成并插入
{
aKey = (int)(java.lang.Math.random() *
keysPerCell * size);
aDataItem = new DataItem(aKey);//封装为哈希表中的数据格式
theHashTable.insert(aDataItem);//插入
}
while(true)
{
System.out.print("Enter first letter of ");
System.out.print("show, insert, delete, or find: ");
char choice = getChar();
switch(choice)
{
case 's':
theHashTable.displayTable();
break;
case 'i':
System.out.print("Enter key value to insert: ");
aKey = getInt();
aDataItem = new DataItem(aKey);
theHashTable.insert(aDataItem);
break;
case 'd':
System.out.print("Enter key value to delete: ");
aKey = getInt();
theHashTable.delete(aKey);
break;
case 'f':
System.out.print("Enter key value to find: ");
aKey = getInt();
aDataItem = theHashTable.find(aKey);
if(aDataItem != null)
{
System.out.println("Found " + aKey);
}
else
System.out.println("Could not find " + aKey);
break;
default:
System.out.print("Invalid entry\n");
} // end switch
} // end while
} // end main()
//--------------------------------------------------------------
public static String getString() throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
String s = br.readLine();
return s;
}
//--------------------------------------------------------------
public static char getChar() throws IOException
{
String s = getString();
return s.charAt(0);
}
//-------------------------------------------------------------
public static int getInt() throws IOException
{
String s = getString();
return Integer.parseInt(s);
}
//--------------------------------------------------------------
} // end class HashTableApp
////////////////////////////////////////////////////////////////
二、二次探测法
线性探测法会发生集聚现象,即冲突数据项会集聚在一块儿,缘由是查找空数据项是一步一步移动的。
二次探测法是为了防止集聚产生的一种尝试方法,思想是探测间隔较远的单元,而不是临近的单元。具体方法是把步长设为探测次数的平方,好比第1次探测步长为1,第2次为4,第3次为9以此类推。
可是二次探测法会产生二次集聚。一般不采用该方法,由于有更好的解决方案。三、再哈希法
方法是对冲突的关键字用另外一个哈希函数计算其值,把结果做为搜索时的步长。这就使得不一样的关键字步长不一样,避免了集聚现象。
第二个哈希函数必须具有如下条件:
(1)与第一个哈希函数不一样
(2)不能得出结果为0,不然步长为0.
一般第二个哈希函数采用以下函数:
step=constant-(key%contant)
constant是一个质数且小于数组容量,key是关键字。step范围在1-constant之间。
再哈希法要求表的容量是一个质数,这是为了使查找操做能够遍历整个表。不然假设表的容量为15,不是一个质数。而查找初始位置为4,查找步长为5,那么每次查找都是固定的三个数,即下标为9,14,4对应的数据。设为质数能够避免这种状况。
再哈希法的java实现
package hashDouble;
class DataItem
{
private int iData; // 关键字
//--------------------------------------------------------------
public DataItem(int ii) // 构造器
{ iData = ii; }
//--------------------------------------------------------------
public int getKey() //获取关键字
{ return iData; }
//--------------------------------------------------------------
} // end class DataItem
////////////////////////////////////////////////////////////////
package hashDouble;
class HashTable
{
private DataItem[] hashArray;
private int arraySize;
private DataItem nonItem;
//-------------------------------------------------------------
HashTable(int size) // 构造器
{
arraySize = size;
hashArray = new DataItem[arraySize];
nonItem = new DataItem(-1);
}
//-------------------------------------------------------------
public void displayTable()
{
System.out.print("Table: ");
for(int j=0; j<arraySize; j++)
{
if(hashArray[j] != null)
System.out.print(hashArray[j].getKey()+ " ");
else
System.out.print("** ");
}
System.out.println("");
}
//-------------------------------------------------------------
public int hashFunc1(int key)
{
return key % arraySize;
}
//-------------------------------------------------------------
public int hashFunc2(int key) //再哈希
{
return 5 - key % 5;
}
//-------------------------------------------------------------
public void insert(int key, DataItem item)
// 假设表未满
{
int hashVal = hashFunc1(key); // 计算哈希值
int stepSize = hashFunc2(key); // 计算步长
while(hashArray[hashVal] != null &&
hashArray[hashVal].getKey() != -1)//非空且数据未删除
{
hashVal += stepSize; // 加步长
hashVal %= arraySize; // 循环到表头
}
hashArray[hashVal] = item; // 插入
} // end insert()
//-------------------------------------------------------------
public DataItem delete(int key) // 删除
{
int hashVal = hashFunc1(key); //计算哈希值
int stepSize = hashFunc2(key); // 计算步长
while(hashArray[hashVal] != null) // 非空
{
if(hashArray[hashVal].getKey() == key)//找到
{
DataItem temp = hashArray[hashVal];
hashArray[hashVal] = nonItem;
return temp;
}
hashVal += stepSize;
hashVal %= arraySize;
}
return null; // 没法找到
} // end delete()
//-------------------------------------------------------------
public DataItem find(int key) // 查找
// 假设表未满
{
int hashVal = hashFunc1(key);
int stepSize = hashFunc2(key);
while(hashArray[hashVal] != null) // 非空
{
if(hashArray[hashVal].getKey() == key)
return hashArray[hashVal]; // 找到返回
hashVal += stepSize; // 加步长
hashVal %= arraySize;
}
return null; // can't find item
}
//-------------------------------------------------------------
} // end class HashTable
////////////////////////////////////////////////////////////////
package hashDouble;
import java.io.*;
class HashDoubleApp
{
public static void main(String[] args) throws IOException
{
int aKey;
DataItem aDataItem;
int size, n;
System.out.print("Enter size of hash table: ");
size = getInt();
System.out.print("Enter initial number of items: ");
n = getInt();
HashTable theHashTable = new HashTable(size);
for(int j=0; j<n; j++)
{
aKey = (int)(java.lang.Math.random() * 2 * size);
aDataItem = new DataItem(aKey);
theHashTable.insert(aKey, aDataItem);
}
while(true)
{
System.out.print("Enter first letter of ");
System.out.print("show, insert, delete, or find: ");
char choice = getChar();
switch(choice)
{
case 's':
theHashTable.displayTable();
break;
case 'i':
System.out.print("Enter key value to insert: ");
aKey = getInt();
aDataItem = new DataItem(aKey);
theHashTable.insert(aKey, aDataItem);
break;
case 'd':
System.out.print("Enter key value to delete: ");
aKey = getInt();
theHashTable.delete(aKey);
break;
case 'f':
System.out.print("Enter key value to find: ");
aKey = getInt();
aDataItem = theHashTable.find(aKey);
if(aDataItem != null)
System.out.println("Found " + aKey);
else
System.out.println("Could not find " + aKey);
break;
default:
System.out.print("Invalid entry\n");
} // end switch
} // end while
} // end main()
//--------------------------------------------------------------
public static String getString() throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
String s = br.readLine();
return s;
}
//--------------------------------------------------------------
public static char getChar() throws IOException
{
String s = getString();
return s.charAt(0);
}
//-------------------------------------------------------------
public static int getInt() throws IOException
{
String s = getString();
return Integer.parseInt(s);
}
//--------------------------------------------------------------
} // end class HashDoubleApp
////////////////////////////////////////////////////////////////
由于链地址法采用链表做为表中的数据格式,因此容许存储相同的关键字数据。并且咱们能够把链设为有序链表。
Link类定义了链表结构
package hashChain;
class Link //定义链表
{ // 能够动态存储数据,扩充容量
private int iData; // 关键字
public Link next; // 连接到下一个
//-------------------------------------------------------------
public Link(int it) //构造器
{ iData= it; }
//-------------------------------------------------------------
public int getKey() //获取关键字
{ return iData; }
//-------------------------------------------------------------
public void displayLink() // 显示
{ System.out.print(iData + " "); }
} // end class Link
////////////////////////////////////////////////////////////////
package hashChain;
class SortedList //有序链表
{
private Link first; // 链表头
//-------------------------------------------------------------
public void SortedList() // 构造器
{ first = null; }
//-------------------------------------------------------------
public void insert(Link theLink) // 插入并有序
{
int key = theLink.getKey();
Link previous = null; // 前一个数据项
Link current = first;
while( current != null && key > current.getKey() )//非空且关键字大于当前数据关键字
{
previous = current; //继续查找下一个
current = current.next;
}
if(previous==null) // 若是表为空
first = theLink; // 表头指向该数据项
else // 表非空
previous.next = theLink; // key<current.getKey()时,数据项应插入previous后,previous-->theLink
theLink.next = current; // theLink --> current
} // end insert()
//-------------------------------------------------------------
public void delete(int key) //删除
{
Link previous = null;
Link current = first;
while( current != null && key != current.getKey() )//未找到
{
previous = current;
current = current.next; // 查找下一个
}
if(previous==null) // 要删除数据项为表头
first = first.next; // 删除表头
else // 不是表头
previous.next = current.next; // 删除current
} // end delete()
//-------------------------------------------------------------
public Link find(int key) // 查找
{
Link current = first;
while(current != null && current.getKey() <= key)
{
if(current.getKey() == key) // 找到
return current; // 返回
current = current.next; //未找到继续查找下一个
}
return null; //未找到
} // end find()
//-------------------------------------------------------------
public void displayList()//显示
{
System.out.print("List (first-->last): ");
Link current = first;
while(current != null)
{
current.displayLink();
current = current.next;
}
System.out.println("");
}
} // end class SortedList
////////////////////////////////////////////////////////////////
package hashChain;
class HashTable
{
private SortedList[] hashArray;
private int arraySize;
//-------------------------------------------------------------
public HashTable(int size) //构造器
{
arraySize = size;
hashArray = new SortedList[arraySize]; // 初始化数组,数组中存储的是链表
for(int j=0; j<arraySize; j++) // 初始化每一个数组元素
hashArray[j] = new SortedList();
}
//-------------------------------------------------------------
public void displayTable()
{
for(int j=0; j<arraySize; j++)
{
System.out.print(j + ". ");
hashArray[j].displayList();
}
}
//-------------------------------------------------------------
public int hashFunc(int key) // 计算哈希值
{
return key % arraySize;
}
//-------------------------------------------------------------
public void insert(Link theLink) // 插入数据
{
int key = theLink.getKey(); //获取关键字
int hashVal = hashFunc(key); // 计算关键字哈希值
hashArray[hashVal].insert(theLink); // 插入哈希表中对应的位置
} // end insert()
//-------------------------------------------------------------
public void delete(int key) // 根据关键字删除数据
{
int hashVal = hashFunc(key); // 计算关键字哈希值
hashArray[hashVal].delete(key); // 删除哈希表中对应数据
} // end delete()
//-------------------------------------------------------------
public Link find(int key) // 查找
{
int hashVal = hashFunc(key);
Link theLink = hashArray[hashVal].find(key);
return theLink;
}
//-------------------------------------------------------------
} // end class HashTable
////////////////////////////////////////////////////////////////
主函数package hashChain;
import java.io.*;
class HashChainApp
{
public static void main(String[] args) throws IOException
{
int aKey;
Link aDataItem;
int size, n, keysPerCell = 100;
System.out.print("Enter size of hash table: ");
size = getInt();
System.out.print("Enter initial number of items: ");
n = getInt();
HashTable theHashTable = new HashTable(size);
for(int j=0; j<n; j++)
{
aKey = (int)(java.lang.Math.random() *
keysPerCell * size);
aDataItem = new Link(aKey);
theHashTable.insert(aDataItem);
}
while(true)
{
System.out.print("Enter first letter of ");
System.out.print("show, insert, delete, or find: ");
char choice = getChar();
switch(choice)
{
case 's':
theHashTable.displayTable();
break;
case 'i':
System.out.print("Enter key value to insert: ");
aKey = getInt();
aDataItem = new Link(aKey);
theHashTable.insert(aDataItem);
break;
case 'd':
System.out.print("Enter key value to delete: ");
aKey = getInt();
theHashTable.delete(aKey);
break;
case 'f':
System.out.print("Enter key value to find: ");
aKey = getInt();
aDataItem = theHashTable.find(aKey);
if(aDataItem != null)
System.out.println("Found " + aKey);
else
System.out.println("Could not find " + aKey);
break;
default:
System.out.print("Invalid entry\n");
} // end switch
} // end while
} // end main()
//--------------------------------------------------------------
public static String getString() throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
String s = br.readLine();
return s;
}
//-------------------------------------------------------------
public static char getChar() throws IOException
{
String s = getString();
return s.charAt(0);
}
//-------------------------------------------------------------
public static int getInt() throws IOException
{
String s = getString();
return Integer.parseInt(s);
}
//--------------------------------------------------------------
} // end class HashChainApp
////////////////////////////////////////////////////////////////
一、不使用无用数据项
关键字的选取时,要提出原始数据中的无用数据项,例如起始位、校验位、结束位等,由于这些数据位没有携带信息。
二、使用全部的有用数据位
全部的有用数据位在哈希函数中都应当有体现。不要使用前四位或者后五位等其余方法。
三、使用质数做为取模运算的基数。
若关键字彻底随机分布,质数和非质数的表现差很少。可是当关键字不是随机分布时,就应该使用质数做为哈希表的大小。使用质数能够是关键字较为平均的映射到哈希表的各个位置。
7、开放地址法和链地址法的比较
开放地址法在表快满时,性能有明显降低,且对哈希表进行扩展时操做复杂。链地址法须要设计链表类,可是不会随着数据项的增多致使性能快速降低,并且能够动态扩展哈希表。
参考文献: java数据结构与算法(第二版)