java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java哈希表实现

Java哈希表的概念及实现完整代码

作者:小川_wenxun

这篇文章主要介绍了Java哈希表的概念及实现的相关资料,哈希表是一种高效查找数据的结构,通过哈希函数将关键字映射到数组的索引位置,当发生冲突时,可以通过闭散列或开散列(链地址法)来解决,需要的朋友可以参考下

哈希表

概念

哈希表是一种理想的从顺序表以及平衡树中查找元素的方式,它可以不经过任何比较,一次直接从表中得到想要搜索的元素。如果构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

该种方法即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表

例如:数据集合{1,7,6,4,5,9}; 哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

上面这种存放元素的方式,不用多次进行关键码的比较,搜索速度比较快,但是上面所取的集合只是一个普通情况,如果集合里再添加一个 14 那么,14应该放在哪里那?

这里便是要提到冲突。

冲突

对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

对于冲突,我们要认识到:

由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一 个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。哈希函数设计原则:

常见的哈希函数有:

其他的方法还有:平方取中法、折叠法、随机数法、数学分析法等,感兴趣的话可以了解一下。

负载因子调节

 产生冲突的概率叫做冲突率,已知哈希表中已有的关键字个数是不变的,那么我们能调整的就只有哈希表中的数组的大小。

Java中负载因子的值为0.75,即当 填入表中的元素个数 / 散列表的长度 > 0.75时。产生冲突的概率会很大,这时候我们就要来解决冲突。

冲突的解决

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列:当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把key存放到冲突位置中的“下一个” 空位置中去。

开散列:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子 集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

这里主要用的是通过开散列(哈希桶)来解决冲突

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

哈希桶的实现

从上图哈希桶图所示,我们可以把它看成是数组+链表的形式,这样我们就可以定义相关变量了。

//定义相关变量
static class Node{
    public int val;
    public int key;
    public Node next;
    
    public Node(int val, int key){
        this.val = val;
        this.key = key;
    }
    
    public Node[] elem = new Node[10];
    public int useSize;
}

插入数据

插入数据的第一步是要在数组中找到它所在的位置,然后进行链表的插入,头插法 

public void  push(int key, int val){
    int index = key % array.length;
    Node cur = array[index];
    while(cur != null){
        if(cur.key == key){
            cur.val = val;
            return;
        }
        cur = cur.next;
    }
    //没有找到当前链表中有这个key的节点
    //头插法
    Node node = new Node(val, key);
    node.next = array[index];
    array[index] = node;
    useSize++;
}

不过,这里有个重点,要注意负载因子,计算负载因子。

以代码的数据为例,如果数组中的所放数据个数大于7,那么就会有很大概率产生冲突,这时我们要解决冲突,就要对哈希表进行扩容,这里并不是简单地把数组扩大两倍,在扩大后还要把前面整个数组的数据遍历一遍,然后再次进行对应位置的存储。

像是没扩容之前,array[4] 中可能放着 4和14两个数据,现在数组长度从10扩容到20,那么4还应该放在array[4]里面,而14应该放在array[14]里面。

故要包括扩容以及再次哈希来进行

插入完整代码如下

public void  push(int key, int val){
    int index = key % array.length;
    Node cur = array[index];
    while(cur != null){
        if(cur.key == key){
            cur.val = val;
            return;
        }
        cur = cur.next;
    }
    //没有找到当前链表中有这个key的节点
    //头插法
    Node node = new Node(val, key);
    node.next = array[index];
    array[index] = node;
    useSize++;
    if(doLoadFactor() >= DEFAULT_LOAD_FACTOR){
        //扩容
        resize();
    }
}

public void resize(){
    //array = Arrays.copyOf(array, 2*array.length);
    Node[] newArray = new Node[2*array.length];
    for (int i = 0; i < array.length; i++) {
        Node cur = array[i];
        while(cur != null){
            int newIndex = cur.key % newArray.length;
            Node curN = cur.next;
            cur.next = newArray[newIndex];
            newArray[newIndex] = cur;
            cur = curN;
        }
    }
    array = newArray;
}


private double doLoadFactor() {
    return useSize*1.0 / array.length;
}

注意:这里的 DEFAULT_LOAD_FACTOR 是在定义在相关变量里的,其完整代码为:

//定义相关变量
static class Node{
    public int val;
    public int key;
    public Node next;
    
    public Node(int val, int key){
        this.val = val;
        this.key = key;
    }
    
    public Node[] elem = new Node[10];
    public int useSize;
    public static final double DEFAULT_LOAD_FACTOR = 0.75f;
}

这里我们可以简单进行调试,

Test类代码为:

public class Test {
    public static void main(String[] args) {
        HashBusk hashBusk = new HashBusk();
        hashBusk.push(1, 9);
        hashBusk.push(11, 9);
        hashBusk.push(14, 9);
        hashBusk.push(4, 9);
        hashBusk.push(2, 9);
        hashBusk.push(15, 9);
        hashBusk.push(6, 9);
        hashBusk.push(5, 9);
    }
}

调试的断点放在了第7个数的位置,因为再往下走需要进行扩容了,可以看到代码是按照上面的数组+链表的方式进行存储的。

然后是扩容的部分

可以看到,扩容后数组的长度来到15,证明扩容部分也是可以正常进行的。

getVal方法

通过key的值,来得到val值,这部分代码,其实和插入里部分代码有些相同的部分:

通过key值来找到数据的位置,如果相同返回val值,没找到返回-1。

public int getVal(int key){
    int index = key % array.length;
    Node cur = array[index];
    while(cur != null) {
        if(cur.key == key){
            return cur.val;
        }
        cur = cur.next;
    }
    return -1;
}

完整代码

public class HashBusk {
    
    //定义相关变量
    static class Node {
        public int val;
        public int key;
        public Node next;

        public Node(int val, int key) {
            this.val = val;
            this.key = key;
        }

    }

    public Node[] array = new Node[10];
    public int useSize;
    public static final double DEFAULT_LOAD_FACTOR = 0.75f;

    public void  push(int key, int val){
        int index = key % array.length;
        Node cur = array[index];
        while(cur != null){
            if(cur.key == key){
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //没有找到当前链表中有这个key的节点
        //头插法
        Node node = new Node(val, key);
        node.next = array[index];
        array[index] = node;
        useSize++;
        if(doLoadFactor() >= DEFAULT_LOAD_FACTOR){
            //扩容
            resize();
        }
    }

    public void resize(){
        Node[] newArray = new Node[2*array.length];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while(cur != null){
                int newIndex = cur.key % newArray.length;
                Node curN = cur.next;
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;
                cur = curN;
            }
        }
        array = newArray;
    }


    private double doLoadFactor() {
        return useSize*1.0 / array.length;
    }

    public int getVal(int key){
        int index = key % array.length;
        Node cur = array[index];
        while(cur != null) {
            if(cur.key == key){
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }
}

总结 

到此这篇关于Java哈希表的概念及实现的文章就介绍到这了,更多相关Java哈希表实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文