大话Java中的哈希(hash)结构(一)

o( ̄▽ ̄)d

小伙伴们在上网或者搞程序设计的时候,总是会听到关于“哈希(hash)”的一些东西。比如哈希算法、哈希表等等的名词,那么什么是hash呢?

一.相关概念

1.hash算法:一类特殊的算法(注意哦,hash算法并不是某个固定的算法,而是一类特殊功能算法的统称)。

2.哈希表(hash table)、哈希映射(hash map)、哈希集合(hash set):一种基于hash算法的数据结构

3.哈希函数:在hash算法中的核心函数。

4.map:译为“映射”,是一种从键(key)到值(value)的对应关系。

5.消息摘要:又叫“数字摘要”、“数字指纹”,类似于文章的梗概,或者人的指纹,是一个唯一对应一段信息的值。

二.hash算法

(一).hash算法是什么?

第一步,我们先去查查字典,看看hash是啥意思。

hash

英 [hæ?]   美 [hæ?] 

n.剁碎的食物;#号;蔬菜肉丁

vt.把…弄乱;切碎;反复推敲;搞糟

差不多就是“切碎”的意思,“哈希”又叫“散列”。(这里就想吐槽一下,起这么多小名干嘛,名字不重要......)

第二步,我们去查查百科。

Hash算法可以接受任意长度字节的输入值,并经过复杂运算,给出固定长度的输出值

从这里我们可以看出,只要一个算法符合上面的特征,就可以称其为“hash算法”。Hash算法产生“数字摘要”:也就是说,从不同的输入中,通过一些计算摘取出来一段输出数据,值可以用以区分输入数据

(二).hash算法到底有什么用处呢?

1.在信息安全方面

Hash算法可用作加密算法。

  • 文件校验:通过对文件摘要,可以得到文件的“数字指纹”,你下载的任何副本的“数字指纹”只要和官方给出的“数字指纹”一致,那么就可以知道这是未经篡改的。例如著名的SHA 。
  • 数字签名:由于hash算法几乎一一对应的关系(当然不是绝对的一一对应,不过发生“碰撞”的概率微乎其微),所以hash算法可以用于产生一个机构的数字签名,类似于物理上的某个人的文件签名。

2.在数据结构方面

Hash算法可用作快速查找。

在数据结构领域,有一种哈希表(hash table)的结构,正是利用了hash算法的特性,实现了以常数平均时间执行插入、删除和查找。这个也是接下来讨论的重点。

三.哈希/散列表

原则上来说,计算机中最基本的数据结构只有两种:数组(连续型)和链表(离散型)。其它诸如堆、栈、树、表等都是数组与链表的特殊实现。也就是说,把数组和链表经过特殊处理(这个过程可以叫做“封装”),就产生了其他的“高级数据结构类型”。哈希表也是其中一种。或者换一种说法,堆、栈、树等都是逻辑结构,而连续存储和离散存储是在物理方面的结构。

我们分析一下传统的查找模式。比如我们建立了一个“查找树”。当我们在查找时,会从根节点“逐一比较”,查找的效率依赖于查找的次数。

~哈希表

一种最理想的情况是不需要做任何比较,一次存取就能得到想要的记录。那就必须在记录的存储位置和它的关键字之间建立一个确定的关系h,使每个关键字和结构中一个唯一的存储位置相对应。因而在查找时,只要根据这个对应关系h找到给定值K的像h(K)。若结构中存在关键字和K相等的记录,则必定在h(K)的存储位置上,反之在这个位置上没有记录。由此,不需要比较便可直接取得所查记录。在此,我们称这个对应关系h为哈希(Hash)函数 ,按这个思想建立的表为哈希表 。

~哈希函数

1.灵活性

哈希函数是一种映像关系,说的通俗一点,就是一种对应关系。因此只要得到的哈希值在表允许的范围内就可以。

2.冲突

对不同的关键字可能得到同一哈希地址,即key1≠key2,而h(key1)=h(key2) ,这种现象称为冲突(collision)。

很遗憾,冲突是不可避免的。上面我们说,哈希函数是关键字到地址集合的映射。关键字可以在格式允许的范围内任意取,但是我们内存的地址集合是有限的。从大范围到小范围,所以冲突不可避免。但是我们可以设法减小冲突的概率。因此单单一个合适的哈希函数是不够的,还要有一个良好的解决冲突的方法。

由此我们得到了哈希表的两个关键:哈希函数以及解决冲突的方法

