【一起来学Java数据结构】——哈希表

【一起来学Java数据结构】——哈希表

hashMap的实现

在jvm内部中 ,hashMap是使用数组加链表的方式来实现的,也叫做哈希桶

hashmap的结构

下面是它内部的成员变量

static class Node{
		public int key;
		public int val;
		public Node nextNode;
		public Node(int key,int val) {
			this.key=key;
			this.val=val;
		}
	}
	public Node[] nodes;
	public int usedsize;
	public static final double DEFAULT_LOAD_FACTOR=0.75;//负载因子
	

hashmap的插入元素

在这之前,我们一定要明白一件事情。当我们在hashMap中插入的key是一个自定义类型的时候,在该自定义类型内部一定要重写equals和hashcode函数。

hashcode是判断key对应的整数值

equals是判断在同一个hashcode下的元素是否相同。

所以,hashcode相同的,equals不一定相同。equals相同,hashcode一定相同。

class Person{
	public String name;
	public int ID;
	@Override
	public int hashCode() {
		return Objects.hash(ID, name);
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		return ID == other.ID && Objects.equals(name, other.name);
	}
	
}

下面是具体的插入过程:

  1. 先计算key的hashcode值,并找到应放入的下标
  2. 在查看在该下标中是否已经存在该值
  3. 如果存在了,就更新该值
  4. 如果没有存在就使用头插或尾插来插入
  5. 计算此时的负载因子,看是否超过指定的值
  6. 如果超过了就扩容
	//插入元素
	public void put(K key,V val) {
		int hash=key.hashCode();
		//先找到相应的下标
		int index=hash%this.nodes.length;
		//看该下标下是否已经有了该值
		//有了该值,就更新,插入操作就已经完成
		Node curNode=this.nodes[index];
		while(curNode!=null) {
			if(curNode.equals(key)) {
				curNode.val=val;
				return;
			}
			curNode=curNode.nextNode;
		}
		//如果没有找到该值的话,就头插
		Node node=new Node(key, val);
		node.nextNode=this.nodes[index];
		this.nodes[index]=node;
		this.usedsize++;
		//插入之后计算一下负载因子:如果大于就增加数组的长度并重新分配空间
		if(loadFactor()>=DEFAULT_LOAD_FACTOR)
			resize();//进行扩容
	}
	private double loadFactor() {
		return 1.0*this.usedsize/this.nodes.length;
	}

下面来介绍一下扩容函数:

  1. 将新的数组的长度变为现数组长度的两倍。

  2. 遍历老数组,将老数组上面的值全部更新到新数组中去

  3. 将新数组赋予老数组

	private void resize() {
		Node[] newNodes=new Node[this.nodes.length*2];
		for(Node node:this.nodes) {
			Node curNode=node;
			while(curNode!=null){
				//先计算出来新的坐标
				int hash=curNode.hashCode();
				int newIndex=hash%newNodes.length;
				//记录一下该节点后面的节点
				Node tmpNode=curNode.nextNode;
				//头插法
				curNode.nextNode=newNodes[newIndex];
				newNodes[newIndex]=curNode;
				//更新curNode节点
				curNode=tmpNode;
			}
			
		} 
		//最后,让nodes等于我们新扩容的newNodes
		this.nodes=newNodes;
		
	}

hashMap的get函数

该函数十分简单,就是遍历一下hashMap看是否可以找到该值就可以。

	public V get(K key) {
		for(Node node:this.nodes) {
			Node curNode=node;
			while(curNode!=null) {
				if(curNode.equals(key))
					return (V)curNode.val;
				curNode=curNode.nextNode;
			}
				
		}
		return null;
	}

总体代码

import java.util.Objects;



