• hashMap原理剖析


      在日常开发中,hashMap应该算是比较常用的一个类了,今天就来学习一下hashMap的实现原理。

      概念

        1.什么时hash?

          书面定义:就是把一个不固定长度的二进制值映射成一个固定长度的二进制值。

          个人理解:每一个人都有不同的属性,省份,出生日期,身高,体重等等,这其实就相当于一个不固定长度的二进制值,而身份证号码就是一个唯一的标识,根据每个人的出生信息,按照一定的规则映射成身份证号码,而这个号码就相当于一个固定的二进制值。而这个映射关系就相当于hash算法,身份证号码就相当于hash值。

        2.hash算法

          除留余数法:key%table.leng = 2 就是我们需要存放的位置的索引。

            有时候会出现key1%table.leng = 3    key2%table.leng = 3 这样就会造成hash冲突。而这些冲突的位置会创建一个单向的链表去存储数据。

          平方取中法:将一个m位数的数字区平方,得到一个2m的数字,如果不足2m则前面补0,取中间的m位数字。

        3.为什么会存在这样的数据结构?

          为了更好的存、取数据,基本上要做到时间复杂度越低越好。

      实现

        我们都知道jdk的hashMap是需要实现一个顶层接口Map的,接下来我们来自己实现一个hashMap。

         在hashMap中其实是一个数组存放着entry对象,entry只能够有一个key,一个value,一个entry类型的next指针,其中key存的是hash值,value存的是存放在hashMap的对象,而next存的是hash冲突的下一个对象的引用。所以我们来创建自己的Map接口MyMap

     1 public interface MyMap<K,V> {
     2     
     3     public V put (K k, V v);
     4     
     5     public V get(K k);
     6     
     7     public int size();
     8     
     9     V remove(Object key);
    10     
    11     void putAll(Map<? extends K, ? extends V> m);
    12     
    13     void clear();
    14     
    15     public interface Entry<K, V>{
    16         public K getKey();
    17         
    18         public V getValue(K k);
    19         
    20         V setValue(V value);
    21        22     }
    23 }

        下面去写实现类myHashMap,实现myMap接口。

        在上面定义的myMap中有一个内部的接口Entry,而实现类中有一个内部类叫Entry实现类myMap中的接口Entry,而Entry中需要定义key,value,还有一个Entry类型的next的指针,指向下一个链表数据。

     1 class Entry<K, V> implements MyMap.Entry<K, V>{
     2         
     3         final K k;
     4         
     5         V v;
     6         
     7         //next指针,指向下一个链表数据
     8         Entry<K, V> next;
     9 
    10         @Override
    11         public K getKey() {
    12             // TODO Auto-generated method stub
    13             return null;
    14         }
    15 
    16         @Override
    17         public V getValue(K k) {
    18             // TODO Auto-generated method stub
    19             return null;
    20         }
    21 
    22         @Override
    23         public V setValue(V value) {
    24             // TODO Auto-generated method stub
    25             return null;
    26         }
    27         
    28     }

        下面去实现接口中定义的方法。

           定义好Entry中的构造器。

    1         //构造器
    2         public Entry(K k, V v, Entry<K, V> next) {
    3             super();
    4             this.k = k;
    5             this.v = v;
    6             this.next = next;
    7         }

           实现getkey,getValue,setValue。

     1         @Override
     2         public K getKey() {
     3             return k;
     4         }
     5 
     6         @Override
     7         public V getValue(K k) {
     8             return v;
     9         }
    10 
    11         @Override
    12         public V setValue(V value) {
    13             V oldvale = v;
    14             v = value;
    15             return oldvale;
    16         }                

          Entry中的方法实现以后去实现myHashMap的方法。

          实现前先将变量定义好,一个数组的默认长度,一个负载因子,一个存放Entry的数组,一个数组中默认存放元素的个数。

     1   //默认数组长度
     2     private static int dafaultlength = 16;
     3     
     4     //负载因子
     5     private static double defaultLoad = 0.75;
     6     
     7     //存放Entry对象的数组
     8     private Entry<K, V>[] table = null;
     9     
    10     //数组中存储元素个数
    11     private int size = 0;

          实现put方法前先将hash算法定义好。

     1      /**
     2      * 定义hash算法,除留余数法
     3      * @return hash值
     4      */
     5     private int getIndex(K k){
     6         
     7         int m = dafaultlength; 
     8         //因为hashCode有可能是一个负数,所以结果有可能是一个负数
     9         int indext = k.hashCode() % m;
    10         
    11         return indext >= 0 ? indext : -indext;
    12     }

       在我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。但是扩容就存在一个问题,我们是否要对之前老表的数据再hash呢?答案是肯定的。所以这里还需要一个再hash的方法。

     1     /**
     2      * 递归取单向链表的元素
     3      * @param entry
     4      * @param list
     5      */
     6     private void findEntryByNext (Entry<K,V> entry, List<Entry<K, V>> list){
     7         if (entry != null && entry.next == null){
     8             list.add(entry);
     9             findEntryByNext(entry.next, list);
    10         } else {
    11             list.add(entry);
    12         }
    13     }    
     1     /**
     2      * 数组扩容时,再hash
     3      */
     4     private void reHash (Entry<K, V>[] newTable){
     5         List<Entry<K, V>> list = new ArrayList<Entry<K,V>>();
     6         //for循环取线性表的数据
     7         for (int i = 0; i < table.length; i++) {
     8             if (table[i] == null){
     9                 continue;
    10             }
    11             //递归取单向链表的元素
    12             findEntryByNext(table[i], list);
    13         }
    14         
    15         //再hash
    16         if (list.size() > 0){
    17             size = 0;
    18             defaultlength = defaultlength * 2;
    19             table = newTable;
    20             
    21             for (Entry<K, V> entry : list) {
    22                 if (entry.next != null){
    23                     entry.next = null;
    24                 }
    25                 put(entry.getKey(), entry.getValue());
    26             }
    27         }
    28     }
     1     /**
     2      * 数组扩容:
     3      * 我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一
     4      * 个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,
     5      * 所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。 
     6      */
     7     private void up2size (){
     8         Entry<K, V>[] newTable = new Entry[2 * defaultlength];
     9         reHash(newTable);
    10     }

           实现put方法:

           1.根据k得到index

           2.将index位置的元素拿出来

           3.判断该位置是否有元素存在,如果这个位置不存在元素,直接将值存入,而这里的entry为null,如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。

     1     @Override
     2     public V put(K k, V v) {
     3         //1.根据k得到index
     4         int index = getIndex(k);
     5         //2.将index位置的元素拿出来
     6         Entry<K, V> entry = table[index];
     7         //3.判断该位置是否有元素存在
     8         if (entry == null){
     9             //如果这个位置不存在元素,直接将值存入,而这里的entry为null
    10             table[index] = newEntry(k, v, entry);
    11             size ++;
    12         } else {
    13             //如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。
    14             table[index] = newEntry(k, v, entry);
    15         }
    16         return v;
    17     }    

        实现get方法

          首先通过hash值获取线性表的位置,然后通过比较获取单向链表中的Enery对象

     1     /**
     2      * 递归获取单向链表中的Entry对象
     3      * @param k
     4      * @param entry
     5      * @return
     6      */
     7     private V findValueEqualKey (K k, Entry<K, V> entry){
     8         if (k == entry.getKey() || k.equals(entry.getKey())){
     9             return entry.getValue();
    10         }
    11         if (entry.next != null){
    12             findValueEqualKey(k, entry.next);
    13         }
    14         return null;
    15     }
    1     @Override
    2     public V get(K k) {
    3         int index = getIndex(k);
    4         if (table[index] == null){
    5             return null;
    6         }
    7         return findValueEqualKey(k, table[index]);
    8     }

        实现remove方法:还是通过index查找到单向链表,然后递归找到删除元素的上一个元素,将上一个元素的next指向该元素的后一个元素。

     1     @Override
     2     public V remove(K k) {
     3         int index = getIndex(k);
     4         if (table[index] == null){
     5             return null;
     6         }
     7         if (k == table[index].getKey() || k.equals(table[index].getKey())){
     8             table[index] = table[index].next;
     9             return table[index].getValue();
    10         }
    11         Entry<K, V> entry = findMoveEntry(k, table[index]);
    12         if (entry == null){
    13             return null;
    14         }
    15         Entry<K, V> moveEntry = entry.next;
    16         entry.next = entry.next.next;
    17         return moveEntry.getValue();
    18     }
    19 
    20     private Entry<K, V> findMoveEntry(K k, Entry<K, V> entry){
    21         if (entry.next == null){
    22             return null;
    23         }
    24         if (k == entry.next.getKey() || k.equals(entry.next.getKey())){
    25             return entry;
    26         }
    27         findMoveEntry(k, entry.next);
    28         return null;
    29     }

        看了上面的方法,其实对hashMap的结构有了一定的了解,在hashMap中还有一些其他方法,就不在这一一实现了。

        最后附上实现类中的全部代码。

      1 package hash;
      2 
      3 import java.util.ArrayList;
      4 import java.util.List;
      5 import java.util.Map;
      6 
      7 public class MyHashMap<K, V> implements MyMap<K, V> {
      8     
      9     //默认数组长度
     10     private static int defaultlength = 16;
     11     
     12     //负载因子
     13     private static double defaultLoad = 0.75;
     14     
     15     //存放Entry对象的数组
     16     private Entry<K, V>[] table = null;
     17     
     18     //数组中存储元素个数
     19     private int size = 0;
     20     
     21     public void MyHashMap(int defaultlength, double defaultLoad){
     22         defaultlength = this.defaultlength;
     23         defaultLoad = this.defaultLoad;
     24         table = new Entry[defaultlength];
     25     }
     26     
     27     public MyHashMap (){
     28         this.MyHashMap(defaultlength, defaultLoad);
     29     }
     30 
     31     @Override
     32     public V put(K k, V v) {
     33         //数组扩容
     34         if (size > defaultlength * defaultLoad){
     35             up2size();
     36         }
     37         //1.根据k得到index
     38         int index = getIndex(k);
     39         //2.将index位置的元素拿出来
     40         Entry<K, V> entry = table[index];
     41         //3.判断该位置是否有元素存在
     42         if (entry == null){
     43             //如果这个位置不存在元素,直接将值存入,而这里的entry为null
     44             table[index] = newEntry(k, v, entry);
     45             size ++;
     46         } else {
     47             //如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。
     48             table[index] = newEntry(k, v, entry);
     49         }
     50         return v;
     51     }
     52     
     53     /**
     54      * 数组扩容:
     55      * 我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一
     56      * 个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,
     57      * 所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。 
     58      */
     59     private void up2size (){
     60         Entry<K, V>[] newTable = new Entry[2 * defaultlength];
     61         reHash(newTable);
     62     }
     63     
     64     /**
     65      * 数组扩容时,再hash
     66      */
     67     private void reHash (Entry<K, V>[] newTable){
     68         List<Entry<K, V>> list = new ArrayList<Entry<K,V>>();
     69         //for循环取线性表的数据
     70         for (int i = 0; i < table.length; i++) {
     71             if (table[i] == null){
     72                 continue;
     73             }
     74             //递归取单向链表的元素
     75             findEntryByNext(table[i], list);
     76         }
     77         
     78         //再hash
     79         if (list.size() > 0){
     80             size = 0;
     81             defaultlength = defaultlength * 2;
     82             table = newTable;
     83             
     84             for (Entry<K, V> entry : list) {
     85                 if (entry.next != null){
     86                     entry.next = null;
     87                 }
     88                 put(entry.getKey(), entry.getValue());
     89             }
     90         }
     91     }
     92     
     93     /**
     94      * 递归取单向链表的元素
     95      * @param entry
     96      * @param list
     97      */
     98     private void findEntryByNext (Entry<K,V> entry, List<Entry<K, V>> list){
     99         if (entry != null && entry.next == null){
    100             list.add(entry);
    101             findEntryByNext(entry.next, list);
    102         } else {
    103             list.add(entry);
    104         }
    105     }
    106     
    107     /**
    108      * 获取Entry方法
    109      * @param k
    110      * @param v
    111      * @param next
    112      * @return
    113      */
    114     private Entry<K, V> newEntry (K k, V v, Entry<K, V> next){
    115         return new Entry<K, V>(k, v, next);
    116     } 
    117 
    118     /**
    119      * 定义hash算法,除留余数法
    120      * @return hash值
    121      */
    122     private int getIndex(K k){
    123         
    124         int m = defaultlength; 
    125         //因为hashCode有可能是一个负数,所以结果有可能是一个负数
    126         int indext = k.hashCode() % m;
    127         
    128         return indext >= 0 ? indext : -indext;
    129     }
    130     
    131     @Override
    132     public V get(K k) {
    133         int index = getIndex(k);
    134         if (table[index] == null){
    135             return null;
    136         }
    137         return findValueEqualKey(k, table[index]);
    138     }
    139     
    140     /**
    141      * 递归获取单向链表中的Entry对象
    142      * @param k
    143      * @param entry
    144      * @return
    145      */
    146     private V findValueEqualKey (K k, Entry<K, V> entry){
    147         if (k == entry.getKey() || k.equals(entry.getKey())){
    148             return entry.getValue();
    149         }
    150         if (entry.next != null){
    151             findValueEqualKey(k, entry.next);
    152         }
    153         return null;
    154     }
    155 
    156     @Override
    157     public int size() {
    158         return size;
    159     }
    160 
    161     @Override
    162     public V remove(K k) {
    163         int index = getIndex(k);
    164         if (table[index] == null){
    165             return null;
    166         }
    167         if (k == table[index].getKey() || k.equals(table[index].getKey())){
    168             table[index] = table[index].next;
    169             return table[index].getValue();
    170         }
    171         Entry<K, V> entry = findMoveEntry(k, table[index]);
    172         if (entry == null){
    173             return null;
    174         }
    175         Entry<K, V> moveEntry = entry.next;
    176         entry.next = entry.next.next;
    177         return moveEntry.getValue();
    178     }
    179 
    180     private Entry<K, V> findMoveEntry(K k, Entry<K, V> entry){
    181         if (entry.next == null){
    182             return null;
    183         }
    184         if (k == entry.next.getKey() || k.equals(entry.next.getKey())){
    185             return entry;
    186         }
    187         findMoveEntry(k, entry.next);
    188         return null;
    189     }
    190     @Override
    191     public void putAll(Map<? extends K, ? extends V> m) {
    192     }
    193 
    194     @Override
    195     public void clear() {
    196         Entry<K, V>[] tab = table;
    197         for (int i = 0; i < tab.length; i++)
    198             tab[i] = null;
    199         size = 0;
    200     }
    201 
    202     class Entry<K, V> implements MyMap.Entry<K, V>{
    203         
    204         final K k;
    205         
    206         V v;
    207         
    208         //next指针,指向下一个链表数据
    209         Entry<K, V> next;
    210         
    211         //构造器
    212         public Entry(K k, V v, Entry<K, V> next) {
    213             super();
    214             this.k = k;
    215             this.v = v;
    216             this.next = next;
    217         }
    218 
    219         @Override
    220         public K getKey() {
    221             return k;
    222         }
    223 
    224         @Override
    225         public V getValue() {
    226             return v;
    227         }
    228 
    229         @Override
    230         public V setValue(V value) {
    231             V oldvale = v;
    232             v = value;
    233             return oldvale;
    234         }
    235         
    236     }
    237 }
  • 相关阅读:
    配置caffe matlab 中遇到的坑
    VGGnet论文总结(VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION)
    内核笔记
    VM上安装centos6.x & java & tomcat & mysql
    ----
    python 入门笔记
    centos7 安装 mysql & jetty
    VM 中 CentOS 6 的相关配置
    论文 单词句子笔记
    Android 百度地图开发(三)--- 实现比例尺功能和替换自带的缩放组件
  • 原文地址:https://www.cnblogs.com/wuyx/p/8542769.html
Copyright © 2020-2023  润新知