前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据结构是哈希表(hashTable)(一)

数据结构是哈希表(hashTable)(一)

作者头像
Java帮帮
发布2018-03-16 15:25:59
6640
发布2018-03-16 15:25:59
举报
哈希表也

哈希化之后难免会产生一个问题,那就是对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?一种方法是开放地址法,即通过系统的方法找到数组的另一个空位,把数据填入,而不再用哈希函数得到的数组下标,因为该位置已经有数据了;另一种方法是创建一个存放链表的数组,数组内不直接存储数据,这样当发生冲突时,新的数据项直接接到这个数组下标所指的链表中,这种方法叫做链地址法。下面针对这两种方法进行讨论。

1.开放地址法

线性探测法

所谓线性探测,即线性地查找空白单元。如果21是要插入数据的位置,但是它已经被占用了,那么就是用22,然后23,以此类推。数组下标一直递增,直到找到空白位。下面是基于线性探测法的哈希表实现代码:

public class HashTable {  
 
    private DataItem[] hashArray; // DateItem类是数据项,封装数据信息  
    private int arraySize=10;  
    private int itemNum; // 数组中目前存储了多少项  
    private DataItem nonItem; // 用于删除项的  
 
    public HashTable() {  
 hashArray = new DataItem[arraySize];  
 nonItem = new DataItem(-1); // deleted item key is -1  
    }  
 
    public boolean isFull() {  
        return (itemNum == arraySize);  
    }  
 
    public boolean isEmpty() {  
        return (itemNum == 0);  
    }  
 
    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 hashFunction(int key) {  
        return key % arraySize; // hash function  
    }  
 
    public void insert(DataItem item) {  
        if (isFull()) {  
            // 扩展哈希表  
            System.out.println("哈希表已满,重新哈希化..");  
            extendHashTable();  
        }  
        int key = item.getKey();  
        int hashVal = hashFunction(key);  
        while (hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) {  
            ++hashVal;  
            hashVal %= arraySize;  
        }  
        hashArray[hashVal] = item;  
        itemNum++;  
    }  
 
    /*  
     * 数组有固定的大小,而且不能扩展,所以扩展哈希表只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。  
     * 但是哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上,因此不能直接拷贝,需要按顺序遍历老数组,  
     * 并使用insert方法向新数组中插入每个数据项。这叫重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程是必须的。  
     */  
    public void extendHashTable() { // 扩展哈希表  
        int num = arraySize;  
 itemNum = 0; // 重新记数,因为下面要把原来的数据转移到新的扩张的数组中  
        arraySize *= 2; // 数组大小翻倍  
        DataItem[] oldHashArray = hashArray;  
 hashArray = new DataItem[arraySize];  
        for (int i = 0; i < num; i++) {  
            insert(oldHashArray[i]);  
        }  
    }  
 
    public DataItem delete(int key) {  
        if (isEmpty()) {  
            System.out.println("Hash table is empty!");  
            return null;  
        }  
        int hashVal = hashFunction(key);  
        while (hashArray[hashVal] != null) {  
            if (hashArray[hashVal].getKey() == key) {  
                DataItem temp = hashArray[hashVal];  
                hashArray[hashVal] = nonItem; // nonItem表示空Item,其key为-1  
                itemNum--;  
                return temp;  
            }  
            ++hashVal;  
            hashVal %= arraySize;  
        }  
        return null;  
    }  
 
    public DataItem find(int key) {  
        int hashVal = hashFunction(key);  
        while (hashArray[hashVal] != null) {  
            if (hashArray[hashVal].getKey() == key) {  
                return hashArray[hashVal];  
            }  
            ++hashVal;  
            hashVal %= arraySize;  
        }  
        return null;  
    }  
}  
 
class DataItem {  
    private int iData;  
 
    public DataItem(int data) {  
 iData = data;  
    }  
 
    public int getKey() {  
        return iData;  
    }  
}  

线性探测有个弊端,即数据可能会发生聚集。一旦聚集形成,它会变得越来越大,那些哈希化后落在聚集范围内的数据项,都要一步步的移动,并且插在聚集的最后,因此使聚集变得更大。聚集越大,它增长的也越快。这就导致了哈希表的某个部分包含大量的聚集,而另一部分很稀疏。

为了解决这个问题,我们可以使用二次探测:二次探测是防止聚集产生的一种方式,思想是探测相隔较远的单元,而不是和原始位置相邻的单元。线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。二次探测虽然消除了原始的聚集问题,但是产生了另一种更细的聚集问题,叫二次聚集:比如讲184,302,420和544依次插入表中,它们的映射都是7,那么302需要以1为步长探测,420需要以4为步长探测, 544需要以9为步长探测。只要有一项其关键字映射到7,就需要更长步长的探测,这个现象叫做二次聚集。二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有好的解决方法,比如再哈希法。

再哈希法

为了消除原始聚集和二次聚集,现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样。即:不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。再哈希法就是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同关键字使用不同的步长、经验说明,第二个哈希函数必须具备如下特点:

1. 和第一个哈希函数不同;

2. 不能输出0(否则没有步长,每次探索都是原地踏步,算法将进入死循环)。

专家们已经发现下面形式的哈希函数工作的非常好:stepSize = constant - key % constant; 其中constant是质数,且小于数组容量。

再哈希法要求表的容量是一个质数,假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是0,5,10,0,5,10,以此类推一直循环下去。算法只尝试这三个单元,所以不可能找到某些空白单元,最终算法导致崩溃。如果数组容量为13, 质数,探测序列最终会访问所有单元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一个空位,就可以探测到它。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java帮帮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.开放地址法
    • 线性探测法
      • 再哈希法
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档