哈希表(HashMap)分析及实现(JAVA)

转自:http://www.java3z.com/cwbwebhome/article/article8/83560.html?id=4649

——————————————————————————————————————————————————————————————————


探讨Hash表中的一些原理/概念,及根据这些原理/概念,自己设计一个用来存放/查找数据的Hash表,并且与JDK中的HashMap类进行比较。

我们分一下七个步骤来进行。
一。 Hash表概念
二 . Hash构造函数的方法,及适用范围
三.
Hash处理冲突方法,各自特征
四. Hash查找过程
五. 实现一个使用Hash存数据的场景--Hash查找算法,插入算法
六.
JDK中HashMap的实现
七. Hash表与HashMap的对比,性能分析

一。 Hash表概念

在Hash表中,记录在表中的位置和其关键字之间存在着一种确定的关系。这样
我们就能预先知道所查关键字在表中的位置,从而直接通过下标找到记录。

1) 哈希(Hash)函数是一个映象,即: 将关键字的集合映射到某个地址集合上,它的设置很灵活,
只要这个地址集合的大小不超出允许范围即可;

2) 由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,
即: key1!=key2,而 f (key1) =
f(key2)。

3).
只能尽量减少冲突而不能完全避免冲突,这是因为通常关键字集合比较大,其元素包括所有可能的关键字,
而地址集合的元素仅为哈希表中的地址值.在构造这种特殊的“查找表”
时,除了需要选择一个“好”(尽可能少产生冲突)
的哈希函数之外;还需要找到一 种“处理冲突” 的方法。

二 . Hash构造函数的方法,及适用范围

直接定址法
数字分析法
平方取中法
折叠法
除留余数法
随机数法

(1)直接定址法:

哈希函数为关键字的线性函数,H(key) = key 或者 H(key) = a * key + b

(2)数字分析法:
    假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体,

并从中提取分布均匀的若干位或它们的组合作为地址。

此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。

(3)平方取中法:
   以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,

时平方值的中间各位又能受到整个关键字中各位的影响。

(4)折叠法:
   
将关键字分割成若干部分,然后取它们的叠加和为哈希地址。两种叠加处理的方法:移位叠加:
将分割后的几部分低位对齐相加;间界叠加:从一端沿分割界来回折叠,然后对齐相加。

此法适于:关键字的数字位数特别多。

(5)除留余数法:

设定哈希函数为:H(key) = key MOD p ( p≤m ),其中, m为表长,p 为不大于 m 的素数,或 是不含 20 以下的质因子

(6)随机数法:

设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数

实际造表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),
以及哈希表长度(哈希地址范围),总的原则是使产生冲突的可能性降到尽可能地小。

三. Hash处理冲突方法,各自特征

“处理冲突” 的实际含义是:为产生冲突的关键字寻找下一个哈希地址。

开放定址法
再哈希法
链地址法

(1)开放定址法:
    为产生冲突的关键字地址 H(key) 求得一个地址序列: H0, H1, H2, …, Hs 1≤s≤m-1,Hi =
( H(key) +di ) MOD m,
其中: i=1, 2, …, s,H(key)为哈希函数;m为哈希表长;

(2)链地址法:

将所有哈希地址相同的记录都链接在同一链表中。

(3)再哈希法:
   方法:构造若干个哈希函数,当发生冲突时,根据另一个哈希函数计算下一个哈希地址,直到冲突不再发
生。
即:Hi=Rhi(key) i=1,2,……k,其中:Rhi——不同的哈希函数,特点:计算时间增加

四. Hash查找过程

对于给定值 K,计算哈希地址 i = H(K),若 r[i] = NULL 则查找不成功,若 r[i].key = K 则查找成功,
否则
“求 下一地址 Hi” ,直至r[Hi] = NULL (查找不成功) 或r[Hi].key = K (查找成功) 为止。

五. 实现一个使用Hash存数据的场景-------Hash查找算法,插入算法

假设我们要设计的是一个用来保存中南大学所有在校学生个人信息的数据表。因为在校学生数量也不是特别巨大(8W),
每个学生的学号是唯一的,因此,我们可以简单的应用直接定址法,声明一个10W大小的数组,每个学生的学号作为主键。
然后每次要添加或者查找学生,只需要根据需要去操作即可。

但是,显然这样做是很脑残的。这样做系统的可拓展性和复用性就非常差了,比如有一天人数超过10W了?
如果是用来保存别的数据呢?或者我只需要保存20条记录呢?声明大小为10W的数组显然是太浪费了的。

