数据结构——哈希表

数据结构——哈希表

刀刀

第一次结束哈希表是在数据结构课上,在讲查找的时候老师随便提了一下哈希表这个概念,最近在做聊天室的时候要用到哈希表,更加深入的理解了哈希表。


1.什么是HashMap?

先说说存储结构,实际上在我们学过的数据结构可以归结为两类:连续的的存储结构和不联系的存储结构,其代表分别为数组和链表。而我们学过的堆栈,队列,树,图,都可以用这两种结构来实现。连续的存储结构——数组,在数据的查找和修改上具有很好的优点,很方便,时间复杂度很小。但是在数据的增添和删除上则显得很麻烦,空间复杂度很大。而非连续,非顺序的存储结构——链表恰和数组相反,数据的增添和删除容易,空间复杂度很小,查找和修改复杂,时间复杂度很大。

那么有没有一种数据结构能折衷一下数组和链表的优缺点呢?那就是——哈希表,既满足了数据的查找和修改很容易,同时又不占用很多空间的特点。

哈希表是基于哈希函数的,哈希表中的元素是有哈希函数确定的,哈希表作为一种数据结构,我们用哈希表来存储数据,在保存的时候存入的是一个<key—value>的结构,value由哈希函数作用于key上得到。但是存在一个哈希冲突问题,那就是当你用hash函数作用在两个互不相同的key上,得到的value值相等。这就好比“一个班里面有两个叫做李洋的同学,老师上课的时候叫到李洋起来回答问题,这时就不知道是哪个李洋起来回答问题。”

因此在创建一个哈希表的时候要考虑两个方面的问题:

一. 构造一个好的哈希函数:所谓一个好的哈希函数指的就是,当用这个hash函数作用在不同的key时,所得到的value能够均匀的分布在hash表中,即能尽可能少的减少hash冲突。比较常见的hash函数的构造方法有:

  • 直接定址法
  • 数字分析法
  • 平方取中法
  • 折叠法
  • 除留余数法
  • 随机数法

这里就不再对这些方法一一阐述。

二. .hash冲突是不可能完全避免的,那么我们要考虑的还有就是当产生哈希冲突的时候,我们如何来解决。比较常见的hash冲突的解决方法有:

  • 开放定址法
  • 链地址法
  • 再哈希法

2.为什么要用HashMap?

首先,我们采用哈希表的初衷就是对连续的存储结构数组和非连续的存储结构链表,在数据的增删查改等操作上进行折衷。

我们不妨设想一下有这样的一个场景:

我们要设计一个数据表来保存用户信息,如果我们对用户信息有一个大致的估算为五万个,如果我们采用数组来保存的话,我们设计一个可以保存六万个用户的数组来作为数据表。我们在前面已经分析过了,如果用数组来保存,在数据的查询和修改方面相当方便,但是随着客户的增长,如果有一天客户增长到了五万零一个呢???这个时候我们就要建一个更大的数组来进行数据得迁徙。那如果用链表来进行存储呢?链表存储的话,在对用户的信息进行查询时,我们得从链表的第一个节点往后一个一个找,这个也是耗时耗力的。

所以这个地方我有一种思路,那就是用链地址这种哈希构造方法来创建一个哈希表。在数据的增删查改方法上可以做到相对的要好。

链地址法

我们可以发现上面这个由“链地址法”构造的哈希表是由数组+链表构成的。元素的存入方法可以这样简单的来描述:

首先我们创建一个容量为n的数组,让要存入的数据x对n取模,那么结果就存入对应的数组的下标中,当有一个元素要存入数组时,这个位置已经有一个元素了,那么我们就把这些哈希值相同的元素挂在已有的元素的后面生成一条链表。这就是对链地址法的哈希表的简单的描述。

OK,我们简单的介绍到这里,下面我们会介绍一下JDK中的HashMap,和自己来写一个我的HashMap,并在数据存储的效率方面进行一下比对。


3.实现我的HashMap

/*
 * Entry类,相当于定义了链表一个节点的结构。
 */

public class Entry<K, V> {
	Entry<K, V> next;  
	K key;
	V value;
	int hash;

	public Entry(K k, V v, int hash) {
		this.key = k;
		this.value = v;
		this.hash = hash;
	}

}

每个Entry对象包括key(键),value(值),next(Entry的引用,可以形成单链表,用于解决哈希冲突)以及hash(哈希值)。

/**
 * 哈希表的实现
 * 
 * @author ZhanHaoxin
 *
 */
