【Java数据结构】——哈希表

什么是哈希表

哈希表也称为散列表,是用来存储群体对象的集合类结构

数组和向量都可以存储对象,但对象的存储位置是随机的,也就是说对象本身与其存储位置之间没有必然的联系。当要查找一个对象时,只能以某种顺序(如顺序查找或二分查找)与各个元素进行比较,当数组或向量中的元素数量很多时,查找的效率会明显的降低。

一种有效的存储方式,是不与其他元素进行比较,一次存取便能得到所需要的记录。这就需要在对象的存储位置和对象的关键属性之间建立一个特定的对应关系,使每个对象与一个唯一的存储位置相对应。在查找时,只要根据待查对象的关键属性 k 计算 f(k)的值即可。如果此对象在集合中,则必定在存储位置 f(k)上,因此不需要与集合中的其他元素进行比较。称这种对应关系 f 为哈希(hash)方法,按照这种思想建立的表为哈希表

哈希表的概念

Java使用哈希表类(Hashtable)来实现哈希表,以下是与哈希表相关的一些概念
容量(Capacity):Hashtable的容量不是固定的,随对象的加入其容量也可以自动增长。
关键字(Key:每个存储的对象都需要有一个关键字,key可以是对象本身,也可以是对象的一部分(如某个属性)。要求在一个Hashtable中的所有关键字都是唯一的。
哈希码(Hash Code:若要将对象存储到Hashtable上,就需要将其关键字key映射到一个整型数据,成为key的哈希码。
项(Item):Hashtable中的每一项都有两个域,分别是关键字域 key 和值域 value(存储的对象)。Key 和 Value 都可以是任意的Object类型的对象,但不能为空。
装填因子(Load Factor):装填因子表示为哈希表的装满程度,其值等于元素数比上哈希表的长度。

哈希(散列)方法

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

哈希函数

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_14,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_16,color_FFFFFF,t_70,g_se,x_16

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

元素44使用哈希函数应当插入 位置 为 4下标,但是4下标已经有了元素,这种情况怎么办呢?

哈希冲突(碰撞)

这里我们引出一个概念叫做 哈希冲突也叫哈希碰撞

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_16,color_FFFFFF,t_70,g_se,x_16

 冲突-避免

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

冲突-避免-哈希函数设计

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

哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间

哈希函数计算出来的地址能均匀分布在整个空间中

哈希函数比较简单

常见哈希函数

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_19,color_FFFFFF,t_70,g_se,x_16

 冲突-避免-负载因子调节

loadFactor = usedSize/array.length;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_18,color_FFFFFF,t_70,g_se,x_16

 如何解决冲突?

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

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

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

缺点:会把冲突的元素都放到一起

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_17,color_FFFFFF,t_70,g_se,x_16

 2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置方法为:aabb5187897046748596895839f61e44.png或者cf6d69734892456c9e29fc2c635e7f17.pnga95cbcd514564a59b83255cac0bbb4f5.png

其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,

m是表的大小。 对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_17,color_FFFFFF,t_70,g_se,x_16

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


因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
 

冲突-解决0开散列/哈希桶

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

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_16,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5pq06b6Z5oiY5aOr57uI57qn6L-b5YyW,size_12,color_FFFFFF,t_70,g_se,x_16

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

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

代码实现

public class HashBuck {//哈希桶
    //构造节点
    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 static final double DEFAULT_LOAD_FACTOR = 0.75;//载荷因子
    public HashBuck() {
        this.array = new Node[10];
    }

    /**
     * put 函数
     * @param key
     * @param val
     */
    public void put(int key,int val) {
        //1.找到key所在的位置
        int index = key % this.array.length;

        //2.遍历这个下标的链表,看是不是有相同的key,有要更新value值得
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                cur.val = val;//更新val值
                return;
            }
            cur = cur.next;
        }
        //3.没有这个key 这个元素
        //头插或者尾插,这里头插
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
        //4.插入元素成功之后,检查当前散列表的负载因子
        if(loadFactor()>= DEFAULT_LOAD_FACTOR) {//超过了0.75要扩容
            resize();//
        }
    }



    public void resize() {
        Node[] newArray = new Node[array.length*2];
        for (int i = 0; i < this.array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                int index = cur.key % newArray.length; //获取新的下标
                //就是把cur这个节点以头插或者尾插 插入到新的数组对应下标的链表当中
                Node curNext = cur.next;//画图好理解
                cur.next = newArray[i];//先绑定后面
                newArray[i] = cur;//绑定前面
                cur = curNext;
            }
        }
        array = newArray;
    }

    private double loadFactor() {
        return 1.0*this.usedSize/array.length;
    }

    /**
     * 根据key获取val值
     * @param key
     * @return
     */
    public int get(int key) {
        //1.找到key所在的位置
        int index = key % this.array.length;
        //2.遍历这个下标的链表,看是不是有相同的key,有要更新val
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }

    public static void main(String[] args) {
        HashBuck hashBuck = new HashBuck();
        hashBuck.put(1,1);
        hashBuck.put(12,12);
        hashBuck.put(3,3);
        hashBuck.put(6,6);
        hashBuck.put(7,7);
        hashBuck.put(2,2);
        hashBuck.put(11,11);
        hashBuck.put(8,8);
        System.out.println(hashBuck.get(11));
    }
}


 

猜你喜欢

转载自blog.csdn.net/Biteht/article/details/123091273
今日推荐