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
public interface MyMap<K,V> { public V put (K k, V v); public V get(K k); public int size(); V remove(Object key); void putAll(Map<? extends K, ? extends V> m); void clear(); public interface Entry<K, V>{
public K getKey(); public V getValue(K k); V setValue(V value);
}
}
下面去写实现类myHashMap,实现myMap接口。
在上面定义的myMap中有一个内部的接口Entry,而实现类中有一个内部类叫Entry实现类myMap中的接口Entry,而Entry中需要定义key,value,还有一个Entry类型的next的指针,指向下一个链表数据。
class Entry<K, V> implements MyMap.Entry<K, V>{ final K k; V v; //next指针,指向下一个链表数据
Entry<K, V> next; @Override
public K getKey() {
// TODO Auto-generated method stub
return null;
} @Override
public V getValue(K k) {
// TODO Auto-generated method stub
return null;
} @Override
public V setValue(V value) {
// TODO Auto-generated method stub
return null;
} }
下面去实现接口中定义的方法。
定义好Entry中的构造器。
//构造器
public Entry(K k, V v, Entry<K, V> next) {
super();
this.k = k;
this.v = v;
this.next = next;
}
实现getkey,getValue,setValue。
@Override
public K getKey() {
return k;
} @Override
public V getValue(K k) {
return v;
} @Override
public V setValue(V value) {
V oldvale = v;
v = value;
return oldvale;
}
Entry中的方法实现以后去实现myHashMap的方法。
实现前先将变量定义好,一个数组的默认长度,一个负载因子,一个存放Entry的数组,一个数组中默认存放元素的个数。
//默认数组长度
private static int dafaultlength = 16; //负载因子
private static double defaultLoad = 0.75; //存放Entry对象的数组
private Entry<K, V>[] table = null; //数组中存储元素个数
private int size = 0;
实现put方法前先将hash算法定义好。
/**
* 定义hash算法,除留余数法
* @return hash值
*/
private int getIndex(K k){ int m = dafaultlength;
//因为hashCode有可能是一个负数,所以结果有可能是一个负数
int indext = k.hashCode() % m; return indext >= 0 ? indext : -indext;
}
在我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。但是扩容就存在一个问题,我们是否要对之前老表的数据再hash呢?答案是肯定的。所以这里还需要一个再hash的方法。
/**
* 递归取单向链表的元素
* @param entry
* @param list
*/
private void findEntryByNext (Entry<K,V> entry, List<Entry<K, V>> list){
if (entry != null && entry.next == null){
list.add(entry);
findEntryByNext(entry.next, list);
} else {
list.add(entry);
}
}
/**
* 数组扩容时,再hash
*/
private void reHash (Entry<K, V>[] newTable){
List<Entry<K, V>> list = new ArrayList<Entry<K,V>>();
//for循环取线性表的数据
for (int i = 0; i < table.length; i++) {
if (table[i] == null){
continue;
}
//递归取单向链表的元素
findEntryByNext(table[i], list);
} //再hash
if (list.size() > 0){
size = 0;
defaultlength = defaultlength * 2;
table = newTable; for (Entry<K, V> entry : list) {
if (entry.next != null){
entry.next = null;
}
put(entry.getKey(), entry.getValue());
}
}
}
/**
* 数组扩容:
* 我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一
* 个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,
* 所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。
*/
private void up2size (){
Entry<K, V>[] newTable = new Entry[2 * defaultlength];
reHash(newTable);
}
实现put方法:
1.根据k得到index
2.将index位置的元素拿出来
3.判断该位置是否有元素存在,如果这个位置不存在元素,直接将值存入,而这里的entry为null,如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。
@Override
public V put(K k, V v) {
//1.根据k得到index
int index = getIndex(k);
//2.将index位置的元素拿出来
Entry<K, V> entry = table[index];
//3.判断该位置是否有元素存在
if (entry == null){
//如果这个位置不存在元素,直接将值存入,而这里的entry为null
table[index] = newEntry(k, v, entry);
size ++;
} else {
//如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。
table[index] = newEntry(k, v, entry);
}
return v;
}
实现get方法
首先通过hash值获取线性表的位置,然后通过比较获取单向链表中的Enery对象
/**
* 递归获取单向链表中的Entry对象
* @param k
* @param entry
* @return
*/
private V findValueEqualKey (K k, Entry<K, V> entry){
if (k == entry.getKey() || k.equals(entry.getKey())){
return entry.getValue();
}
if (entry.next != null){
findValueEqualKey(k, entry.next);
}
return null;
}
@Override
public V get(K k) {
int index = getIndex(k);
if (table[index] == null){
return null;
}
return findValueEqualKey(k, table[index]);
}
实现remove方法:还是通过index查找到单向链表,然后递归找到删除元素的上一个元素,将上一个元素的next指向该元素的后一个元素。
@Override
public V remove(K k) {
int index = getIndex(k);
if (table[index] == null){
return null;
}
if (k == table[index].getKey() || k.equals(table[index].getKey())){
table[index] = table[index].next;
return table[index].getValue();
}
Entry<K, V> entry = findMoveEntry(k, table[index]);
if (entry == null){
return null;
}
Entry<K, V> moveEntry = entry.next;
entry.next = entry.next.next;
return moveEntry.getValue();
} private Entry<K, V> findMoveEntry(K k, Entry<K, V> entry){
if (entry.next == null){
return null;
}
if (k == entry.next.getKey() || k.equals(entry.next.getKey())){
return entry;
}
findMoveEntry(k, entry.next);
return null;
}
看了上面的方法,其实对hashMap的结构有了一定的了解,在hashMap中还有一些其他方法,就不在这一一实现了。
最后附上实现类中的全部代码。
package hash; import java.util.ArrayList;
import java.util.List;
import java.util.Map; public class MyHashMap<K, V> implements MyMap<K, V> { //默认数组长度
private static int defaultlength = 16; //负载因子
private static double defaultLoad = 0.75; //存放Entry对象的数组
private Entry<K, V>[] table = null; //数组中存储元素个数
private int size = 0; public void MyHashMap(int defaultlength, double defaultLoad){
defaultlength = this.defaultlength;
defaultLoad = this.defaultLoad;
table = new Entry[defaultlength];
} public MyHashMap (){
this.MyHashMap(defaultlength, defaultLoad);
} @Override
public V put(K k, V v) {
//数组扩容
if (size > defaultlength * defaultLoad){
up2size();
}
//1.根据k得到index
int index = getIndex(k);
//2.将index位置的元素拿出来
Entry<K, V> entry = table[index];
//3.判断该位置是否有元素存在
if (entry == null){
//如果这个位置不存在元素,直接将值存入,而这里的entry为null
table[index] = newEntry(k, v, entry);
size ++;
} else {
//如果这个位置存在元素,则这个entry对象将老元素位置占用,并且next指向存在的老元素。
table[index] = newEntry(k, v, entry);
}
return v;
} /**
* 数组扩容:
* 我们存数据时,如果我们的数组长度一直默认为16的话势必会造成线性表上的每一
* 个位置上都会存在hash冲突,而每一个位置的单向链表就会特别长,
* 所以我们应该定义一个负载因子,当达到这个负载因子之后数组扩容。
*/
private void up2size (){
Entry<K, V>[] newTable = new Entry[2 * defaultlength];
reHash(newTable);
} /**
* 数组扩容时,再hash
*/
private void reHash (Entry<K, V>[] newTable){
List<Entry<K, V>> list = new ArrayList<Entry<K,V>>();
//for循环取线性表的数据
for (int i = 0; i < table.length; i++) {
if (table[i] == null){
continue;
}
//递归取单向链表的元素
findEntryByNext(table[i], list);
} //再hash
if (list.size() > 0){
size = 0;
defaultlength = defaultlength * 2;
table = newTable; for (Entry<K, V> entry : list) {
if (entry.next != null){
entry.next = null;
}
put(entry.getKey(), entry.getValue());
}
}
} /**
* 递归取单向链表的元素
* @param entry
* @param list
*/
private void findEntryByNext (Entry<K,V> entry, List<Entry<K, V>> list){
if (entry != null && entry.next == null){
list.add(entry);
findEntryByNext(entry.next, list);
} else {
list.add(entry);
}
} /**
* 获取Entry方法
* @param k
* @param v
* @param next
* @return
*/
private Entry<K, V> newEntry (K k, V v, Entry<K, V> next){
return new Entry<K, V>(k, v, next);
} /**
* 定义hash算法,除留余数法
* @return hash值
*/
private int getIndex(K k){ int m = defaultlength;
//因为hashCode有可能是一个负数,所以结果有可能是一个负数
int indext = k.hashCode() % m; return indext >= 0 ? indext : -indext;
} @Override
public V get(K k) {
int index = getIndex(k);
if (table[index] == null){
return null;
}
return findValueEqualKey(k, table[index]);
} /**
* 递归获取单向链表中的Entry对象
* @param k
* @param entry
* @return
*/
private V findValueEqualKey (K k, Entry<K, V> entry){
if (k == entry.getKey() || k.equals(entry.getKey())){
return entry.getValue();
}
if (entry.next != null){
findValueEqualKey(k, entry.next);
}
return null;
} @Override
public int size() {
return size;
} @Override
public V remove(K k) {
int index = getIndex(k);
if (table[index] == null){
return null;
}
if (k == table[index].getKey() || k.equals(table[index].getKey())){
table[index] = table[index].next;
return table[index].getValue();
}
Entry<K, V> entry = findMoveEntry(k, table[index]);
if (entry == null){
return null;
}
Entry<K, V> moveEntry = entry.next;
entry.next = entry.next.next;
return moveEntry.getValue();
} private Entry<K, V> findMoveEntry(K k, Entry<K, V> entry){
if (entry.next == null){
return null;
}
if (k == entry.next.getKey() || k.equals(entry.next.getKey())){
return entry;
}
findMoveEntry(k, entry.next);
return null;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
} @Override
public void clear() {
Entry<K, V>[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
} class Entry<K, V> implements MyMap.Entry<K, V>{ final K k; V v; //next指针,指向下一个链表数据
Entry<K, V> next; //构造器
public Entry(K k, V v, Entry<K, V> next) {
super();
this.k = k;
this.v = v;
this.next = next;
} @Override
public K getKey() {
return k;
} @Override
public V getValue() {
return v;
} @Override
public V setValue(V value) {
V oldvale = v;
v = value;
return oldvale;
} }
}
hashMap原理剖析的更多相关文章
- Java基础-hashMap原理剖析
Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...
- ARouter原理剖析及手动实现
ARouter原理剖析及手动实现 前言 路由跳转在项目中用了一段时间了,最近对Android中的ARouter路由原理也是研究了一番,于是就给大家分享一下自己的心得体会,并教大家如何实现一款简易的路由 ...
- Spring 中常用注解原理剖析
前言 Spring 框架核心组件之一是 IOC,IOC 则管理 Bean 的创建和 Bean 之间的依赖注入,对于 Bean 的创建可以通过在 XML 里面使用 <bean/> 标签来配置 ...
- short URL 短网址实现原理剖析
short URL 短网址实现原理剖析 意义,简短便于分享,避免出现超长 URL 的字符长度限制问题 原理分析, 使用 HashMap 存储对应的映射关系 (长度不超过7的字符串,由大小写字母加数字共 ...
- ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件)
ASP.NET Core 运行原理剖析2:Startup 和 Middleware(中间件) Startup Class 1.Startup Constructor(构造函数) 2.Configure ...
- ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行
ASP.NET Core 运行原理剖析1:初始化WebApp模版并运行 核心框架 ASP.NET Core APP 创建与运行 总结 之前两篇文章简析.NET Core 以及与 .NET Framew ...
- 【Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析】
原文:[Xamarin挖墙脚系列:Xamarin.IOS机制原理剖析] [注意:]团队里总是有人反映卸载Xamarin,清理不完全.之前写过如何完全卸载清理剩余的文件.今天写了Windows下的批命令 ...
- 【Xamarin 跨平台机制原理剖析】
原文:[Xamarin 跨平台机制原理剖析] [看了请推荐,推荐满100后,将发补丁地址] Xamarin项目从喊口号到现在,好几个年头了,在内地没有火起来,原因无非有三,1.授权费贵 2.贵 3.原 ...
- iPhone/Mac Objective-C内存管理教程和原理剖析
http://www.cocoachina.com/bbs/read.php?tid-15963.html 版权声明 此文版权归作者Vince Yuan (vince.yuan#gmail.com)所 ...
随机推荐
- [译]Quartz 框架 教程(中文版)2.2.x 之第二课 Quartz API,Jobs和Triggers简介
第二课:QuartzAPI,Jobs和Triggers简介 Quartz API Quartz API 关键的几个接口: Scheduler:跟任务调度相关的最主要的API接口. Job:你期望任务调 ...
- Stat3—因子分析(Factor Analysis)
题注:主成分分析分析与因子分析也有不同,主成分分析仅仅是变量变换,而因子分析需要构造因子模型.主成分分析:原始变量的线性组合表示新的综合变量,即主成分:因子分析:潜在的假想变量和随机影响变量的线性组合 ...
- Tomcat面试题目
1.tomcat给你你怎样去调优? 1. JVM参数调优:-Xms<size> 表示JVM初始化堆的大小,-Xmx<size>表示JVM堆的最大值.这两个值的大小一般根据需要进 ...
- 洛谷 1.5.1 Number Triangles 数字金字塔
Description 考虑在下面被显示的数字金字塔. 写一个程序来计算从最高点开始在底部任意处结束的路径经过数字的和的最大. 每一步可以走到左下方的点也可以到达右下方的点. 7 3 8 8 1 0 ...
- Django中的MiddleWare中间件
1. middleware简介 Django的middleware的概念相当于SSH框架里面的filter的概念.中间键的作用就是对所有的request,在request前,和在response后做一 ...
- tf.name_scope tf.variable_scope学习
1. 首先看看比较简单的 tf.name_scope(‘scope_name’). tf.name_scope 主要结合 tf.Variable() 来使用,方便参数命名管理. ''' Signatu ...
- NEO发行资产Token
NEO注册发行全局资产(Token 和 Share)功能已经在neo-gui里面集成,发行非常方便, 高级-注册资产 注册Token消耗GAS感人 4990 Gas 点击调用,获取交易ID为资产ID ...
- Windows降权
使用invoke-tokenmanipulation进行降权 枚举所有令牌 PS C:\Users\SMC> Get-ExecutionPolicy Restricted PS C:\Users ...
- wget下载整个网站或特定目录
下载整个网站或特定目录 wget -c -k -r -np -p http://www.yoursite.com/path -c, –continue 断点下载 -k, –convert-links ...
- MongoDB之conf配置文件详解(五)
详细看一下mongodb配置文件. mongodb.conf # mongodb.conf # 数据库文件位置 dbpath=/var/lib/mongodb #日志文件的路径 logpath=/va ...