public class MyHashMap<K, V> {

	private int size;// 当前容量
	private static int initialCapacity = 16; // 默认初始容量为16
	private static float loadFactor = 0.75f; // 默认装载因子为0.75
	private Entry<K, V>[] container; // 存储数据的数据表
	private int max; // 能存的最大数据量 等于装载因子和初始容量的乘积

	/*
	 * 使用默认参数的构造方法
	 */
	public MyHashMap() {
		this(initialCapacity, loadFactor);
	}

	/*
	 * 使用自定义参数的构造方法
	 */
	public MyHashMap(int Capacity, float factor) {
		if (Capacity < 0) {
			throw new IllegalArgumentException("容量有错:" + Capacity);
		}

		if (factor <= 0) {
			throw new IllegalArgumentException("装载因子有错: " + factor);
		}
		this.loadFactor = factor;
		max = (int) (loadFactor * Capacity);
		container = new Entry[Capacity];
		size = 0;
	}

	/*
	 * 实现数据 存 的功能
	 * 
	 */
	public boolean put(K k, V v) {
		// 因为取模运算要求均为整数运算,这里key值不一定是整形,
		//所以调用JDK的hashcode()方法
		// 取得key的hash值用来进行取模运算
		int hash = k.hashCode();
		// 将参数信息封装为一个entry,entry即为哈希表中的“桶”中的元素
		Entry<K, V> temp = new Entry(k, v, hash);
		if (setEntry(temp, container)) { // 如果哈希表中无此值便插入
			size++;
			return true;
		}
		return false;
	}

	/*
	 * 实现数据 取 的功能
	 */

	public V get(K k) {
		Entry<K, V> entry = null;
		// 计算K的hash值
		int hash = k.hashCode();
		// 根据hash值找到下标
		int index = indexFor(hash, container.length);
		// 根据index找到链表
		entry = container[index];
		// 若链表为空,返回null
		if (null == entry) {
			return null;
		}
		// 若不为空,遍历链表,比较k是否相等,如果k相等,则返回该value
		while (null != entry) {
			if (k == entry.key || entry.key.equals(k)) {
				return entry.value;
			}
			entry = entry.next;
		}
		// 如果遍历完了不相等,则返回空

		return null;
	}

	/*
	 * 将指定的节点temp添加到哈希表中 添加时判断该结点是否已经存在 
	 * 如果已经存在,返回false 添加成功返回true
	 */
	private boolean setEntry(Entry<K, V> temp, Entry<K, V>[] map) {
		// 根据hash值找到下标
		int index = indexFor(temp.hash, map.length);
		// 找到下标位置对应的元素
		Entry<K, V> entry = map[index];

		if (null != entry) { // 若元素存在则遍历整个链表,判断值是否相等
			while (null != entry) {
				// 判断值是否相等除了要判断值相等还要判断地址是否相等
				// 都相等的话就不存这个元素,返回false
				if ((temp.key == entry.key || temp.key.equals(entry.key))
					&& temp.hash == entry.hash
			        && (temp.value == entry.value || temp.value.equals(entry.value))) 
				{

					return false;

				} else if (temp.key == entry.key && temp.value != entry.value) {

					entry.value = temp.value;
					return true;
				} else if (temp.key != entry.key) { // 不相等则往由链表往下比较

					if (null == entry.next) {

						break; // 到达链尾则跳出循环
					}
					entry = entry.next; // 没到链尾则继续下一个元素
				}
			}
			// 此时遍历到了链尾还没相同的元素则把它挂在链尾
			addEntry2Container(entry, temp);
			return true;
		}
		// 若不存在,直接设置初始化元素
		setFirstEntry(index, temp, map);
		return true;
	}
	
     //桶中没有元素,把这个元素设为初始化元素
	private void setFirstEntry(int index, Entry<K, V> temp, Entry<K, V>[] map) {
		if (size > max) {
			reSize(map.length * 2);
		}
		map[index] = temp;
		temp.next = null;
	}

	//把hash值相同的元素挂在链表的尾部
	private void addEntry2Container(Entry<K, V> temp, Entry<K, V> entry) {
		if (size > max) {
			reSize(container.length * 2);
		}
		entry.next = temp;

	}

	/*
	 * 扩容的方法
	 */

