`
大_圣
  • 浏览: 17053 次
  • 性别: Icon_minigender_1
  • 来自: 湖南
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

Hash表的理解以及实现

 
阅读更多

1. 理解

为每个要被存储的对象给定一个关键字,用一个Hash函数,把这个关键字映射到一个存储单元的地址. 这样, 在查找这个对象的时候, 只需要知道该对象的关键字. 再通过Hash函数, 便可以直接到该地址下的内存单元中去寻找所需要的数据.

但是,这当中又存在一个问题.. 对于每个不同的关键字. 通过Hash函数得到的地址是不是绝对不一样 ? 我是不知道会不会绝对不一样.. 但是数学家们说不同的关键字通过Hash函数也会有可能得到一样内存地址(胡*总说的好, 数学家说什么你就得信什么).

于是又出现一个问题: 解决Hash冲突. 

解决Hash冲突的方法:1)拉链法;

       2)开放定址法;

       3)双Hash函数法;

......

ps:(1) 拉链法: 即不同对象的关键字通过Hash函数得到的内存地址的值如果是一样的的话, 就将这两个(或多个)对象存储在一条线性链表中


 

 

 如图:{dt1,dt8}, {dt4, dt7}, {dt3, dt6, dt10}, {dt2, dt9}通过Hash函数算得的地址值是一样的, 故它们分别用一条链接起来, 可以看出, 该表中的数组里的每个元素其实是一个链表的表头.

 

(2) 开放定址法: 就是通过Hash函数算得的地址如果是一样的话, 就往该地址之后的存储空间去寻找, 只要找到有空间可以存储, 就把该数据放到该空间里存储起来 (线性探查法; 平方探查法)

 

(3) 双Hash函数法: 即给定两个Hash函数, 当通过第一个Hash函数得到的地址与其他数据地址冲突时, 将得到的值通过另外一个Hash函数再得到一个地址值, 用来尽量避免冲突.(可以扩展到多Hash函数)

 

  不难看出, 一个Hash表的存储性能与其Hash函数有着很密切的联系

而Hash函数又有多种构造方法:1) 直接定址法;

  2) 除留余数法;

  3) 数字分析法

   ........;

ps: (1) 直接定址法: 就是通过各个要被存储的数据的关键字本身或者加上某个常量值作为地址(个人觉得: 如果一个Hash表通过这样的方法来构造, 我还是直接显式的用数组算了).

 

      (2) 除留余数法: 以各个数据的关键字除以某个不大于Hash表长度的数, 得到的余数作为该数据的Hash地址.

     

      (3) 数字分析法: ...  这个就是得看具体问题了.

 

2. 实现(1):

 

首先,是Hash函数. 开始我是采用的取余的方法; 当存储的数据总量达到Map的0.75的时候,就开始扩容,每次扩大为原来的两倍. 话不多说, 上代码:

 

 

public class HashTest<K, V> {

	// 记录Map的长度
	private static int size;
	// 存放数据的数组
	private Node[] nodeArray;
	// 初始容量为11
	private static int CAPACITY = 11;
	// 数组中存放的数据为数组总长度的0.75则扩容
	private static float LOAD_FACTORY = 0.75f;

	/**
	 * @param Capacity
	 *            指定Map容量
	 * @param Factory
	 *            构造因子
	 */
	HashTest(int Capacity, float Factory) {
		if (Capacity < 0)
			throw new IllegalArgumentException("Illegal initial capacity: "
					+ Capacity);
		if (Factory <= 0 || Float.isNaN(Factory))
			throw new IllegalArgumentException("Illegal load factor: "
					+ Factory);
		this.CAPACITY = Capacity;
		this.LOAD_FACTORY = Factory;
		size = 0;
		nodeArray = new Node[CAPACITY]; // 以该容量为长度创建结点数组
	}

	/**
	 * 无参构造器
	 */
	HashTest() {
		this(CAPACITY, LOAD_FACTORY);
	}

	// 以取模得到的值为下标
	public void put(K Key, V Value) {
		size++;
		Node node = new Node<K, V>(Key, Value);
		node.hash = Key.hashCode();
		int index = Math.abs(node.hash % CAPACITY);
		// 如果放在数组中的此位置原来没有元素时, 加在本位置
		if (nodeArray[index] == null) {
			nodeArray[index] = node;
		} else {// 如果原来有元素, 则把本位置的元素替换成新加的元素, 原来在此位置的元素链接到新加元素后
			node.next = nodeArray[index];
			nodeArray[index] = node;
		}
		if ((float) size / (float) CAPACITY > LOAD_FACTORY) {
			CAPACITY = 2 * CAPACITY;// 更新Map容量
			extend(CAPACITY);// 判断是否扩容
		}
	}

	// 根据Key查找映射当中Value值的方法
	public V get(K Key) {
		int index = Math.abs(Key.hashCode() % CAPACITY);
		Node node = null;
		V reuslt = null;
		for (node = nodeArray[index]; node != null; node = node.next) {
			if (Key.equals(node.k)) {
				reuslt = (V) node.v;
				break;
			}
		}
		return reuslt;
	}

	// 扩容的方法
	public void extend(int Capacity) {
		Node[] newArray = new Node[Capacity];
		for (int i = 0; i < nodeArray.length; i++) {
			if (nodeArray[i] != null) {
				Node n = nodeArray[i];
				while (n != null) {
					Node next = n.next;
					int index = Math.abs(n.hash % Capacity);
					if (newArray[index] == null) {
						newArray[index] = n;
						n.next = null;
					} else {// index位置下原来就有元素
						n.next = newArray[index];
						newArray[index] = n; // 将新添加的元素放到该位置,
					}
					n = next;
				}
			}
		}
		nodeArray = newArray;// 更新数组

	}

	public int size() {
		return size;
	}

	// 用来存放键值对
	class Node<K, V> {
		K k;
		V v;
		int hash; // 存储当前放在数组中的结点的hash code
		Node<K, V> next;// 同一个hash值下, 此值存储的是下一个Node的地址

		Node(K k, V v) {
			this.k = k;
			this.v = v;
		}

	}
}

 

 

 由于Hash函数比较简单, 存储的性能还算过的去, 以下是测试方法:

 

 

public static void main(String args[]) {
		HashTest map = new HashTest<String, String>();
		long time = System.currentTimeMillis();
		for (int i = 0; i <= 1000000; i++) {
			map.put("" + i, "" + i);
		}
		long time1 = System.currentTimeMillis();
		System.out.println("存储时间:" + (time1 - time));
		long time2 = System.currentTimeMillis();
		String s = (String) map.get("1000000");
		long time3 = System.currentTimeMillis();
		System.out.println("查找时间:" + (time3 - time2) + " 查找到的Value值:" + s);
	}

 

  存储一百万个数据, 最后输出的结果是:

 

存储时间:2242

查找时间:0 查找到的Value值:1000000

 

 

 

 

实现(2):

 

看了下Java中HashMap的源代码, 对于以下的hash函数和indexFor函数比较有兴致(各种位运算, 觉得没兴致才怪... 好吧, 其实..之所以会想到用系统给的方法, 是因为之前在用取余数法的时候碰到了一些小问题, 导致性能低下的不能再低下, 然后就写了这个实现):

 

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

 

public class HashTest<K, V> {

	private int size; // 元素个数
	// 初始容量为11
	private static int CAPACITY = 11;
	// 数组中存放的数据为数组总长度的0.75则扩容
	private static float LOAD_FACTORY = 0.75f;
	private Node<K, V>[] nodeArray;// 存储结点的数组

	// 指定容量个构造因子的构造函数
	@SuppressWarnings("unchecked")
	HashTest(int Capacity, float Factory) {
		if (Capacity < 0)
			throw new IllegalArgumentException("Illegal initial capacity: "
					+ Capacity);
		if (Factory <= 0 || Float.isNaN(Factory))
			throw new IllegalArgumentException("Illegal load factor: "
					+ Factory);
		CAPACITY = Capacity;
		LOAD_FACTORY = Factory;
		size = 0;
		nodeArray = new Node[CAPACITY];
	}

	// 无参构造函数
	HashTest() {
		this(CAPACITY, LOAD_FACTORY);
	}

	// 得到hash码 (借用系统的方法)
	private int hash(int h) {
		h ^= (h >>> 20) ^ (h >>> 12);
		return h ^ (h >>> 7) ^ (h >>> 4);
	}

	// 根据hash code来得到元素要放在哪个位置(借用系统的方法)
	private int FindIndex(int hash, int length) {
		return hash & (length - 1);
	}

	// 存放键值对
	@SuppressWarnings("unchecked")
	public void put(K Key, V Value) {
		size++;
		Node node = new Node();
		node.k = Key;
		node.v = Value;
		// 得到本结点的hash码. 放入结点中, 用于之后扩容.
		int hash = hash(Key.hashCode());
		node.hashcode = hash;
		int index = FindIndex(hash, CAPACITY); // 找到要放的位置
		node.next = nodeArray[index]; // 原来在该位置的元素链到要添加的本元素后
		nodeArray[index] = node; // 将新添加的元素放到该位置
		if ((float) size / (float) CAPACITY > LOAD_FACTORY) {// 总个数大于数组容量的0.75时就扩容
			CAPACITY *= 2;
			extend(CAPACITY);
		}
	}

	// 扩容的方法
	@SuppressWarnings("unchecked")
	private void extend(int capacity) {
		Node[] newArray = new Node[capacity];
		Node n = null, next = null;
		for (int i = 0; i < nodeArray.length; i++) {
			if ((n = nodeArray[i]) != null) { // 该位置上有元素的时候
				while (n != null) {// 重新放置每个元素的位置
					next = n.next;
					int index = FindIndex(n.hashcode, capacity);
					n.next = newArray[index]; // 将本来在该位置上的元素放到将被放到此位置上的元素之后
					newArray[index] = n;
					n = next;
				}
			}
		}
		nodeArray = newArray;
	}

	// 根据Key值得到Map中的元素值
	@SuppressWarnings("unchecked")
	public V get(K Key) {
		int hash = hash(Key.hashCode());
		int index = FindIndex(hash, CAPACITY);
		Node node = null;
		V reuslt = null;
		for (node = nodeArray[index]; node != null; node = node.next) {
			if (Key.equals(node.k)) {
				reuslt = (V) node.v;
				break;
			}
		}
		return reuslt;
	}
}

// 结点类
class Node<K, V> {
	int hashcode; // 存储自己的hash码, 便于之后的查找
	K k;
	V v;
	Node<K, V> next; // 下一个结点地址

}

 

最后的存储性能确是比取余法要差了那么一点: 同样是一百万个数据存储, 这次用了 2600 多毫秒.

以下是测试方法:

 

public static void main(String args[]) {
		HashTest<String, String> map = new HashTest<String, String>();
		long time = System.currentTimeMillis();
		for (int i = 0; i <= 1000000; i++) {
			map.put("" + i, "" + i);
		}
		long time1 = System.currentTimeMillis();
		System.out.println("存储时间:" + (time1 - time));
		long time2 = System.currentTimeMillis();
		String s = (String) map.get("1000000");
		long time3 = System.currentTimeMillis();
		System.out.println("查找时间:" + (time3 - time2) + " 查找到的Value值:" + s);
	}

 

这次的存储一百万个数据输出的结果:

  存储时间:2632

  查找时间:0 查找到的Value值:1000000

 

  • 大小: 18.2 KB
0
1
分享到:
评论

相关推荐

    理解内存页面调度的机理,掌握几种理论调度算法实现,并通过实验比较各种调度算法的优劣。此外通过实验了解HASH表数据结构的使用。

    理解内存页面调度的机理,掌握几种理论调度算法实现,并通过实验比较各种调度算法的优劣。此外通过实验了解HASH表数据结构的使用。

    实验五:安全Hash算法SHA-1的实现

    Hash函数是提供数据完整性保障的一个...本次实验,我们希望通过上机操作,使同学们对安全Hash算法SHA-1的基本原理有一个全面的理解。通过本次实验,使学生掌握对Hash函数的应用,为后面数字签名方案的学习打下基础。

    geohash算法实现Java代码

    geohash基本原理是将地球理解为一个二维平面,将平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码。GeoHash将二维的经纬度转换成一维的字符串。

    安全Hash算法SHA-1的实现.zip_安全Hash算法SHA-1的实现_安全工具_密码学实验

    Hash函数是提供数据完整性保障的一个...本次实验,我们希望通过上机操作,使 同学们对安全Hash算法SHA-1的基本原理有一个全面的理解。通过本次实验,使学生掌握对 Hash函数的应用,为后面数字签名方案的学习打下基础。

    HASH算法之MD5算法

    hash函数之md5程序,可运行,包含testbench

    哈希表实例源码

    使用c语言实现了hashtable算法,可以用来作为新手学习,理解hashtable。理解linux内核中使用的hashtable

    高级进阶c语言教程..doc

    43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 ...

    ECC椭圆曲线签名方案的实现

    本次课题要求基于椭圆曲线算法,实现对消息签名的方案,要求密钥长度至少160比特,相关Hash函数选用SHA-1。具体要求如下: 1.深入理解椭圆曲线密码体制的实现思想和原理; 2.选择一种编程语言和开发工具,编写程序...

    集合底层源码分析.doc

     哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组”  一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到...

    python 密码学示例——理解哈希(Hash)算法

    Hash 是密码学安全性的基石,它引入了单向...很多时候一段加密的消息无法被他人读取和理解(保密性),并不意味着该密文不会在传播过程中被截取和恶意修改(完整性)。 信息摘要(message digest)或指纹(fingerpri

    数据结构实验

    2.编写程序实现Hash表的建立、删除、插入以及查找操作。 程序应包含的主要功能函数有: Hash( ):计算哈希地址 InitialHash( ):初始化哈希表 SearchHash( ):在哈希表中查找关键字 InsertHash( ):向哈希表中...

    十三个常用算法

    一、A*搜索算法 一(续)、A*,Dijkstra,BFS 算法性能比较及A*算法的应用 ...十一、从头到尾彻底解析Hash 表算法 十二、快速排序算法之所有版本的c/c++实现 十三、通过浙大上机复试试题学SPFA 算法

    md5.zip_DSA C数字签名_DSA 签名_DSA实现_Stankiewicz_dsa 数字签名

    通过实现数字签名算法(DSA),加深对数字签名算法的理解,同时学习Hash算法的实现。 1)利用C\C++语言实现DSA算法。 2)DSA中的Hash函数采用SHA算法。

    《C语言接口与实现》书中源代码.rar

    书中描述的24个高效的API帮助你深入理解C语言,资源内包含作者详细描述的24个借口和它们的实现细节,还有一些实例可以参考。。。。

    sesvc.exe 阿萨德

    一文让你彻底理解 Java HashMap 和 ConcurrentHashMap 2018-07-25 分类:JAVA开发、编程开发、首页精华0人评论 来源:crossoverjie.top 分享到:更多0 前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常...

    十五个经典算法研究与总结、目录+索引(定稿版)

    十一、从头到尾彻底解析Hash表算法 十一(续)、倒排索引关键词Hash不重复编码实践 十二、快速排序算法 (快速排序算法3篇文章) 十二(续)、快速排序算法的深入分析 十二(再续):快速排序算法之所有版本的c/c++...

    免费下载:C语言难点分析整理.doc

    43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 ...

    用python实现sm2国密算法

    sm2需要调用sm3杂凑算法来求hash值,里面包括了kdf密钥派生函数,和一些字符串进制转换函数,都放在sm2头文件里面,s m2包含了一些必要的数字签名和验签,加解密算法,有注释,对应理解。

    浅谈对vueRouter的理解

    首先可以了解一下是如何通过mode字段来实现路由的模式分配的,VueRouter的默认路由模式是hash模式,并且在浏览器不支持history模式(即不支持history.pushState)时会自动降级为hash模式: export d

    JDK1.8 ConcurrentHashMap的一点理解

    只是都是相通的,当我们了解了ConcurrentHashMap的实现原理以及各个方法的实现机制,我们对于其他的hash类型实现也能快速的理解,今天我们就来通过源码来一点一点的分析下ConcurrentHashMap的实现。 首先我们来看...

Global site tag (gtag.js) - Google Analytics