转自: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的对比,性能分析

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表的查找要求(废话)

能支持从小数据量到大数据量的自动转变(自动扩容)

使用挂链法解决冲突

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

  1 public class MyMap< K, V> {
2 private int size;// 当前容量
3 private static int INIT_CAPACITY = 16;// 默认容量
4 private Entry< K, V>[] container;// 实际存储数据的数组对象
5 private static float LOAD_FACTOR = 0.75f;// 装载因子
6 private int max;// 能存的最大的数=capacity*factor
7
8 // 自己设置容量和装载因子的构造器
9 public MyMap(int init_Capaticy, float load_factor) {
10 if (init_Capaticy < 0)
11 throw new IllegalArgumentException("Illegal initial capacity: "
12 + init_Capaticy);
13 if (load_factor <= 0 || Float.isNaN(load_factor))
14 throw new IllegalArgumentException("Illegal load factor: " + load_factor);
15 this.LOAD_FACTOR = load_factor;
16 max = (int) (init_Capaticy * load_factor);
17 container = new Entry[init_Capaticy];
18 }
19
20 // 使用默认参数的构造器
21 public MyMap() {
22 this(INIT_CAPACITY, LOAD_FACTOR);
23 }
24
25 /**
26 * 存
27 *
28 * @param k
29 * @param v
30 * @return
31 */
32 public boolean put(K k, V v) {
33 // 1.计算K的hash值
34 // 因为自己很难写出对不同的类型都适用的Hash算法,故调用JDK给出的hashCode()方法来计算hash值
35 int hash = k.hashCode();
36 //将所有信息封装为一个Entry
37 Entry< K,V> temp=new Entry(k,v,hash);
38 if(setEntry(temp, container)){
39 // 大小加一
40 size++;
41 return true;
42 }
43 return false;
44 }
45
46
47 /**
48 * 扩容的方法
49 *
50 * @param newSize
51 * 新的容器大小
52 */
53 private void reSize(int newSize) {
54 // 1.声明新数组
55 Entry< K, V>[] newTable = new Entry[newSize];
56 max = (int) (newSize * LOAD_FACTOR);
57 // 2.复制已有元素,即遍历所有元素,每个元素再存一遍
58 for (int j = 0; j < container.length; j++) {
59 Entry< K, V> entry = container[j];
60 //因为每个数组元素其实为链表,所以…………
61 while (null != entry) {
62 setEntry(entry, newTable);
63 entry = entry.next;
64 }
65 }
66 // 3.改变指向
67 container = newTable;
68
69 }
70
71 /**
72 *将指定的结点temp添加到指定的hash表table当中
73 * 添加时判断该结点是否已经存在
74 * 如果已经存在,返回false
75 * 添加成功返回true
76 * @param temp
77 * @param table
78 * @return
79 */
80 private boolean setEntry(Entry< K,V> temp,Entry[] table){
81 // 根据hash值找到下标
82 int index = indexFor(temp.hash, table.length);
83 //根据下标找到对应元素
84 Entry< K, V> entry = table[index];
85 // 3.若存在
86 if (null != entry) {
87 // 3.1遍历整个链表,判断是否相等
88 while (null != entry) {
89 //判断相等的条件时应该注意,除了比较地址相同外,引用传递的相等用equals()方法比较
90 //相等则不存,返回false
91 if ((temp.key == entry.key||temp.key.equals(entry.key)) &&
92 temp.hash == entry.hash&&(temp.value==entry.value||temp.value.equals(entry.value))) {
93 return false;
94 }
95
96 else if(temp.key == entry.key && temp.value != entry.value) {
97 entry.value = temp.value;
98 return true;
99 }
100
101 //不相等则比较下一个元素
102 else if (temp.key != entry.key) {
103 //到达队尾,中断循环
104 if(null==entry.next){
105 break;
106 }
107 // 没有到达队尾,继续遍历下一个元素
108 entry = entry.next;
109 }
110 }
111 // 3.2当遍历到了队尾,如果都没有相同的元素,则将该元素挂在队尾
112 addEntry2Last(entry,temp);
113 return true;
114 }
115 // 4.若不存在,直接设置初始化元素
116 setFirstEntry(temp,index,table);
117 return true;
118 }
119
120 private void addEntry2Last(Entry< K, V> entry, Entry< K, V> temp) {
121 if (size > max) {
122 reSize(container.length * 4);
123 }
124 entry.next=temp;
125
126 }
127
128 /**
129 * 将指定结点temp,添加到指定的hash表table的指定下标index中
130 * @param temp
131 * @param index
132 * @param table
133 */
134 private void setFirstEntry(Entry< K, V> temp, int index, Entry[] table) {
135 // 1.判断当前容量是否超标,如果超标,调用扩容方法
136 if (size > max) {
137 reSize(table.length * 4);
138 }
139 // 2.不超标,或者扩容以后,设置元素
140 table[index] = temp;
141 //!!!!!!!!!!!!!!!
142 //因为每次设置后都是新的链表,需要将其后接的结点都去掉
143 //NND,少这一行代码卡了哥哥7个小时(代码重构)
144 temp.next=null;
145 }
146
147 /**
148 * 取
149 *
150 * @param k
151 * @return
152 */
153 public V get(K k) {
154 Entry< K, V> entry = null;
155 // 1.计算K的hash值
156 int hash = k.hashCode();
157 // 2.根据hash值找到下标
158 int index = indexFor(hash, container.length);
159 // 3。根据index找到链表
160 entry = container[index];
161 // 3。若链表为空,返回null
162 if (null == entry) {
163 return null;
164 }
165 // 4。若不为空,遍历链表,比较k是否相等,如果k相等,则返回该value
166 while (null != entry) {
167 if (k == entry.key||entry.key.equals(k)) {
168 return entry.value;
169 }
170 entry = entry.next;
171 }
172 // 如果遍历完了不相等,则返回空
173 return null;
174
175 }
176
177 /**
178 * 根据hash码,容器数组的长度,计算该哈希码在容器数组中的下标值
179 *
180 * @param hashcode
181 * @param containerLength
182 * @return
183 */
184 public int indexFor(int hashcode, int containerLength) {
185 return hashcode & (containerLength - 1);
186
187 }
188
189 /**
190 * 用来实际保存数据的内部类,因为采用挂链法解决冲突,此内部类设计为链表形式
191 *
192 * @param < K>key
193 * @param < V>
194 * value
195 */
196 class Entry< K, V> {
197 Entry< K, V> next;// 下一个结点
198 K key;// key
199 V value;// value
200 int hash;// 这个key对应的hash码,作为一个成员变量,当下次需要用的时候可以不用重新计算
201
202 // 构造方法
203 Entry(K k, V v, int hash) {
204 this.key = k;
205 this.value = v;
206 this.hash = hash;
207
208 }
209
210 //相应的getter()方法
211
212 }
213 }