	private void reSize(int newSize) {
		// 创建一个新的数组
		Entry<K, V>[] newMap = new Entry[newSize];
		max = (int) (loadFactor * newSize);
		// 将原来数组中的元素迁移到新数组中
		for (int i = 0; i < container.length; i++) {
			Entry<K, V> entry = container[i];
			// 因为“桶”是链表,所以还要用next把桶中的元素连接起来
			while (null != entry) {
				setEntry(entry, newMap);
				entry = entry.next;
			}
		}
		container = newMap;
	}

	/*
	 * 根据hashcode,容器数组长度,计算hashcode在容器数组中的下表值
	 */
	private int indexFor(int hashcode, int lengthOfContainer) {

		return hashcode & (lengthOfContainer - 1);
		// h & (length-1)就相当于h%length,用于计算index也就是在table数组中的下标
	}

}

我实现的HashMap的数据结构

其中table就是HashMap的核心,即为Entry数组,为数据结构图中绿色的部分;size 为HashMap中的Entry的数目,即数据结构中橙黄色部分的数目;loadFactor为加载因子,表示HashMap中元素的填满的程度。当加载因子大时,HashMap中的元素比较多,因而更容易产生哈希冲突;而当加载因子比较小时,HashMap中的元素比较少,会浪费空间。实时加载因子的计算方法为size/capacity,capacity即为 table数组的数目,均为2的n次幂。加载因子的默认值为0.75,即当实时加载因子到达0.75时,就会进行HashMap的扩容了。threshold表示当HashMap的size大于threshold时会执行哈希表的扩容即resize操作。 所以,其计算方法为 threshold = capacity * loadFactor。

这里要说明的几个点:

  • 我们知道HashMap是由数组+链表组成,那么HashMap存在的两个极端就是HashMap可能退化为了一个数组或者是一个链表。
  • HashMap的resize是一个十分消耗资源的过程,在此基础上,就要求我们在哈希表的创建之初对数据的数量有一个良好的估计,还有就是rehash方法的优化。
  • Hash表的resize方法,在这里我们可以简单的理解为原有的桶不装不下这些数据了,还需要更多的桶,那么就是在hash函数中,我们还需要更大的模,产生更多的结果。

HashMap方法操作的核心是找到key值所在的桶,然后便是按照单链表的操作进行查找、插入、删除或者其它操作了

put 方法主要进行以下4个步骤

1、判断key是否是null,是null的话单独处理,因为key为null总会将数据存储在第一个桶里;

2、计算key的哈希值,并寻找到要存放该key的桶;

3、判断桶内是否有该key,如果有的话,将老数据进行覆盖;

4、将该key的相关数据添加到该桶里。

get 方法要比 put 方法简单很多,主要流程为

1、判断key是否是null,是null的话单独处理,因为key为null总会将数据存储在第一个桶里;

2、计算key的哈希值,并寻找到要存放该key的桶;

3、寻找桶内是否有该key的Entry,如果有,返回其value值,如果没有,返回null。

在我的代码里只实现了哈希表的存和取的方法,其他的方法之后会更新,后面也会分析一下:

  • HashMap和HashTable的区别。
  • 一致性哈希。
  • JDK中HashMap源码解读

4.我的HashMap和JDK中的HashMap在存取效率上的比较

/**
 * 测试我的哈希表的存取速度
 *
 */
public class testMyHashMap {

	public static void main(String[] args) {

		MyHashMap<String, String> testmap = new MyHashMap<String, String>();
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			testmap.put("user" + i, "password" + i);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("MyHashMap Insert Time:" + (endTime - startTime));

		Long BeginTime = System.currentTimeMillis();// 记录BeginTime
		testmap.get("user" + 9999);
		Long EndTime = System.currentTimeMillis();// 记录EndTime
		System.out.println("MyHashMap seach time:" + (EndTime - BeginTime));
	}

}

结果为:

/*
 * JDK中哈希表的存取速度
 */
import java.util.HashMap;
import java.util.Map;

public class TestJDK {
	public static void main(String[] args) {
		HashMap<String, String> map = new HashMap<String, String>();
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			map.put("user" + i, "password" + i);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("JDK HashMap Insert Time:" + (endTime - startTime));
		
		
		Long BeginTime = System.currentTimeMillis();// 记录BeginTime
		map.get("user" + 9999);
		Long EndTime = System.currentTimeMillis();// 记录EndTime
		System.out.println("JDK HashMap seach time:" + (EndTime - BeginTime));
	}

}

结果为:

为什么我写的哈希表存入的速度比JDK的快呢???

下次带你一起解读源码!!!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wangchuang2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值