综上,我们可以得到哈希表的一个定义:

根据设定的Hash函数 - H(key)和处理冲突的方法,将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的作为记录在表中的存储位置,这样的表便称为Hash表

四.java中的HashMap

1.FAQ

问:为什么有HashMap?

答:HashMap利用hash算法实现了快速存取的特性。

问:hash表和HashMap有什么关系?

答:Hash表 是一种逻辑数据结构,HashMap是Java中的一种数据类型(结构类型),它通过代码实现了Hash表 这种数据结构,并在此结构上定义了一系列操作。

2.上帝视角的HashMap

  • HashMap是基于数组来实现哈希表的,数组就好比内存储空间,数组的index就好比内存的地址;
  • HashMap的每个记录就是一个Entry<K, V>对象,数组中存储的就是这些对象;
  • HashMap的哈希函数 = 计算出hashCode + 计算出数组的index;
  • HashMap解决冲突:使用链地址法,每个Entry对象都有一个引用next来指向链表的下一个Entry;
  • HashMap的装填因子:默认为0.75;

基本上HashMap就像这样:

3.实现一个new HashMap

 1 /*** 1. 构造方法:最终使用的是这个构造方法 ***/
 2 // 初始容量initialCapacity为16,装填因子loadFactor为0.75
 3 public HashMap(int initialCapacity, float loadFactor) {
 4     if (initialCapacity < 0)
 5         throw new IllegalArgumentException("Illegal initial capacity: " +
 6                                            initialCapacity);
 7     if (initialCapacity > MAXIMUM_CAPACITY)
 8         initialCapacity = MAXIMUM_CAPACITY;
 9     if (loadFactor <= 0 || Float.isNaN(loadFactor))
10         throw new IllegalArgumentException("Illegal load factor: " +
11                                            loadFactor);
12
13     this.loadFactor = loadFactor;
14     threshold = initialCapacity;
15     init();//init可以忽略,方法默认为空{},当你需要集成HashMap实现自己的类型时可以重写此方法做一些事
16 }
17
18 /*** 2. (静态/实例)成员变量 ***/
19 /** 默认的容量,容量必须是2的幂 */
20 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
21 /** 最大容量2的30次方 */
22 static final int MAXIMUM_CAPACITY = 1 << 30;
23 /** 默认装填因子0.75 */
24 static final float DEFAULT_LOAD_FACTOR = 0.75f;
25 /** 默认Entry数组 */
26 static final Entry<?,?>[] EMPTY_TABLE = {};
27
28 /** Entry数组:table */
29 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
30
31 /** table中实际的Entry数量 */
32 transient int size;
33
34 /**
35  * size到达此门槛后,必须扩容table;
36  * 值为capacity * load factor,默认为16 * 0.75 也就是12。
37  * 意味着默认情况构造情况下,当你存够12个时,table会第一次扩容
38  */
39 int threshold;
40
41 /** 装填因子,值从一开构造HashMap时就被确定了,默认为0.75 */
42 final float loadFactor;
43
44 /**
45  * 哈希种子,实例化HashMap后在将要使用前设置的随机值,可以使得key的hashCode冲突更难出现
46  */
47 transient int hashSeed = 0;
48
49 /**
50  * The number of times this HashMap has been structurally modified
51  * Structural modifications are those that change the number of mappings in
52  * the HashMap or otherwise modify its internal structure (e.g.,
53  * rehash).  This field is used to make iterators on Collection-views of
54  * the HashMap fail-fast.  (See ConcurrentModificationException).
55  */
56 transient int modCount;
57
58 /*** 3. Map.Entry<K,V>:数组table中实际存储的类型 ***/
59 static class Entry<K,V> implements Map.Entry<K,V> {
60     final K key;       // "Key-Value对"的Key
61     V value;           // "Key-Value对"的Value
62     Entry<K,V> next;
63     int hash;
64
65     Entry(int h, K k, V v, Entry<K,V> n) {
66         value = v;
67         next = n;//链表的下一个Entry
68         key = k;
69         hash = h;
70     }
71     public final int hashCode() {
72         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
73     }
74 }

HashMap