class Person{
	public String name;
	public int ID;
	@Override
	public int hashCode() {
		return Objects.hash(ID, name);
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		return ID == other.ID && Objects.equals(name, other.name);
	}
	
}
public class HashBuck2<K,V> {
	static class Node<K,V>{
		public K key;
		public V val;
		public Node nextNode;
		public Node(K key,V val) {
			this.key=key;
			this.val=val;
		}
	}
	public Node<K,V>[] nodes=(Node<K,V>[])new Node[10];
	public int usedsize;
	public static final double DEFAULT_LOAD_FACTOR=0.7;
	//插入元素
	public void put(K key,V val) {
		int hash=key.hashCode();
		//先找到相应的下标
		int index=hash%this.nodes.length;
		//看该下标下是否已经有了该值
		//有了该值,就更新,插入操作就已经完成
		Node curNode=this.nodes[index];
		while(curNode!=null) {
			if(curNode.key.equals(key)) {
				curNode.val=val;
				return;
			}
			curNode=curNode.nextNode;
		}
		//如果没有找到该值的话,就头插
		Node node=new Node(key, val);
		node.nextNode=this.nodes[index];
		this.nodes[index]=node;
		this.usedsize++;
		//插入之后计算一下负载因子:如果大于就增加数组的长度并重新分配空间
		if(loadFactor()>=DEFAULT_LOAD_FACTOR)
			resize();//进行扩容
	}
	private double loadFactor() {
		return 1.0*this.usedsize/this.nodes.length;
	}
	public V get(K key) {
		for(Node node:this.nodes) {
			Node curNode=node;
			while(curNode!=null) {
				if(curNode.key.equals(key))
					return (V)curNode.val;
				curNode=curNode.nextNode;
			}
				
		}
		return null;
	}
	private void resize() {
		Node[] newNodes=new Node[this.nodes.length*2];
		for(Node node:this.nodes) {
			Node curNode=node;
			while(curNode!=null){
				//先计算出来新的坐标
				int hash=curNode.hashCode();
				int newIndex=hash%newNodes.length;
				//记录一下该节点后面的节点
				Node tmpNode=curNode.nextNode;
				//头插法
				curNode.nextNode=newNodes[newIndex];
				newNodes[newIndex]=curNode;
				//更新curNode节点
				curNode=tmpNode;
			}
			
		} 
		//最后,让nodes等于我们新扩容的newNodes
		this.nodes=newNodes;
		
	}

}

hashMap的源码

声明的变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  最开始的容量必须是2的幂数,其实也是有道理的,因为左移都是以2的倍数为单位的
static final int MAXIMUM_CAPACITY = 1 << 30;//最大的容量只可以是2^30
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;//当链表的长度超过8且总容量大于64的时候就树化
static final int UNTREEIFY_THRESHOLD = 6;//从红黑树转到链表的条件是红黑树的节点的个数小于6个的时候

hashMap的构造函数

构造函数1

具有初始化容量和负载因子

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

构造函数2

具有初始容量

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

构造函数3

无参数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

hash函数

image-20220303090121818

第一步:先是调用hashCode函数得到h值。这是一个32位的数,但是我们的数组只是有16位,太大了。所以在第二步进行一些处理

第二步:将h和h>>>16进行异或,这样就增加了低位的随机性,使得数据不会聚集在一起。

第三步:就是hash值和数组长度取模的过程。因为数组长度总是2的幂数,且与的效率比取余的效率高,所以就使用(n-1)和hansh值相与

put函数

这里我们需要有一个小问题,就是什么时候进行数组的开辟。

事实上,在我们进行构造函数的时候,并没有对数组进行开辟。

通过看put函数的源码,我们可以看到:

 if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

只有在插入第一个元素的时候,才会有数组的开辟

hashMap的面试题

  1. 如果new HashMap(19),那么hashMap的数组开辟的空间是多大?

    开辟的空间是最接近19且大于19的2的幂数,也就是25

  2. hashMap如何扩容,怎么扩容?

    当小于负载因子的时候进行扩容,以两倍的方式进行扩容

  3. 查找成功和查找不成功的平均查找长度

    image-20220303183302545

查找成功的长度:整个的比较次数相加/关键字的个数

查找不成功的长度:对于每一个元素都要进行比较。对于没有值的元素就是1,有值的就是要看出现经过多少次操作出现空位。

然后再除以整个数组的长度。

​ 4.待解决的问题

image-20220228215911082

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值