java哈希表

35 篇文章 0 订阅
8 篇文章 0 订阅

1.哈希表

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。

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

向该结构中插入元素时:根据插入元素的关键码,以此函数计算出该元素的存储位置并按照此位置进行存放
搜索:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

2.Hash冲突

首先了解基本的哈希函数设置:
hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
利用此方法存储 1,7,6,4,5,9;
我们将capacity设置为10;
则有:
hash(1) = 1%10=1
hash(7) = 7%10=7
hash(6) = 6%10=6
hash(4) = 4%10=4
hash(5) = 5%10=5
hash(9) = 9%10=9
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

在44取余10,hash(10) = 44%10=4;此时4的位置上已经存储了4,因而产生了冲突。
冲突就是不同的关键字通过相同的哈希函数计算出相同的哈希地址,我们将具有不同关键码拥有相同哈希地址的数据称为同义词。

3.避免冲突

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

3.1 哈希函数的设计

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

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数:

  1. 直接定制-常用:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。
  2. 除留余数法-常用:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
  3. 平方取中法:假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。
  4. 折叠法:折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
  5. 随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。通常应用于关键字长度不等时采用此法
  6. 数学分析法:设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

3.2 负载因子调节

概念:

在这里插入图片描述

当冲突率达到一个无法忍受的程度时,需要通过降低负载因子来变相的降低冲突率;已知哈希表中关键字的个数是不可更改的,因此我们能调整的就是哈希表中的数组大小;

3.3 闭散列

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

3.3.1 线性探测

比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
线性探测的过程:

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
    在这里插入图片描述

** 注意:闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。**

3.3.2 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此需要二次探测避免此问题:即在计算出当前关键码的位置发生哈希冲突之后,再次进行哈希函数的计算。
如果当前找下个位置的方法是:
在这里插入图片描述

或者
在这里插入图片描述
其中:i = 1,2,3…, H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。 对于上述中如果要插入44,产生冲突,使用解决后的情况为:

Hi = (4+1)%10=5;出现哈希冲突继续Hi = (4+4)%10=8;没有哈希冲突,因此44放在下标为8的位置;

注意:研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此闭散列的缺点就是空间利用率低,

3.4 哈希桶–开散列法

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

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

4.自行实现哈希表

package demo;

import java.util.Hashtable;

public class HashBuck {
    public static void main(String[] args) {
        HashBuck hashBuck = new HashBuck();
        hashBuck.push(1,1);
        hashBuck.push(2,2);
        hashBuck.push(10,10);
        hashBuck.push(5,5);
        hashBuck.push(6,6);
        hashBuck.push(7,7);
        hashBuck.push(8,8);
        System.out.println(hashBuck.get(6));
    }
    static class Node{
        public int key;
        public int val;
        public Node next;
        public Node(int key,int val){
            this.key = key;
            this.val = val;
        }
    }
    public  Node[] array;
    public int usedSize;
    public HashBuck(){
        this.array = new Node[8];
    }
    public  void push(int key,int val){
        Node node = new Node(key,val);
        int index =  key%array.length;
        Node cur = array[index];
        while(cur!=null){
            if (cur.key==key){
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        node.next = array[index];
        array[index] = node;
        this.usedSize++;
        //每次放入元素,都需要检查负载因子。判断是否大于0.75;
        if (loadFactor()>=0.75){
            resize();
        }

    }
    public double loadFactor(){
        return this.usedSize*1.0/this.array.length;
    }
    //如果负载因子超过0.75,需要进行扩容,
    //注意:扩容之后数组长度改变了,因此所有的元素都需要重新hash,需要遍历原本每个链表,
    protected 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 index = cur.key % newArray.length;
                Node curNext = cur.next;
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }
    public int get(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;
    }
}

在这里插入图片描述
了解一下原码中扩容方法:

protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值