4.存 - put(key, value)、解决冲突

 1 /** 存放 **/
 2 public V put(K key, V value) {
 3     if (table == EMPTY_TABLE) {
 4         inflateTable(threshold);//table会被初始化为长度16,且hashSeed会被赋值;
 5     }
 6     if (key == null)
 7         //HashMap允许key为null:在table中找到null key,然后设置Value,同时其hash为0;
 8         return putForNullKey(value);
 9
10     // a). 计算key的hashCode,下面详细说
11     int hash = hash(key);
12
13     // b). 根据hashCode计算index
14     int i = indexFor(hash, table.length);
15
16     // c). 覆盖(如是相同Key则覆盖Value,注意这里不是解决冲突):
17     // 遍历index位置的Entry链表,如果链表中Entry的hash相等且== || equals则认为是同一个key,所以覆盖value
18     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
19         Object k;
20         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
21             V oldValue = e.value;
22             e.value = value;
23             e.recordAccess(this);
24             return oldValue;
25         }
26     }
27
28     modCount++;
29     // d). 正常添加Entry,如果需要增加table长度(size>threshold)就乘2增加,并重新计算每个元素在新table中的位置和转移
30     addEntry(hash, key, value, i);
31     return null;//增加成功最后返回null
32 }
33
34
35 //详细说说上面的a). b). d).
36
37 /** a). 为了防止低质量的hash函数,HashMap在这里会重新计算一遍key的hashCode **/
38 final int hash(Object k) {
39     int h = hashSeed;
40     if (0 != h && k instanceof String) {//字符串会被特殊处理,返回32bit的整数(就是int)
41         return sun.misc.Hashing.stringHash32((String) k);
42     }
43
44     h ^= k.hashCode();//将key的hashCode与h按位异或,最后赋值给h
45
46     // This function ensures that hashCodes that differ only by
47     // constant multiples at each bit position have a bounded
48     // number of collisions (approximately 8 at default load factor).
49     h ^= (h >>> 20) ^ (h >>> 12);
50     return h ^ (h >>> 7) ^ (h >>> 4);
51 }
52
53 /**
54  * b). 计算此hashCode该被放入table的哪个index
55  */
56 static int indexFor(int h, int length) {
57     return h & (length-1);//与table的length - 1按位与,就能保证返回结果在0-length-1内
58 }
59
60 /**
61  * 解决冲突:链地址法
62  * d).  addEntry(hash, key, value, i)最终是调用了此函数
63  */
64 void createEntry(int hash, K key, V value, int bucketIndex) {
65     Entry<K,V> e = table[bucketIndex];//首先table中此index的Entry拿出来
66     // 在构造函数中,e代表next。
67     // 即如果e是null,那正好设置next为null,否则将原有的Entry设置为next,新建的Entry放在此位置
68     // 也就是说这里用链地址法解决冲突时,会将原有元素放入链尾,新元素放在链头
69     table[bucketIndex] = new Entry<>(hash, key, value, e);
70     size++;
71 }

put(key,value)

5.取-get(key)

1 //其实看完了最精髓的存,取的话就比较简单,就不放代码在这里了,仅说下思路。
2
3 // 1. 根据k使用hash(k)重新计算出hashCode
4 // 2. 根据indexFor(int h, int length)计算出该k的index
5 // 3. 如果该index处Entry的key与此k相等,就返回value,否则继续查看该Entry的next

五.Java中的equals()hashCode()比较

Java中Object类有两个方法,都是有关于“相同”的概念。

在上面对于hash函数的讨论中,我们知道对于相同的key必须得到同一个hashCode。

但是在Java中,相同有两个概念,一个是“同一个”,另一个是“相等”。

从上面我们可以看出,HashMap设计选择了equals()。

此时,也许你就能明白为什么Object的

HashCode()方法说:相等的对象必须有相等的哈希值。

Equals()方法说:覆盖此方法,通常由必要重写hashCode()方法,以维护其general contract。

原因:

1. Object的equals()方法开始看,此方法默认使用==进行判断;还要知道,hashCode()native方法,即它是依赖C语言算出来的。

2. 我们可以观察一下HashMap的put(key)操作,它首先进行的是判断相等的key就覆盖的操作,也就是使用了key的equals()方法,在这种情况下,如果      你自己的类覆盖了euqals()方法而没有管hashCode()方法,那么,在put(key)方法中,依赖hashCode计算出index这一步,就会将原本equals()的对象放在    不同index中,这样接下来的覆盖操作就不会起作用了。造成的结果是,相等的对象被放入了不同index位置,而不是覆盖。

所以我们要避免出现这类问题,在改写equals()的同时也要改写hashCode()。我们可以去看官方给出的API文档,就很好地遵循了这一准则。

