哈希表简介
哈希表通过把保存的数据项本身映射到数组的某个下标来加快查找速度。数组、链表等数据结构查找某个数据项,通常要遍历整个数据结构,也就是O(N)的时间级。但是对于哈希表来说,查找只是O(1)的时间级。
我们使用哈希函数来实现数据项到数组下标的映射:
arrayIndex = largeNumber % smallRange
哈希函数可以把一个大范围的数字压缩成一个小范围的数字(hash化),这个小范围的数对应着该元素在hash表中存储的下标。
冲突
把巨大的数字范围压缩到较小的数字范围,那么肯定会有几个不同的元素哈希化到同一个数组下标,即产生了冲突。冲突可能会导致哈希化方案无法实施,有两种方法消除冲突,分别是开放地址法和链地址法。
装填因子
已填入哈希表的数据项和表长的比率叫装填因子,比如有1000个单元的哈希表填入了6667个数据后,其装填因子为 0.6667。装填因子越大,产生冲突的可能性就越大。
开放地址法
我们一般指定的数组范围大小是实际存储数据的两倍,因此可能有一半的空间是空着的,所以,当冲突产生时,一个方法是在数组中找到另一个空位,并把这个元素填入,这种方法称为开放地址法。
若数据项不能直接存放在由哈希函数所计算出来的数组下标时,就要寻找其他的位置。有三种寻找其他位置的方法:线性探测、二次探测以及再哈希法。
需要注意的是,当哈希表变得满时,我们需要扩展数组,但是,数据项不能放到新数组中和老数组相同的位置,而是要根据数组大小重新计算插入位置。这是一个比较耗时的过程,所以一般我们要确定数据的范围,给定好数组的大小,尽量避免扩容。
另外,当哈希表变得比较满时,我们每插入一个新的数据,都要频繁的探测插入位置,因为可能很多位置都被前面插入的数据所占用了,这称为聚集。数组填的越满,聚集越可能发生。
线性探测
在线性探测中,它会以元素哈希化得到的数字作为开始下标,在数组中一步一步地向后查找空白单元。
线性探测的缺点:
使用线性探测的方法,当装填因子不断变大,聚集分布也越来越连贯,哈希表某部分可能集聚了大量数据项,而另外某些部分还可能很稀疏。一旦形成了集聚,集聚就会越来越大,凡是哈希化后的值落在集聚的范围都要一步一步移动,并最后插入集聚的后面。所以集聚越大就越影响哈希表的性能。
二次探测
二测探测是防止聚集产生的一种方式,思想是探测相距较远的单元,而不是和原始位置相邻的单元。在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。
二次探测虽然消除了原始的聚集问题,但是产生了另一种聚集问题,叫二次聚集:比如302,420和544依次插入表中,它们的映射都是7,那么302需要以1为步长探测,420需要以4为步长探测,544需要以9为步长探测。只要有一项其关键字映射到7,就需要更长步长的探测,这个现象叫做二次聚集。二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有好的解决方法,比如再哈希法。
再哈希法
我们知道二次聚集的原因是二测探测的算法产生的探测序列步长总是固定的:1,4,9,16以此类推。
我们想到的是需要产生一种依赖关键字的探测序列,而不是每个关键字都一样,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。
如何实现不同的探测序列呢?方法是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为探测的步长。
第二个哈希函数必须具备如下特点:
- 和第一个哈希函数不同。
- 不能输出零(否则没有步长,每次探测都是原地踏步,算法将陷入死循环)。
stepSize = constant - key % constant。其中constant是质数,且小于数组容量。 - 再哈希法要求表的容量是一个质数。这样可以保证探测序列会访问到所有单元。
假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是0,5,10,0,5,10,以此类推一直循环下去。
链地址法
链地址法在哈希表每个单元中设置链表,某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。
链地址法中,装填因子(数据项数和哈希表容量的比值)与开放地址法不同,在链地址法中,装填因子一般为1,或比1大(有可能某些位置包含的链表中包含两个或两个以上的数据项)。
找到初始单元需要O(1)的时间级别,而搜索链表的时间与M成正比,M为链表包含的平均项数,即O(M)的时间级别。
Java实现哈希表
线性探测
package hashtable;
public class MyHashTableList {
public static void main(String[] args) {
MyHashTable myHashTable = new MyHashTable(4);
myHashTable.insert(new DataItem(12));
myHashTable.insert(new DataItem(200));
myHashTable.insert(new DataItem(120));
myHashTable.insert(new DataItem(30));
myHashTable.insert(new DataItem(50));
myHashTable.display();
DataItem item = myHashTable.find(12);
System.out.println();
System.out.println(item.getData());
myHashTable.delete(12);
myHashTable.display();
}
}
//定义在hash表中保存的数据项对应的类
class DataItem{
private int data; //数据项中的值
//还可以在这里封装其他属性
//构造方法初始化它
public DataItem(int data){
this.data = data;
}
public int getData() {
return data;
}
}
class MyHashTable{
//声明hashtable中的底层属性
private DataItem[] hashArray;//保存在hash表中的数据项
private int arraySize;//保存上面数组的长度
private int itemNum;//保存上面的数据中真正含有的数据项的个数
private DataItem delItem;//表示hash表中被删除的数据项
//构造方法
public MyHashTable(){} //习惯性加上无参构造
public MyHashTable(int arraySize){
this.arraySize = arraySize;
hashArray = new DataItem[arraySize];
itemNum = 0;
delItem = new DataItem(-1);//规定被删除的数据项里面的值就是-1
}
//判断数组是不是满的
public boolean isFull(){
return itemNum == arraySize;
}
//判断数组是不是空的
public boolean isEmpty(){
return itemNum == 0;
}
//实现hash表中的数据项的值打印输出
public void display(){
System.out.println("hashTable: ");
for (int i=0;i<arraySize;i++){
if (hashArray[i] != null){
System.out.print(hashArray[i].getData() + " ");
}else {
System.out.print("** ");
}
}
}
//实现哈希函数
public int hashFunction(int key){
return key%arraySize; // %为取余
}
//向hash表插入新的数据项
public void insert(DataItem item){
//如果hash表已经满了,要对hash表进行扩容
if (isFull()){
System.out.println("hash表满了,现在进行扩容...");
extendHashTable();
}
int data = item.getData();//把封装在数据项中的数值取出来
//对data进行hash化
int hashval = hashFunction(data); //使用这个hash值来做为保存数值的index
while (hashArray[hashval] != null && hashArray[hashval].getData() != -1){
//该位置已经被占用
//进行线性探索,查找相邻的空位
hashval++;
hashval = hashval%arraySize; //hash值+1后,再进行hash化
}
//while结束后,表示找到了空位置
hashArray[hashval] = item;
itemNum++;
}
//扩容hash表内部的数组的功能
public void extendHashTable(){
//扩容时需要注意,不能把老数组中的数据项直接复制到新的更大的数组的相同的位置
int num = arraySize;//把老数组的长度暂时保存起来
itemNum = 0; //重新计数
arraySize = arraySize*2;//把老数组的长度扩大为原来的2倍
DataItem[] oldDataItemArr = hashArray;//把老数组的数据也临时保存起来
hashArray = new DataItem[arraySize];
//把老数组中的数据插入新数组
for (int i=0;i<num;i++){
//因为老数组满了之后才进行扩展,所以不用判断数据是否为空。不可能为空
insert(oldDataItemArr[i]);
}
}
//删除数据项
public DataItem delete(int data){
//判断hash表是不是空的
if (isEmpty()){
//如果是空的,删除就没有意义
System.out.println("hash表是空的");
return null;
}else {
//删除之前,先找到要删除的目标
int hashval = hashFunction(data);
while (hashArray[hashval] != null){
if (hashArray[hashval].getData() == data){
//找到了要删除的对象
DataItem tmp = hashArray[hashval];
hashArray[hashval] = delItem; //这个就是真正意义上的删除动作
itemNum --;
return tmp;
} else {
hashval++;
hashval = hashval%arraySize;
}
}
//如果hash表中没有要删除的值
return null;
}
}
//实现在hash表中查找指定的数据项
public DataItem find(int data){
int hashval = hashFunction(data);//把data进行hash化
while (hashArray[hashval] != null){
if (hashArray[hashval].getData() == data){
//找到了
return hashArray[hashval];
}else {
hashval++;
hashval = hashval % arraySize;
}
}
return null;//hash表中没有要找的目标
}
}
再哈希法
package hashtable;
public class HashTableDoubleTest {
public static void main(String[] args) {
HashTableDouble myHashTable = new HashTableDouble(6);
myHashTable.insert(new DataItem(12));
myHashTable.insert(new DataItem(200));
myHashTable.insert(new DataItem(120));
myHashTable.insert(new DataItem(30));
myHashTable.insert(new DataItem(50));
myHashTable.insert(new DataItem(40));
myHashTable.insert(new DataItem(9));
myHashTable.display();
myHashTable.insert(new DataItem(7));
myHashTable.display();
DataItem item = myHashTable.find(12);
System.out.println();
System.out.println(item.getData());
myHashTable.delete(12);
myHashTable.display();
}
}
class HashTableDouble{
//声明hashtable中的底层属性
private DataItem[] hashArray;//保存在hash表中的数据项
private int arraySize;//保存上面数组的长度
private int itemNum;//保存上面的数据中真正含有的数据项的个数
private DataItem delItem;//表示hash表中被删除的数据项
//构造方法做初始化
public HashTableDouble(){}
public HashTableDouble(int size){
//再hash法要求size必须是一个质数
while (true){//确保arraySize是一个质数
if (isPrime(size)){
//是一个质数
arraySize = size;
break;
}else {
size++;
}
}
hashArray = new DataItem[arraySize];
itemNum = 0;
delItem = new DataItem(-1);//规定被删除的数据项里面的值就是-1
}
//判断是不是质数
private boolean isPrime(int n){
if (n%2 ==0) return false;
for (int i=3;i<Math.sqrt(n);i++){//从3到平方根
if (n%i ==0) return false;
}
return true;
}
//判断数组是不是满的
public boolean isFull(){
return itemNum == arraySize;
}
//判断数组是不是空的
public boolean isEmpty(){
return itemNum == 0;
}
//实现hash表中的数据项的值打印输出
public void display(){
System.out.println("hashTable: ");
for (int i=0;i<arraySize;i++){
if (hashArray[i] != null){
System.out.print(hashArray[i].getData() + " ");
}else {
System.out.print("** ");
}
}
System.out.println();
}
//实现第一个哈希函数,将数据项hash化为数组的下标
public int hashFunction1(int key){
return key%arraySize; // %为取余
}
//第二个hash函数,将数据项hash化为探测步长
public int hashFunction2(int key){
return 5-key%5;//在这里我们使用质数5
}
//向hash表插入新的数据项
public void insert(DataItem item){
//如果hash表已经满了,要对hash表进行扩容
if (isFull()){
System.out.println("hash表满了,现在进行扩容...");
extendHashTable();
}
int data = item.getData();//把封装在数据项中的数值取出来
//对data进行hash化
int hashval = hashFunction1(data); //使用这个hash值来做为保存数值的index
int hashStep = hashFunction2(data);
while (hashArray[hashval] != null && hashArray[hashval].getData() != -1){
//该位置已经被占用
//进行线性探索,查找相邻的空位
hashval+= hashStep;
hashval = hashval%arraySize; //hash值+1后,再进行hash化
}
//while结束后,表示找到了空位置
hashArray[hashval] = item;
itemNum++;
}
//扩容hash表内部的数组的功能
public void extendHashTable(){
//扩容时需要注意,不能把老数组中的数据项直接复制到新的更大的数组的相同的位置
int num = arraySize;//把老数组的长度暂时保存起来
itemNum = 0; //重新计数
arraySize = arraySize*2;//把老数组的长度扩大为原来的2倍
while (true){//确保arraySize是一个质数
if (isPrime(arraySize)){
//是一个质数
break;
}else {
arraySize++;
}
}
DataItem[] oldDataItemArr = hashArray;//把老数组的数据也临时保存起来
hashArray = new DataItem[arraySize];
//把老数组中的数据插入新数组
for (int i=0;i<num;i++){
//因为老数组满了之后才进行扩展,所以不用判断数据是否为空。不可能为空
insert(oldDataItemArr[i]);
}
}
//删除数据项
public DataItem delete(int data){
//判断hash表是不是空的
if (isEmpty()){
//如果是空的,删除就没有意义
System.out.println("hash表是空的");
return null;
}else {
//删除之前,先找到要删除的目标
int hashval = hashFunction1(data);
int hashStep = hashFunction2(data);
while (hashArray[hashval] != null){
if (hashArray[hashval].getData() == data){
//找到了要删除的对象
DataItem tmp = hashArray[hashval];
hashArray[hashval] = delItem; //这个就是真正意义上的删除动作
itemNum --;
return tmp;
} else {
hashval += hashStep;
hashval = hashval%arraySize;
}
}
//如果hash表中没有要删除的值
return null;
}
}
//实现在hash表中查找指定的数据项
public DataItem find(int data){
int hashval = hashFunction1(data);//把data进行hash化
int hashStep = hashFunction2(data);
while (hashArray[hashval] != null){
if (hashArray[hashval].getData() == data){
//找到了
return hashArray[hashval];
}else {
hashval += hashStep;
hashval = hashval % arraySize;
}
}
return null;//hash表中没有要找的目标
}
}
链地址法
package hashtable;
public class HashTableChainTest {
public static void main(String[] args) {
HashTableChain myHashTable = new HashTableChain(6);
myHashTable.insert(12);
myHashTable.insert(20);
myHashTable.insert(121);
myHashTable.insert(16);
myHashTable.insert(27);
myHashTable.insert(18);
myHashTable.insert(39);
myHashTable.displayHashTable();
myHashTable.delete(39);
myHashTable.displayHashTable();
System.out.println(myHashTable.find(16));
}
}
/**
* 链表节点结构
*/
class Node{
public int data; //封装在节点里的数据
public Node next; //指针,指向下一个节点
//构造方法
public Node(int data){
this.data = data;
}
@Override
public String toString() {
return "Node [data=" + data + ", next=" + next + "]";
}
}
/**
* 链表
*/
class OrderLinkedList{
private Node head;
private int size;
//构造方法
public OrderLinkedList(){
head = null;
size = 0;
}
//有序链表插入操作
public void insert(int data){
Node newNode = new Node(data);
Node previous = null;
Node current = head;
while (current!=null && data>current.data){
previous = current;
current = current.next;
}
if(previous == null){
//要插入的值比所有值都小
head = newNode;
head.next = current;
}else{
newNode.next = current;
previous.next = newNode;
}
size ++;
}
//判断链表是不是空链表
public boolean isEmpty(){
return size==0;
}
//链表删除头结点,默认链表中是有节点的
public int deleteHead(){
int obj = head.data;
head = head.next;
size --;
return obj;
}
//遍历输出所有node信息
public void display(){
if (size>0){
//不是空的
Node current = head;
int tmpSize = size;
if (tmpSize == 1){
System.out.println("[" + head.data + "]");
return; // 结束掉方法
}
while (tmpSize>0){
if (current == head){
System.out.print("[" + current.data + "->");
}else if(current.next == null){
//最后一个节点
System.out.print(current.data + "]");
}else {
System.out.print(current.data + "->");
}
tmpSize --;
current = current.next;
}
System.out.println();//输出一个换行符
}else {
//空列表
System.out.println("[]");
}
}
//查找节点
public Node find(int data) {
Node current = head;
while (current!=null && current.data<=data){
if (current.data == data){
//找到了
return current;
}else {
current = current.next;
}
}
return null;//表示链表中没有要找的数据
}
//删除指定节点
public void delete(int data){
Node previous = null;
Node current = head;
while (current!=null && current.data!=data){
previous = current;
current = current.next;
}
if (previous==null && current != null){
//找到的节点为头节点
head = head.next;
size--;
}else if(current!=null && previous != null) {
//找到的节点不是头结点
previous.next = current.next;
size--;
}else {
System.out.println("没找到指定数据");
}
}
}
/**
* hash表
*/
class HashTableChain{
private OrderLinkedList[] hashArray;
private int arraySize;
public HashTableChain(int size){
this.arraySize = size;
hashArray = new OrderLinkedList[size];
//对hashArray数组中的元素初始化进入一个空的链表
for (int i=0;i<arraySize;i++){
hashArray[i] = new OrderLinkedList();
}
}
//hash表的显示
public void displayHashTable(){
for (int i=0;i<arraySize;i++){
System.out.println("第"+i+"个数组元素项:");
hashArray[i].display();
}
}
//hash函数
public int hashFunction(int data){
return data%arraySize;
}
//实现hash表插入
public void insert(int data){
int hashval = hashFunction(data);
hashArray[hashval].insert(data);
}
//实现删除
public void delete(int data){
int hashval = hashFunction(data);
hashArray[hashval].delete(data);
}
//实现查找
public Node find(int data){
int hashval = hashFunction(data);
return hashArray[hashval].find(data);
}
}