第一次初始化加的时候,因为每个元素的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

insert time-->1536

seach time--->0

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

  1. 数据结构HashMap哈希表原理分析

    先看看定义:“散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度. 哈希 ...

  2. 数据结构---散列表查找(哈希表)概述和简单实现(Java)

    散列表查找定义 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key).查找时,根据这个确定的对应关系找到给定值的key的对应f(key) ...

  3. 数据结构 5 哈希表/HashMap 、自动扩容、多线程会出现的问题

    上一节,我们已经介绍了最重要的B树以及B+树,使用的情况以及区别的内容.当然,本节课,我们将学习重要的一个数据结构.哈希表 哈希表 哈希也常被称作是散列表,为什么要这么称呼呢,散列.散列.其元素分布较 ...

  4. java集合-哈希表HashMap

    一.简介 HashMap是一个散列表,是一种用于存储key-value的数据结构. 二.类图 public class HashMap<K,V> extends AbstractMap&l ...

  5. LeetCode 哈希表 380. 常数时间插入、删除和获取随机元素(设计数据结构 List HashMap底层 时间复杂度)

    比起之前那些问计数哈希表的题目,这道题好像更接近哈希表的底层机制. java中hashmap的实现是通过List<Node>,即链表的list,如果链表过长则换为红黑树,如果容量不足(装填 ...

  6. Java知多少(79)哈希表及其应用

    哈希表也称为散列表,是用来存储群体对象的集合类结构. 什么是哈希表 数组和向量都可以存储对象,但对象的存储位置是随机的,也就是说对象本身与其存储位置之间没有必然的联系.当要查找一个对象时,只能以某种顺 ...

  7. java实现自定义哈希表

    哈希表实现原理 哈希表底层是使用数组实现的,因为数组使用下标查找元素很快.所以实现哈希表的关键就是把某种数据类型通过计算变成数组的下标(这个计算就是hashCode()函数 比如,你怎么把一个字符串转 ...

  8. 数据结构和算法(Golang实现)(26)查找算法-哈希表

    哈希表:散列查找 一.线性查找 我们要通过一个键key来查找相应的值value.有一种最简单的方式,就是将键值对存放在链表里,然后遍历链表来查找是否存在key,存在则更新键对应的值,不存在则将键值对链 ...

  9. 第三十四篇 玩转数据结构——哈希表(HashTable)

    1.. 整型哈希函数的设计 小范围正整数直接使用 小范围负整数整体进行偏移 大整数,通常做法是"模一个素数"   2.. 浮点型哈希函数的设计 转成整型进行处理   3.. 字符串 ...

随机推荐

  1. SpringBoot总结之属性配置

    一.SpringBoot简介 SpringBoot是spring团队提供的全新框架,主要目的是抛弃传统Spring应用繁琐的配置,该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配 ...

  2. 达梦数据库(DM8)大规模并行集群MPP 2节点安装部署

    达梦数据库大规模并行集群MPP 2节点安装部署   1.环境准备   os 数据库版本 ip mpp角色 centos7.x86 DM8 192.168.30.100 mpp1 centos7.x86 ...

  3. 第八篇--编写Windows服务

    编写service服务参考网址:https://blog.csdn.net/nodeathphoenix/article/details/24181509 vc获得显示器状态(捕获息屏.亮屏网址):h ...

  4. Cypress 高级用法系列 一

    1. Multiple Assertions cy .get('[data-cy=task]') .then( item => { expect(item[0]).to.contain.text ...

  5. unittest系统(八)一文搞定unittest重试功能

    在前面的介绍中,我们对unittest进行了分享介绍,那么在实际的应用中,因为客观原因需要对失败,错误的测试用例进行重试,所以呢,现有的unittest的框架无法满足,那么我们可以去改造下是否能够满足 ...

  6. RHCSA_DAY02

    Linux:一切皆文件 分区:/boot:做引导盘 /swap:虚拟内存----最大20gb /data:自己放文件用 /:根分区 - 图形界面:   - Ctrl+Shift +号   //调整命令 ...

  7. java 注释,关键字和标识符

    注释 注释是为了防止当写代码的时间过久了之后,忘记了这行代码的意思或者是在一个大型的项目里面,不可能每一个模块的功能你都记得,所以需要一个注释来帮助记忆. 注释不会被执行 平时写代码一定要养成写注释的 ...

  8. (纯js)如何不刷新网页就能链接新的js文件

    如何不刷新网页就能链接新的js文件,其实在HTML语言中已经有相关的函数了,就是再添加一个<script src=.....></script>. 函数叫document.bo ...

  9. Crash course statistics

    Crash course statistics 01什么是统计学 描述性统计(Descriptive statistics) 推理统计可以得出之外的,基于"样本"的推论统计学来估计 ...

  10. 列出文件夹中分级目录java

    package test; import java.io.File; public class exportFileName { public static void main(String[] ar ...