zoukankan      html  css  js  c++  java
  • 散列算法与散列码

    一、引入

    /**
     * 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 + '}';
        }
    }
    Groundhog
    /**
     * 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";
            }
            return "Early Spring!";
        }
    }
    Prediction
     1 public class SpringDetector {
     2 
     3     public static void main(String[] args) {
     4         detectSpring();
     5     }
     6 
     7     public static void detectSpring() {
     8         Map<Groundhog, Prediction> map = new HashMap(16);
     9         for (int i = 0; i < 10; i++) {
    10             map.put(new Groundhog(i), new Prediction());
    11         }
    12         System.out.println("map=" + map);
    13 
    14         Groundhog groundhog = new Groundhog(3);
    15         System.out.println();
    16 
    17         //查找这个key是否存在
    18         if (map.containsKey(groundhog)) {
    19             System.out.println(map.get(groundhog));
    20         } else {
    21             System.out.println("key not find:" + groundhog);
    22         }
    23     }
    24 }

        看这个结果,问题就来了,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,这很好的权衡了时间和空间的成本。

        当 HashMap 中 size > capacity * Load factor,即容量超过了允许的最大元素数目,HashMap 的桶就需要扩容,当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

        这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,这样的话会将时间复杂度从O(n)降为O(logn),其中会用到红黑树的插入、删除、查找等算法。

    tips:为使散列分布均衡,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的原理了。

     1 /**
     2  * Description:首先新建一个类作为map中存储的对象并重写了 hashCode()和 equals() 方法
     3  */
     4 public class MPair implements Map.Entry, Comparable<MPair> {
     5     private Object key, value;
     6 
     7     public MPair(Object key, Object value) {
     8         this.key = key;
     9         this.value = value;
    10     }
    11 
    12     @Override
    13     public int compareTo(MPair mPair) {
    14         return ((Comparable) key).compareTo(mPair.key);
    15     }
    16 
    17     @Override
    18     public Object getKey() {
    19         return key;
    20     }
    21 
    22     @Override
    23     public Object getValue() {
    24         return value;
    25     }
    26 
    27     @Override
    28     public int hashCode() {
    29         int result = key != null ? key.hashCode() : 0;
    30         result = 31 * result + (value != null ? value.hashCode() : 0);
    31         return result;
    32     }
    33 
    34     @Override
    35     public boolean equals(Object o) {
    36         return key.equals(((MPair) o).key);
    37     }
    38 
    39     @Override
    40     public Object setValue(Object v) {
    41         Object result = value;
    42         this.value = v;
    43         return result;
    44     }
    45 
    46     @Override
    47     public String toString() {
    48         return "MPair{" + "key=" + key + ", value=" + value + '}';
    49     }
    50 }
    MPair
    public class SimpleHashMap extends AbstractMap {
    
        /**
         * 定一个初始大小的哈希表容量
         */
        private static final int SZ = 3;
        /**
         * 建一个hash数组,用linkedList实现
         */
        private LinkedList[] linkedLists = new LinkedList[SZ];
    
        @Override
        public Object put(Object key, Object value) {
            Object result = null;
            //对key的值做求模法求出index
            int index = key.hashCode() % SZ;
            if (index < 0) {
                index = -index;
            }
            //如果这个index位置没有对象,就新建一个
            if (linkedLists[index] == null) {
                linkedLists[index] = new LinkedList();
            }
            //取出这个index的对象linkedList
            LinkedList linkedList = linkedLists[index];
            //新建要存储的对象mPair
            MPair mPair = new MPair(key, value);
            ListIterator listIterator = linkedList.listIterator();
            boolean found = false;
            //遍历这个index位置的List,如果查找到跟之前一样的对象(根据equals来比较),则更新那个key对应的value
            while (listIterator.hasNext()) {
                MPair next = (MPair) listIterator.next();
                if (mPair.equals(next)) {
                    result = next.getValue();
                    //更新动作
                    listIterator.set(mPair);
                    found = true;
                    break;
                }
            }
            //如果没有找到这个对象,则在这index的List对象上新增一个元素。
            if (!found) {
                linkedLists[index].add(mPair);
            }
            return result;
    
        }
    
        @Override
        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];
            // 新建一个空的对象值,因为equals()的比较是看他们的key是否相等,而在List中的遍历对象的时候,是通过key来查找对象的。
            MPair mPair = new MPair(key, null);
            ListIterator listIterator = linkedList.listIterator();
            while (listIterator.hasNext()) {
                MPair next = (MPair) listIterator.next();
                // 找到了这个key就返回这个value
                if (next.equals(mPair)) {
                    return next.getValue();
                }
            }
            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");
            //这里有四个元素,其中key是1和key是4的index是一样的,所以index为1的List上面存了两个元素。
            simpleHashMap.put("4", "4");
            System.out.println(simpleHashMap);
            Object o = simpleHashMap.get("1");
            System.out.println(o);
            Object o1 = simpleHashMap.get("4");
            System.out.println(o1);
        }
    }

    原创作品,转载请注明出处:http://www.cnblogs.com/jmcui/p/7419779.html

  • 相关阅读:
    java_设计模式_观察者模式_Observer Pattern(2016-07-27)
    java_设计模式_策略模式_Strategy pattern(2016-07-15)
    一个简单的路由,用javascript实现
    sublime 编辑器配置和构建检查
    图文列表的图片居中
    不用css3的响应式img(按比例缩小图片)
    做前端的小笔记
    javascript创建跟随鼠标好玩的东西
    几十行代码就搞定俄罗斯方块
    来看看css3中的box-shadow
  • 原文地址:https://www.cnblogs.com/jmcui/p/7419779.html
Copyright © 2011-2022 走看看