【面试题之】:手撕HashMap

前几天字节二面,面试官要求自己手写一个HashMap,当时就有些被惊到了,虽然读过HashMap的源码,也能侃侃而谈,但是说的和做的难度还是不一样的,当时吹了一会HashMap之后,面试官淡淡一笑,说:“好的,那写一下吧,,,,”。看来还是逃不过啊,最后写的不太好,所以说,大家以后还是要动起手来啊!

首先HashMap的底层就是一个数组,数组里面保存了Key和Value,以及一个next指针,如果没有哈希冲突的话,next指针就是null,如果有哈希冲突,那么就会形成链表,指向下一个元素。这里我们首先要了解,新版本的HashMap底层已经不只是数组+链表了,而是数组+链表+红黑树,并且已经由原来的“头插法”,改成了“尾插法”。这里我们暂时不讨论新版本的实现,而是使用旧版本的HashMap实现,也就是数组+链表+“头插法”。

下面首先给出代码:

首先创建一个接口,就是定义一个规范,接口里面主要定义了put和get方法。以及Entry类型的getKey和getValue方法(Entry类型实际上就是数组类型)。

package Hash;

//接口,相当于规范,里面定义了map和entry
//entry里面完成getKey和getValue,map里面完成put和get
public interface MyMap<K,V>{
	public V put(K key,V value);
	public V get(K key);
	interface Entry<K,V>{
		public K getKey();
		public V getValue();
	}
}

然后,就是去实现这个接口。(当然,我们也可以不用这样实现接口的方法,就直接写一个类来完成)

package Hash;

import java.util.ArrayList;
import java.util.List;




//手撕hashmap
public class MyHashMap <K,V> implements MyMap<K,V>{
		//内部的属性
		private static final int DEFAULT_INITIAL_CAPACITY=1<<4;//默认大小
		private static final float DEFAULT_LOAD_FACTOR=0.75f;
		private int capacity;
		private float load_factor=0.75f;
		private int entryUseSize;//已经使用的entry的数量
		private Entry<K,V>[] table =null;//entry类型的数组
		
		//构造函数
		public MyHashMap(){  this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);}
		
		public MyHashMap(int initial_capacity,float load_factor){
			if(initial_capacity<0)
				throw new IllegalArgumentException("!!!");
			if(load_factor<=0||Float.isNaN(load_factor))
				throw new IllegalArgumentException("!!");
			this.capacity=initial_capacity;
			this.load_factor=load_factor;
			table=new Entry[this.capacity];
			
		}
		
		class Entry<K, V> implements MyMap.Entry<K, V> { 
	       private  K key;
	       private V value;
	       private Entry<K, V> next;
	        Entry(K k, V v, Entry<K, V> next) {
	            this.key = k;
	            this.value = v;
	            this.next = next;
	        }
	 
	        public K getKey() {
	            return key;
	        }
	 
	        public V getValue() {
	            return value;
	        }
	 
	     
	    }
		@Override
		public V put(K key,V value){
			V oldNum=null;//需要返回的旧的数
			
			if(this.entryUseSize>=this.capacity*this.load_factor)//需要扩容
				resize(2*this.capacity);//扩容以及rehash
			int index=hash(key)&(this.capacity-1);//利用hash函数再散列一次
			//如果是空直接放进去
			if(table[index]==null)
			{
				table[index]=new Entry<>(key,value,null); 
				++this.entryUseSize;
			}
			else{//需要遍历链表
				Entry<K,V> entry=table[index];
				Entry<K,V> e=entry;
				while(e!=null)
				{
					if(e.getKey()==key||key.equals(e.getKey()))
					{
						oldNum=e.value;
						e.value=value;
						return oldNum;
					}
					e=e.next;
				}
				//如果没有,使用头插法
				table[index]=new Entry<K,V> (key,value,entry);
				++this.entryUseSize;
			}
			return oldNum;
		}

		@Override
		public V get(K key) {	
			int index=hash(key)&(this.capacity-1);
			if(table[index]==null)
				return null;
			else{
				Entry<K,V> entry=table[index];
				do{
					if(key==entry.getKey()||key.equals(entry.getKey()))
						return entry.getValue();
					entry=entry.next;	
				}while(entry!=null);
			}
			return null;
		}
		
		//根据hashcode来计算散列值
		private int hash(K key){
			int h=0;
		return (key==null)?0:(h=key.hashCode())^(h>>16);
			
			
		}
		//重新划分数组大小,然后把原来的里面的元素都放入新数组,参数newSize表示新的容量
		private void resize(int newSize){
			Entry<K,V>[] newTable=new Entry[newSize];
			this.capacity=newSize;
			this.entryUseSize=0;
			rehash(newTable);
		}
		
		//把原来的数全放到新的输入数组里面
		private void rehash(Entry<K,V>[] newTable){
			List<Entry<K,V>> list=new ArrayList<Entry<K,V>>();
			for(Entry<K,V> entry:this.table)//遍历数组里面的每个元素
			{
				if(entry!=null){
					do{//遍历链表里面的每个元素
						list.add(entry);
						entry=entry.next;
						
					}while(entry!=null);	
				}
			}
			if(newTable.length>0){
				this.table=newTable;//指向新的数组
			}
			for(Entry<K,V> entry:list){
				//调用put函数,把刚才放入list里面的数据放到新的table里
				put(entry.getKey(),entry.getValue());
			}	
		}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyHashMap<String,String> hash=new MyHashMap<String,String>();
		for(int i=0;i<100;i++)
		{
			hash.put("nihao"+i, "buhao"+i);
		}
		for(int i=0;i<100;i++)
		{
			System.out.println(hash.get("nihao"+i));
		}
}

}

最后的输出:

 这个里面当然最主要的就是put和get方法,注释都帮大家准备好了,简单概括一下吧:

1、put函数,首先判断需不需要扩容,判断的标准就是,当前的容量*负载因子是不是大于当前已经使用的容量,如果大于了,就要执行resize方法,这个方法会创建一个新的数组,新数组的容量是原来的2倍大小。然后把原来链表里面的元素放到一个容器里面,最后把容器里面的数倒出来,在put到新的数组里面。

如果不需要扩容,那么会利用hash方法,进行一次再散列,关于再散列的原因,以及理论这里不细讲了,想知道的话可以看看我的另外一篇博客:hash方法。然后判断当前的Entry数组元素之前有没有被占据,没有直接把这个“坑”占掉,否则,进行链表的遍历,找到了直接修改,没找到,就把元素放在原来头结点的前面(头插法),最后返回旧元素的数值。

2、get方法:依旧是首先利用hash方法对hashcode的值进行二次哈希,然后计算出数组的下标,后面的其实和put差不多,就不细讲了。

扫描二维码关注公众号,回复: 11599845 查看本文章

 

猜你喜欢

转载自blog.csdn.net/qq_35590091/article/details/107490669
今日推荐