数据结构基础:哈希表(HashMap)原理分析

前言:

 数组的特点是:寻址容易,插入和删除困难;

 链表的特点是:寻址困难,插入和删除容易;

我们可以构造一种结合两种优点的链表散列的数据结构,可以理解为链表的数组,HashMap就是基于其实现的。

 

1.哈希表的缺点有和优点

优点:

相对数组可以节省存储空间;

插入和寻址都很快;

在散列表中,查找一个元素的时间和链表中是相同的,都为O(n),但是在实践中散列表效率是很高的,查找一个元素的期望的时间为O(1);

缺点:

它是基于数组的数组创建完后扩展比较难所以当哈希表被填满的时候,性能会下降很多;所以,最好是知道表中要存储多少数据;


2. 理解寻址

在理解Hashmap之前,先理解哈寻址


直接寻址方式:

 

 

 

 

 哈希寻址:

 

 

 

关键字是k的元素被散列到槽h(k);

 


所以现在就剩下几个问题:


1.如何哈希化


//JDK源码

 final int hash(Object k) {

        int h = hashSeed;

        if (0 != h && k instanceof String) {

            return sun.misc.Hashing.stringHash32((String) k);

        }

 

        h ^= k.hashCode();

 

        // This function ensures that hashCodes that differ only by

        // constant multiples at each bit position have a bounded

        // number of collisions (approximately 8 at default load factor).

        h ^= (h >>> 20) ^ (h >>> 12);

        return h ^ (h >>> 7) ^ (h >>> 4);

    }

 

哈希函数的设计其实很有讲究, 目标就是尽量减少冲突,同时把寻址控制在一定范围内;

 具体的理论现在还理解不了, 源码的分析可以参考:

http://pengranxiang.iteye.com/blog/543893


 

2.如何解决冲突

  指定的数组大小是需要存储的数据量的两倍,因此,可能有一半的单元是空的.

当冲突发生

方法一:找到数组的一个空位,把数据插入,称为开放地址法;

 

方法二:创建一个存放链表的数组数组内不直接存放数据,这样当冲突发生,新的数据项直接接到这个数组下标所指的链表中;(链地址法)

 

2.1 开放地址法:

一种简单的就是:当要插入的数据的位置是1234, 如果位置被占了那么就看看1235, 以此类推,直到找到空位这样的方式叫线性探测;

当然,还有其他更好的改进的探测方法,就不仔细说了;

 

 

2.2 链地址法:

在链地址法中,如果需要在N个单元的数组中存放大于N个数据,因此装填因子大于1;

装填因子为2/3左右的时候,开发地址法的哈希表效率会下降很多而链地址法当因子为大于1,且对性能影响不是很大;

当然如果链表中有许多项存储时间会变长因为存储特定的数据需要搜索链表一半的长度;

 

2.3 JDK的链地址法具体实现

 (这部分原文是来自 http://xiaolu123456.iteye.com/blog/1485349)


public V put(K key, V value) {  

        if (key == null)  

            return putForNullKey(value);  

        int hash = hash(key.hashCode());  

        int i = indexFor(hash, table.length);  

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  

            Object k;  

            /*判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。  

            如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。  

            Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。  

            系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),  

            那系统必须循环到最后才能找到该元素。  

*/

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  

                V oldValue = e.value;  

                e.value = value;  

                return oldValue;  

            }  

        }  

        modCount++;  

        addEntry(hash, key, value, i);  

        return null;  

    }  

 


理解HASHMAP冲突最重要的一句话冲突是不可避免的,所以要去解决但是要尽最大努力,减少冲突的机会;

个人的理解是:减少冲突一方面是体现在哈希函数的设计上另外,作为使用者也要注意下容量是否合适;

 

HashMapAPI里面有一句:

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。

 

3. 哈希表怎么扩容

上面提到了默认的加载因子为0.75, 那么什么时候JDK里面的Hashmap数组会扩容扩多大?

在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。

如果 初始容量*加载因子<最大数据条目,则会发生扩容操作。 

//JDK源码 

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
 * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }


        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }



每次在原来的基础上增大1(table.lenght*2)



所以在使用的过程中, 合理使用扩容.


参考:   http://blog.csdn.net/likika2012/article/details/40510007

http://www.cnblogs.com/matrix-skygirl/archive/2013/01/17/2864919.html

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是Java中常用的一种数据结构,它基于哈希表实现。下面是HashMap的简要源码分析: 1. 数据结构HashMap内部使用了数组和链表(或红黑树)来实现。数组的每个位置称为桶(bucket),每个桶存储一个链表的头节点。当链表长度超过阈值(默认为8)时,链表会转换为红黑树,提高查找效率。 2. 成员变量: - `transient Node<K,V>[] table`:用于存储元素的数组,是HashMap的主要数据结构。初始时为null,第一次插入元素时才会初始化。 - `transient int size`:HashMap中元素的个数。 - `int threshold`:扩容的阈值,当元素个数超过此值时触发扩容操作。 - `float loadFactor`:负载因子,用于计算扩容阈值,默认值为0.75。 - `int modCount`:用于记录HashMap结构修改的次数,用于迭代器的快速失败机制。 - `static final int DEFAULT_INITIAL_CAPACITY`:默认初始容量为16。 - `static final int MAXIMUM_CAPACITY`:最大容量,为2^30。 - `static final float DEFAULT_LOAD_FACTOR`:默认负载因子。 - `static final int TREEIFY_THRESHOLD`:链表转化为红黑树的阈值。 - `static final int UNTREEIFY_THRESHOLD`:红黑树转化为链表的阈值。 - `static final int MIN_TREEIFY_CAPACITY`:最小树化容量。 3. 常用方法: - `put(K key, V value)`:向HashMap中插入键值对,如果键已存在,则更新值,否则新增键值对。 - `get(Object key)`:根据键获取对应的值。 - `remove(Object key)`:根据键移除对应的键值对。 - `containsKey(Object key)`:判断是否包含指定的键。 - `containsValue(Object value)`:判断是否包含指定的值。 - `size()`:返回HashMap中键值对的个数。 - `isEmpty()`:判断HashMap是否为空。 - `clear()`:清空HashMap中的所有键值对。 4. 实现原理: - 添加元素时,根据键的hashCode()计算数组下标,如果该位置为空,则直接插入;如果该位置已经有元素,则通过equals()方法比较键的相等性。如果发生冲突(hashCode相等但不相等),则将元素插入链表或红黑树中。 - 查找元素时,根据键的hashCode()计算数组下标,然后遍历链表或红黑树,通过equals()方法比较键的相等性,找到对应的值。 - 扩容时,创建新的两倍大小的数组,将旧数组中的元素重新散列到新数组中。 以上是HashMap的简要源码分析HashMap是Java集合框架中常用的数据结构之一,具有高效的插入、查找和删除操作,适用于存储键值对的场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值