如果我们是用来保存大数据量(比如银行的用户数,4大的用户数都应该有3-5亿了吧?),这时候我们计算出来的
HashCode就很可能会有冲突了,
我们的系统应该有“处理冲突”的能力,此处我们通过挂链法“处理冲突”。

如果我们的数据量非常巨大,并且还持续在增加,如果我们仅仅只是通过挂链法来处理冲突,可能我们的链上挂了
上万个数据后,这个时候再通过静态搜索来查找链表,显然性能也是非常低的。所以我们的系统应该还能实现自动扩容,
当容量达到某比例后,即自动扩容,使装载因子保存在一个固定的水平上。

综上所述,我们对这个Hash容器的基本要求应该有如下几点:

满足Hash表的查找要求(废话)
能支持从小数据量到大数据量的自动转变(自动扩容)
使用挂链法解决冲突

好了,既然都分析到这一步了,咱就闲话少叙,直接开始上代码吧。

public class MyMap< K, V> {
    private int size;// 当前容量
    private static int INIT_CAPACITY = 16;// 默认容量
    private Entry< K, V>[] container;// 实际存储数据的数组对象
    private static float LOAD_FACTOR = 0.75f;// 装载因子
    private int max;// 能存的最大的数=capacity*factor   

    // 自己设置容量和装载因子的构造器
    public MyMap(int init_Capaticy, float load_factor) {
        if (init_Capaticy < 0)
            throw new IllegalArgumentException("Illegal initial capacity: "
                    + init_Capaticy);
        if (load_factor <= 0 || Float.isNaN(load_factor))
            throw new IllegalArgumentException("Illegal load factor: "  + load_factor);
        this.LOAD_FACTOR = load_factor;
        max = (int) (init_Capaticy * load_factor);
        container = new Entry[init_Capaticy];
    }   

    // 使用默认参数的构造器
    public MyMap() {
        this(INIT_CAPACITY, LOAD_FACTOR);
    }   

    /**
     * 存
     *
     * @param k
     * @param v
     * @return
     */
    public boolean put(K k, V v) {
        // 1.计算K的hash值
        // 因为自己很难写出对不同的类型都适用的Hash算法,故调用JDK给出的hashCode()方法来计算hash值
        int hash = k.hashCode();
        //将所有信息封装为一个Entry
        Entry< K,V> temp=new Entry(k,v,hash);
            if(setEntry(temp, container)){
                // 大小加一
                size++;
                return true;
            }
            return false;
    }   

    /**
     * 扩容的方法
     *
     * @param newSize
     *            新的容器大小
     */
    private void reSize(int newSize) {
        // 1.声明新数组
        Entry< K, V>[] newTable = new Entry[newSize];
        max = (int) (newSize * LOAD_FACTOR);
        // 2.复制已有元素,即遍历所有元素,每个元素再存一遍
        for (int j = 0; j < container.length; j++) {
            Entry< K, V> entry = container[j];
            //因为每个数组元素其实为链表,所以…………
            while (null != entry) {
                setEntry(entry, newTable);
                entry = entry.next;
            }
        }
        // 3.改变指向
        container = newTable;   

    }   

    /**
     *将指定的结点temp添加到指定的hash表table当中
     * 添加时判断该结点是否已经存在
     * 如果已经存在,返回false
     * 添加成功返回true
     * @param temp
     * @param table
     * @return
     */
    private boolean setEntry(Entry< K,V> temp,Entry[] table){
        // 根据hash值找到下标
        int index = indexFor(temp.hash, table.length);
        //根据下标找到对应元素
        Entry< K, V> entry = table[index];
        // 3.若存在
        if (null != entry) {
            // 3.1遍历整个链表,判断是否相等
            while (null != entry) {
                //判断相等的条件时应该注意,除了比较地址相同外,引用传递的相等用equals()方法比较
                //相等则不存,返回false
             if ((temp.key == entry.key||temp.key.equals(entry.key)) &&
 temp.hash == entry.hash&&(temp.value==entry.value||temp.value.equals(entry.value))) {
                    return false;
                }  

                 else if(temp.key == entry.key && temp.value != entry.value) {
                     entry.value = temp.value;
                    return true;
                } 

                //不相等则比较下一个元素
                else if (temp.key != entry.key) {
                        //到达队尾,中断循环
                        if(null==entry.next){
                            break;
                        }
                        // 没有到达队尾,继续遍历下一个元素
                        entry = entry.next;
                }
            }
            // 3.2当遍历到了队尾,如果都没有相同的元素,则将该元素挂在队尾
            addEntry2Last(entry,temp);
               return true;
        }
        // 4.若不存在,直接设置初始化元素
        setFirstEntry(temp,index,table);
        return true;
    }   