OK,在这一篇中我们分析了hash算法的构造以及java包中实现的hashmap功能,在下一篇我们会给出一些hash表中的hash函数实现方法和解决“冲突”的方法。

参考:【1】哈希表、Java中HashMap

【2】严蔚敏,吴伟民.数据结构(C语言版).北京:清华大学出版社,2007

【3】 HashMap深度解析(一)

时间: 2024-08-04 19:20:36

大话Java中的哈希(hash)结构(一)的相关文章

java中的mvc和三层结构究竟是什么关系

一件事,要知其然往往很简单,要知其所以然通常不是那么容易,就如最近重新巩固spring的过程中,就觉得还有许多问题其实并不是十分明了. 屈指一算,手头上做过的正式项目也有了四五六七个了,不管用的数据库和其他一些细节上的技术如何,总的来说大的框架结构都是差不多的. 说白了,也就是mvc和三层结构. 而mvc和三层结构究竟是什么关系,我曾在面试的过程中被人问过几次,也曾仔细的想过.查过这个问题,但是直到此时,我也还是不能完全确定. 只不过随着时间的积累,随着技术的沉淀,随着视野的拓宽,我大体上认同了

java中OutofMemoryError和JVM内存结构

OutOfMemoryError在开发过程中是司空见惯的,遇到这个错误,新手程序员都知道从两个方面入手来解决: 1:是排查程序是否有BUG导致内存泄漏: 2:是调整JVM启动参数增大内存. OutOfMemoryError有好几种情况,每次遇到这个错误时,观察OutOfMemoryError后面的提示信息,就可以发现不同之处,如: 引用 java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: unable t

Java中的哈希值

下面都是从String类的源码中粘贴出来的 1 private int hash; // Default to 0 2 public int hashCode() { 3 int h = hash; 4 if (h == 0 && value.length > 0) { 5 char val[] = value; 6 for (int i = 0; i < value.length; i++) { 7 h = 31 * h + val[i]; 8 } 9 hash = h; 1

java中的循环--while循环结构

 1. 循环的三要素: 1)循环变量的初始化: 2)循环的条件: 3)循环变量的叠加: 2. while循环的语法: while(boolean表达式){ 语句块: } 执行顺序: 1.计算Boolean的值: 2.如果Boolean的值为ture则执行语句块:执行完后在计算Boolean的值,如果是ture的话继续执行语句块,如此循环往复,知道Boolean的值为false,循环结束. 示例:培训机构今年有25万人,每年增长25%,那么人数增长到100万需要几年? public class n

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

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

Java中的HashCode(1)之hash算法基本原理

Java中的HashCode(1)之hash算法基本原理 2012-11-16 14:58:59     我来说两句      作者:woshixuye 收藏    我要投稿 一.为什么要有Hash算法 Java中 的集合有两类,一类是List,一类是Set.List内的元素是有序的,元素可以重复.Set元素无序,但元素不可重复.要想保证元素不重复,两个元素 是否重复应该依据什么来判断呢?用Object.equals方法.但若每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的

2014阿里实习生面试题——哈希的原理和java中hashmap怎样实现的

1.哈希的原理 哈希的出现时由于传统数据结构如线性表(数组,链表等),树中.keyword与其他的存放位置不存在相应的关系.因此在查找keyword的时候须要逐个比对,尽管出现了二分查找等各种提高效率的的查找算法. 可是这些并不足够.希望在查询keyword的时候不经过不论什么比較.一次存取便能得到所查记录.因此,我们必须在keyword和其相应的存储位置间建立相应的关系f.这样的相应的关系f被称为哈希函数,按此思想建立的表为哈希表. 关键在于哈希函数怎样构造. 有例如以下几种方法: 1)直接定

C++ 的向量结构结合了Java中数组和向量两者的优点

C++ 的向量结构结合了Java中数组和向量两者的优点.一个C++ 的向量可以方便的被访问,其容量又可以动态的增长.如果 T 是任意类型,则 vector<T> 是一个元素为 T 类型的动态数组.下面的语句 vector<int> a; 产生一个初始为空的向量.而语句 vector<int> a(100); 生成一个初始有100个元素的向量.你可以使用push_back 函数来添加元素: a.push_back(n); 调用 a.pop_back() 从a中取出最后一个

java中的进程与线程及java对象的内存结构【转】

原文地址:http://rainforc.iteye.com/blog/2039501 1.实现线程的三种方式: 使用内核线程实现 内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上.程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量