散列算法与散列码(实例讲解)

一、引入

/**
 * Description:新建一个类作为map的key
 */
public class Groundhog
{
  protected int number;

  public Groundhog(){
  }
  public Groundhog(int number)
  {
    this.number = number;
  }

  @Override
  public String toString()
  {
    return "Groundhog{" + "number=" + number + '}';
  }
}

/**
 * Description:新建一个类作为map的value
 */
public class Prediction
{
  private boolean shadow=Math.random() > 0.5;

  @Override
  public String toString()
  {
    if (shadow) return "Six more weeks of Winter";
    else return "Early Spring!";
  }
}

/**
 * Description:测试类
 */
public class SpringDetector
{
  public static void detectSpring(Class grondHotClass) throws Exception{
    Constructor constructor = grondHotClass.getConstructor(new Class[]{int.class});
    Map map=new HashMap();
    for (int i=0;i<10;i++){
      map.put(constructor.newInstance(new Object[]{new Integer(i)}),new Prediction());
    }
    System.out.println("map="+map);

    Groundhog groundhog=(Groundhog)constructor.newInstance(new Object[]{new Integer(3)});
    System.out.println(groundhog);

    if (map.containsKey(groundhog)) {//查找这个key是否存在
      System.out.println((Prediction)map.get(groundhog));
    }else {
      System.out.println("key not find:"+groundhog);
    }

  }
  public static void main(String[] args)
  {
    try {
      detectSpring(Groundhog.class);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

看这个结果,问题就来了,map中明明存在Groudhog{number=3},为什么结果显示的是Key not find呢??问题出在哪里呢?原来是Groudhog类没有重写hashCode()方法,所以这里是使用Object的hashCode()方法生成散列码,而他默认是使用对象的地址计算散列码。因此,由Groudhog(3)生成的第一个实例的散列码与Groudhog(3)生成的散列码是不同的,所以无法查找到 key。但是仅仅重写hashCode()还是不够的,除非你重写equals()方法。原因在于不同的对象可能计算出同样的hashCode的值,hashCode 的值并不是唯一的,当hashCode的值一样时,就会使用equals()判断当前的“键”是否与表中的存在的键“相同”,即“

如果两个对象相同,那么他们的hashCode值一定相同。

如果两个对象的hashCode值相同,他们不一定相同。

正确的equals()方法必须满足下列5个条件:

1、自反性: x.equals(x) 一定成立。

2、对称性: 如果x.equals(y)成立,那么y.equals(x)也一定成立。

3、传递性:如果x.equals(y)=true ,y.equals(z)=true,那么x.equals(z)=true也成立。

4、一致性:无论调用x.equal(y)多少次,返回的结果应该保持一致。

5、对任何不是null的x,x.equals(null)一定返回false。

二、理解hashCode()

散列的价值在于速度:散列使得查询得以快速执行。由于速度的瓶颈是对“键”进行查询,而存储一组元素最快的数据结构是数组,所以用它来代表键的信息,注意:数组并不保存“键”的本身。而通过“键”对象生成一个数字,将其作为数组的下标索引。这个数字就是散列码,由定义在Object的hashCode()生成(或成为散列函数)。同时,为了解决数组容量被固定的问题,不同的“键”可以产生相同的下标。那对于数组来说?怎么在同一个下标索引保存多个值呢??原来数组并不直接保存“值”,而是保存“值”的 List。然后对 List中的“值”使用equals()方法进行线性的查询。这部分的查询自然会比较慢,但是如果有好的散列函数,每个下标索引只保存少量的值,只对很少的元素进行比较,就会快的多。

不知道大家有没有理解我上面在说什么。不过没关系,下面会有一个例子帮助大家理解。不过我之前一直被一个问题纠结:为什么一个hashCode的下标存的会有多个值?因为hashMap里面只能有唯一的key啊,所以只能有唯一的value在那个下标才对啊。这里就要提出一个新的概念哈希冲突的问题,借用网上的一个例子:

比如:数组的长度是5。这时有一个数据是6。那么如何把这个6存放到长度只有5的数组中呢。按照取模法,计算6%5,结果是1,那么就把6放到数组下标是1的位置。那么,7就应该放到2这个位置。到此位置,哈希冲突还没有出现。这时,有个数据是11,按照取模法,11%5=1,也等于1。那么原来数组下标是1的地方已经有数了,是6。这时又计算出1这个位置,那么数组1这个位置,就必须储存两个数了。这时,就叫哈希冲突。冲突之后就要按照顺序来存放了。所以这里Java中用的解决方法就是在这个hashCode上存一个List,当遇到相同的hashCode时,就往这个List里add元素就可以了。这才是hash原理的精髓所在啊!哈哈、纠结我一天。

三、HashMap的性能因子

容量(Capacity):散列表中的数量。

初始化容量(Initial capacity):创建散列表时桶的数量。HashMap 和 HashSet都允许你在构造器中制定初始化容量。

尺寸(Size):当前散列表中记录的数量。

负载因子(Load factor):等于"size/capacity"。负载因子为0,表示空的散列表,0.5表示半满的散列表,依次类推。轻负载的散列表具有冲突少、适宜插入与适宜查询的特点(但是使用迭代器遍历会变慢)。HashMap和hashSet的构造器允许你制定负载因子。这意味着,当负载达到制定值时,容器会自动成倍的增加容量,并将原有的对象重新分配,存入新的容器内(这称为“重散列”rehashing)。HashMap默认的负载因子为0.75,这很好的权衡了时间和空间的成本。

备注:为使散列分布均衡,Java的散列函数都使用2的整数次方来作为散列表的理想容量。对现代的处理器来说,除法和求余是最慢的动作。使用2的整数次方的散列表,可用掩码代替除法。因为get()是使用最多的操作,求余数的%操作是其开销的大部分,而使用2的整数次方可以消除此开销(也可能对hashCode()有些影响)

四、怎么重写hashCode()

现在的IDE工具中,一般都能自动的帮我们重写了hashCode()和equals()方法,但那或许并不是最优的,重写hashCode()有两个原则:

必须速度快,并且必须有意义。也就是说,它必须基于对象的内容生成散列码。

应该产生分布均匀的散列码。如果散列码都集中在一块,那么在某些区域的负载就会变得很重。

下面是怎么写出一份像样的hashCode()的基本指导:

1、给int变量result 赋予某个非零值常量,例如 17。

2、为每个对象内每个有意义的属性f (即每个可以做equals()的属性)计算出一个 int 散列码c:

3、合并计算得到的散列值:result=37*result+c;

4、返回 result;

5、检查hashCode()最后生成的结果,确保相同的对象有相同的散列码。

五、自定义HashMap

下面我们将自己写一个hashMap,便于了解底层的原理,大家如果看的懂下面的代码,也就很好的理解了hashCode的原理了。

/**
 * Description:首先新建一个类作为map中存储的对象并重写了hashCode()和equals()方法
 */
public class MPair implements Map.Entry,Comparable
{
  private Object key,value;

  public MPair(Object key,Object value)
  {
    this.key = key;
    this.value=value;
  }
  @Override
  public int compareTo(Object o)
  {
    return ((Comparable)key).compareTo(((MPair)o).key);
  }

  @Override
  public Object getKey()
  {
    return key;
  }

  @Override
  public Object getValue()
  {
    return value;
  }

   @Override
  public int hashCode()
  {
    int result = key != null ? key.hashCode() : 0;
    result = 31 * result + (value != null ? value.hashCode() : 0);
    return result;
  }

  @Override
  public boolean equals(Object o)
  {
    return key.equals(((MPair)o).key);
  }

  @Override
  public Object setValue(Object v)
  {
    Object result=value;
    this.value=v;
    return result;
  }

  @Override
  public String toString()
  {
    return "MPair{" + "key=" + key + ", value=" + value + '}';
  }
public class SimpleHashMap extends AbstractMap
{

  private static final int SZ=3;//定一个初始大小的哈希表容量
  private LinkedList[] linkedLists=new LinkedList[SZ];//建一个hash数组,用linkedList实现
  public Object put(Object key,Object value){
    Object result=null;
    int index=key.hashCode() % SZ;//对key的值做求模法求出index
    if (index<0) index=-index;
    if (linkedLists[index]==null) linkedLists[index]=new LinkedList();//如果这个index位置没有对象,就新建一个

    LinkedList linkedList = linkedLists[index];//取出这个index的对象linkedList
    MPair mPair = new MPair(key,value);//新建要存储的对象mPair
    ListIterator listIterator = linkedList.listIterator();
    boolean found =false;
    while (listIterator.hasNext()){//遍历这个index位置的List,如果查找到跟之前一样的对象(根据equals来比较),则更新那个key对应的value
      Object next = listIterator.next();
      if (next.equals(mPair)){
        result = ((MPair) next).getValue();
        listIterator.set(mPair);//更新动作
        found=true;
        break;
      }
    }
    if (!found) linkedLists[index].add(mPair);//如果没有找到这个对象,则在这index的List对象上新增一个元素。
    return result;

  }

  public Object get(Object key){
    int index = key.hashCode() % SZ;
    if (index<0) index=-index;
    if (linkedLists[index]==null) return null;
    LinkedList linkedList = linkedLists[index];
    MPair mPair=new MPair(key,null);//新建一个空的对象值,因为equals()的比较是看他们的key是否相等,而在List中的遍历对象的时候,是通过key来查找对象的。
    ListIterator listIterator = linkedList.listIterator();
    while (listIterator.hasNext()){
      Object next = listIterator.next();
      if (next.equals(mPair)) return ((MPair)next).getValue();//找到了这个key就返回这个value
    }
    return null;

  }

  @Override
  public Set<Entry> entrySet()
  {
    Set set=new HashSet();
    for (int i=0;i<linkedLists.length;i++){
      if (linkedLists[i]==null) continue;
      Iterator iterator = linkedLists[i].iterator();
      while (iterator.hasNext()){
        set.add(iterator.next());
      }
    }
    return set;
  }

  public static void main(String[] args)
  {
    SimpleHashMap simpleHashMap=new SimpleHashMap();
    simpleHashMap.put("1", "1");
    simpleHashMap.put("2", "2");
    simpleHashMap.put("3","3");
    simpleHashMap.put("4","4");//这里有四个元素,其中key是1和key是4的index是一样的,所以index为1的List上面存了两个元素。
    System.out.println(simpleHashMap);
    Object o = simpleHashMap.get("1");
    System.out.println(o);
    Object o1 = simpleHashMap.get("4");
    System.out.println(o1);

  }
}

六、结语

不知道大家理解了没?整了我一天,终于还算大概理解了其中的原理了。文笔比较粗糙,大家凑活看吧,毕竟,不会做饭的作家不是好程序员啊!哈哈...... 或者,可能我有很多理解的不到位的地方,还请大家不吝指教!

(0)

相关推荐

  • java排序算法之_选择排序(实例讲解)

    选择排序是一种非常简单的排序算法,从字面意思我们就可以知道,选择就是从未排序好的序列中选择出最小(最大)的元素,然后与第 i 趟排序的第 i-1(数组中下标从 0 开始) 个位置的元素进行交换,第 i 个元素之前的序列就是已经排序好的序列.整个排序过程只需要遍历 n-1 趟便可排好,最后一个元素自动为最大(最小)值. 举个小例子: arr[] = {3,1,2,6,5,4} 第 1 趟排序: index = 0, min = 1, 交换后 -->  1,3,2,6,5,4 第 2 趟排序: in

  • Java算法之数组冒泡排序代码实例讲解

    冒泡排序是数组查找算法中最为简单的算法 冒泡排序原理: 假设一个数组长度为k(最高索引k-1),遍历前k - 1个(最高索引k-2)元素,若数组中的元素a[i]都与相邻的下一个元素a[i+1]进行比较,若a[i] > a[i+1] ,则这两个元素交换位置.以此类推,若a[i+1] > a[i+2],则交换位置-直至a[k-2]与a[k-1]比较完毕后,第0轮迭代结束.此时,a[k-1]为数组元素中的最大值. 第1轮迭代,再对数组a的前k-1个元素重复进行以上操作. - 第k-2轮迭代,对数组a

  • 散列算法与散列码(实例讲解)

    一.引入 /** * Description:新建一个类作为map的key */ public class Groundhog { protected int number; public Groundhog(){ } public Groundhog(int number) { this.number = number; } @Override public String toString() { return "Groundhog{" + "number=" +

  • SHA:安全散列算法简析 附实例

    前言 体能状态先于精神状态,习惯先于决心,聚焦先于喜好. SHA算法简介 1.1 概述 SHA (Secure Hash Algorithm,译作安全散列算法) 是美国国家安全局 (NSA) 设计,美国国家标准与技术研究院(NIST) 发布的一系列密码散列函数.正式名称为 SHA 的家族第一个成员发布于 1993年.然而人们给它取了一个非正式的名称 SHA-0 以避免与它的后继者混淆.两年之后, SHA-1,第一个 SHA 的后继者发布了. 另外还有四种变体,曾经发布以提升输出的范围和变更一些细

  • Java sha1散列算法原理及代码实例

    直接调用HashKit.sha1(String str)方法就可以了,,返回的是16进制的字符串长度是40, 也就是用md.digest()方法解析出来的字节数是160字节长度. 而MD5散列算法生成的字节数是128字节长度,返回的16进制的字符长度是32位 代码如下 public class HashKit { private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); public stati

  • Android数据加密之SHA安全散列算法

    前言: 对于SHA安全散列算法,以前没怎么使用过,仅仅是停留在听说过的阶段,今天在看图片缓存框架Glide源码时发现其缓存的Key采用的不是MD5加密算法,而是SHA-256加密算法,这才勾起了我的好奇心,所以趁着晚上没啥事,来学习一下. 其他几种加密方式: •Android数据加密之Rsa加密  •Android数据加密之Aes加密  •Android数据加密之Des加密  •Android数据加密之MD5加密  •Android数据加密之Base64编码算法 SHA加密算法 SHA(Secu

  • Python3 hashlib密码散列算法原理详解

    1.hashlib密码散列 hashlib模块定义了一个API来访问不同的密码散列算法.要使用一个特定的散列算法,可以用适当的构造器函数或new()来创建一个散列对象.不论使用哪个具体的算法,这些对象都使用相同的API. 1.1 散列算法 由于hashlib有OpenSSL提供"底层支持",所以OpenSSL库提供的所有算法都可用,包括: md5 sha1 sha224 sha256 sha384 sha512 有些算法在所有平台上都可用,而有些则依赖于底层库.这两种算法分别由algo

  • Java HashSet(散列集),HashMap(散列映射)的简单介绍

    简介 本篇将简单讲解Java集合框架中的HashSet与HashMap. 散列集(HashSet) 快速入门 底层原理:动态数组加单向链表或红黑树.JDK 1.8之后,当链表长度超过阈值8时,链表将转换为红黑树. 查阅HashSet的源码,可以看到HashSet的底层是HashMap,HashSet相当于只用了HashMap键Key的部分,当需要进行添加元素操作时,其值Value始终为常量PRESENT = new Object().以下为HashSet的代码片段: private transi

  • C语言实现散列表(哈希Hash表)实例详解

    C语言实现散列表(哈希Hash表) 实例代码: //散列表查找算法(Hash) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 7 #define NULLKEY -32768 typedef int Status; type

  • C#算法之散列表

    目录 1.散列函数 正整数 浮点数 字符串 组合键 将 HashCode() 的返回值转化为一个数组索引 自定义的 HashCode 软缓存 2.基于拉链法的散列表 散列表的大小 删除操作 有序性相关的操作 3.基于线性探测法的散列表 删除操作 键簇 线性探测法的性能分析 调整数组大小 拉链法 均摊分析 4.内存的使用 如果所有的键都是小整数,我们可以使用一个数组来实现无序的符号表,将键作为数组的索引而数组中键 i 处存储的就是它对应的值.散列表就是用来处理这种情况,它是简易方法的扩展并能够处理

随机推荐