    private void addEntry2Last(Entry< K, V> entry, Entry< K, V> temp) {
        if (size > max) {
            reSize(container.length * 4);
        }
        entry.next=temp;   

    }   

    /**
     * 将指定结点temp,添加到指定的hash表table的指定下标index中
     * @param temp
     * @param index
     * @param table
     */
    private void setFirstEntry(Entry< K, V> temp, int index, Entry[] table) {
        // 1.判断当前容量是否超标,如果超标,调用扩容方法
        if (size > max) {
            reSize(table.length * 4);
        }
        // 2.不超标,或者扩容以后,设置元素
        table[index] = temp;
        //!!!!!!!!!!!!!!!
        //因为每次设置后都是新的链表,需要将其后接的结点都去掉
         //NND,少这一行代码卡了哥哥7个小时(代码重构)
        temp.next=null;
    }   

    /**
     * 取
     *
     * @param k
     * @return
     */
    public V get(K k) {
        Entry< K, V> entry = null;
        // 1.计算K的hash值
        int hash = k.hashCode();
        // 2.根据hash值找到下标
        int index = indexFor(hash, container.length);
        // 3。根据index找到链表
        entry = container[index];
        // 3。若链表为空,返回null
        if (null == entry) {
            return null;
        }
        // 4。若不为空,遍历链表,比较k是否相等,如果k相等,则返回该value
        while (null != entry) {
            if (k == entry.key||entry.key.equals(k)) {
                return entry.value;
            }
            entry = entry.next;
        }
        // 如果遍历完了不相等,则返回空
        return null;   

    }   

    /**
     * 根据hash码,容器数组的长度,计算该哈希码在容器数组中的下标值
     *
     * @param hashcode
     * @param containerLength
     * @return
     */
    public int indexFor(int hashcode, int containerLength) {
        return hashcode & (containerLength - 1);   

    }   

    /**
     * 用来实际保存数据的内部类,因为采用挂链法解决冲突,此内部类设计为链表形式
     *
     * @param < K>key
     * @param < V>
     *            value
     */
    class Entry< K, V> {
        Entry< K, V> next;// 下一个结点
        K key;// key
        V value;// value
        int hash;// 这个key对应的hash码,作为一个成员变量,当下次需要用的时候可以不用重新计算   

        // 构造方法
        Entry(K k, V v, int hash) {
            this.key = k;
            this.value = v;
            this.hash = hash;   

        }   

        //相应的getter()方法   

    }
}

第一次初始化加的时候,因为每个元素的next都是空的,而扩充容量resize()时,
因为冲突处理是链式结构的,当将他们重新hash添加的时候,重复的这些鸟元素的next是有元素的,一定要设置为null。

七.性能分析:

1.因为冲突的存在,其查找长度不可能达到O(1)

2哈希表的平均查找长度是装载因子a 的函数,而不是 n 的函数。

3.用哈希表构造查找表时,可以选择一个适当的装填因子 ,使得平均查找长度限定在某个范围内。

最后给出我们这个HashMap的性能

测试代码

public class Test {   

    public static void main(String[] args) {
        MyMap< String, String> mm = new MyMap< String, String>();
        Long aBeginTime=System.currentTimeMillis();//记录BeginTime
        for(int i=0;i< 1000000;i++){
        mm.put(""+i, ""+i*100);
        }
        Long aEndTime=System.currentTimeMillis();//记录EndTime
        System.out.println("insert time-->"+(aEndTime-aBeginTime));   

        Long lBeginTime=System.currentTimeMillis();//记录BeginTime
        mm.get(""+100000);
        Long lEndTime=System.currentTimeMillis();//记录EndTime
        System.out.println("seach time--->"+(lEndTime-lBeginTime));
    }
}

100W个数据时,全部存储时间为1S多一点,而搜寻时间为0

时间: 2024-10-27 06:15:49

哈希表(HashMap)分析及实现(JAVA)的相关文章

哈希表解决冲突链地址java

package ch17; import java.math.BigInteger; public class HashTable { private LinkList[] arr; /** * 默认的构造方法 */ public HashTable() { arr = new LinkList[100]; } /** * 指定数组初始化大小 */ public HashTable(int maxSize) { arr = new LinkList[maxSize]; } /** * 插入数据

九. 常用类库、向量与哈希6.哈希表及其应用

哈希表也称为散列表,是用来存储群体对象的集合类结构. 什么是哈希表 数组和向量都可以存储对象,但对象的存储位置是随机的,也就是说对象本身与其存储位置之间没有必然的联系.当要查找一个对象时,只能以某种顺序(如顺序查找或二分查找)与各个元素进行比较,当数组或向量中的元素数量很多时,查找的效率会明显的降低. 一种有效的存储方式,是不与其他元素进行比较,一次存取便能得到所需要的记录.这就需要在对象的存储位置和对象的关键属性(设为 k)之间建立一个特定的对应关系(设为 f),使每个对象与一个唯一的存储位置

深入Java基础(四)--哈希表(1)HashMap应用及源码详解

继续深入Java基础系列.今天是研究下哈希表,毕竟我们很多应用层的查找存储框架都是哈希作为它的根数据结构进行封装的嘛. 本系列: (1)深入Java基础(一)--基本数据类型及其包装类 (2)深入Java基础(二)--字符串家族 (3)深入Java基础(三)–集合(1)集合父类以及父接口源码及理解 (4)深入Java基础(三)–集合(2)ArrayList和其继承树源码解析以及其注意事项 文章结构:(1)哈希概述及HashMap应用:(2)HashMap源码分析:(3)再次总结关键点 一.哈希概

Code Review:C#与JAVA的哈希表内部机制的一些区别

看C#与JAVA源码时发现C#与JAVA哈希表的实现略有不同,特此分享一下. 我觉得看哈希表的机制可以从"碰撞"这里划线分为两部分来分析. 1,发生碰撞前 在发生碰撞前决定get与put的速度唯一因素是通过哈希函数计算键值位置的速度.而占用空间大小取决于需要的桶的数量(取决于极限装载值(load factor)(假设已知需要放入哈希表中元素的数量)),和桶的大小. C#的默认装载系数=0.72 // Based on perf work, .72 is the optimal load

Java集合源码学习笔记(四)HashMap分析

ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景,数组的特点是:寻址容易,插入和删除困难:链表的特点是:寻址困难,插入和删除容易:哈希表的实现结合了这两点,哈希表的实现方式有多种,在HashMap中使用的是链地址法,也就是拉链法.看下面这张流传很广的图, 拉链法实际上是一种链表数组的结构,由数组加链表组成,在这个长度为16的数

HashMap/HashSet,hashCode,哈希表

HashMap实现了Map接口,该接口的作用主要是为客户提供三种方式的数据显示:只查看keys列表:只查看values列表,或以key-value形式成对查看.Map接口并没有定义数据要如何存储,也没有指定如何判定key是一样,因此并不是所有的Map实现都会与hashCode方法扯上关系,如TreeMap便是要求对象实现Comparator接口,通过其compare方法来比对两者是否一致,而非hashCode及equals.同理,如果我们自己实现Map接口,我们也可以直接使用数组进行数据存储使用

哈希表工作原理 (并不特指Java中的HashTable)

1. 引言         哈希表(Hash Table)的应用近两年才在NOI中出现,作为一种高效的数据结构,它正在竞赛中发挥着越来越重要的作用.  哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间:而代价仅仅是消耗比较多的内存.然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的.另外,编码比较容易也是它的特点之一.         哈希表又叫做散列表,分为“开散列” 和“闭散列”.考虑到竞赛时多数人通常避免使用动态存储结构,本文中的“哈希表”仅

哈希表在Java类库中的实现——HashSet

HashSet是基于哈希表在Java类库中的实现--HashMap来实现的,与HashMap不同的是,它保存的是单个元素,而不是键值对. 在HashSet里,用HashMap来存储元素: private transient HashMap<E,Object> map; 从这个map的类型看出,它把HashSet的元素存储为HashMap的键.因为键值对的value位置空着,所以用一个值来占据键值对的value的位置: private static final Object PRESENT =

【算法】哈希表的诞生(Java)

参考资料 <算法(java)>                           — — Robert Sedgewick, Kevin Wayne <数据结构>                                  — — 严蔚敏 为什么要使用哈希表 查找和插入是查找表的两项基本操作,对于单纯使用链表,数组,或二叉树实现的查找表来说,这两项操作在时间消耗上仍显得比较昂贵. 以查找为例:在数组实现的查找表中,需要用二分等查找方式进行一系列的比较后,才能找到给定的键值对