数据结构是哈希表(hashTable)
哈希表也称为散列表,是根据关键字值(key value)而直接进行访问的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表。比如我们可以用下面的方法将关键字映射成数组的下标:arrayIndex
= hugeNumber % arraySize。
哈希化之后难免会产生一个问题,那就是对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲突呢?一种方法是开放地址法,即通过系统的方法找到数组的另一个空位,把数据填入,而不再用哈希函数得到的数组下标,因为该位置已经有数据了;另一种方法是创建一个存放链表的数组,数组内不直接存储数据,这样当发生冲突时,新的数据项直接接到这个数组下标所指的链表中,这种方法叫做链地址法。下面针对这两种方法进行讨论。
1.开放地址法
线性探测法
所谓线性探测,即线性地查找空白单元。如果21是要插入数据的位置,但是它已经被占用了,那么就是用22,然后23,以此类推。数组下标一直递增,直到找到空白位。下面是基于线性探测法的哈希表实现代码:
public class HashTable { private DataItem[] hashArray; // DateItem类是数据项,封装数据信息 private int arraySize=10; private int itemNum; // 数组中目前存储了多少项 private DataItem nonItem; // 用于删除项的 public HashTable() { hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); // deleted item key is -1 } public boolean isFull() { return (itemNum == arraySize); } public boolean isEmpty() { return (itemNum == 0); } public void displayTable() { System.out.print("Table:"); for (int j = 0; j < arraySize; j++) { if (hashArray[j] != null) { System.out.print(hashArray[j].getKey() + " "); } else { System.out.print("** "); } } System.out.println(""); } public int hashFunction(int key) { return key % arraySize; // hash function } public void insert(DataItem item) { if (isFull()) { // 扩展哈希表 System.out.println("哈希表已满,重新哈希化.."); extendHashTable(); } int key = item.getKey(); int hashVal = hashFunction(key); while (hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { ++hashVal; hashVal %= arraySize; } hashArray[hashVal] = item; itemNum++; } /* * 数组有固定的大小,而且不能扩展,所以扩展哈希表只能另外创建一个更大的数组,然后把旧数组中的数据插到新的数组中。 * 但是哈希表是根据数组大小计算给定数据的位置的,所以这些数据项不能再放在新数组中和老数组相同的位置上,因此不能直接拷贝,需要按顺序遍历老数组, * 并使用insert方法向新数组中插入每个数据项。这叫重新哈希化。这是一个耗时的过程,但如果数组要进行扩展,这个过程是必须的。 */ public void extendHashTable() { // 扩展哈希表 int num = arraySize; itemNum = 0; // 重新记数,因为下面要把原来的数据转移到新的扩张的数组中 arraySize *= 2; // 数组大小翻倍 DataItem[] oldHashArray = hashArray; hashArray = new DataItem[arraySize]; for (int i = 0; i < num; i++) { insert(oldHashArray[i]); } } public DataItem delete(int key) { if (isEmpty()) { System.out.println("Hash table is empty!"); return null; } int hashVal = hashFunction(key); while (hashArray[hashVal] != null) { if (hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; hashArray[hashVal] = nonItem; // nonItem表示空Item,其key为-1 itemNum--; return temp; } ++hashVal; hashVal %= arraySize; } return null; } public DataItem find(int key) { int hashVal = hashFunction(key); while (hashArray[hashVal] != null) { if (hashArray[hashVal].getKey() == key) { return hashArray[hashVal]; } ++hashVal; hashVal %= arraySize; } return null; } } class DataItem { private int iData; public DataItem(int data) { iData = data; } public int getKey() { return iData; } }
线性探测有个弊端,即数据可能会发生聚集。一旦聚集形成,它会变得越来越大,那些哈希化后落在聚集范围内的数据项,都要一步步的移动,并且插在聚集的最后,因此使聚集变得更大。聚集越大,它增长的也越快。这就导致了哈希表的某个部分包含大量的聚集,而另一部分很稀疏。
为了解决这个问题,我们可以使用二次探测:二次探测是防止聚集产生的一种方式,思想是探测相隔较远的单元,而不是和原始位置相邻的单元。线性探测中,如果哈希函数计算的原始下标是x,
线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。二次探测虽然消除了原始的聚集问题,但是产生了另一种更细的聚集问题,叫二次聚集:比如讲184,302,420和544依次插入表中,它们的映射都是7,那么302需要以1为步长探测,420需要以4为步长探测, 544需要以9为步长探测。只要有一项其关键字映射到7,就需要更长步长的探测,这个现象叫做二次聚集。二次聚集不是一个严重的问题,但是二次探测不会经常使用,因为还有好的解决方法,比如再哈希法。
再哈希法
为了消除原始聚集和二次聚集,现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样。即:不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。再哈希法就是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,对于指定的关键字,步长在整个探测中是不变的,不同关键字使用不同的步长、经验说明,第二个哈希函数必须具备如下特点:
1. 和第一个哈希函数不同;
2. 不能输出0(否则没有步长,每次探索都是原地踏步,算法将进入死循环)。
专家们已经发现下面形式的哈希函数工作的非常好:stepSize = constant - key % constant; 其中constant是质数,且小于数组容量。
再哈希法要求表的容量是一个质数,假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是0,5,10,0,5,10,以此类推一直循环下去。算法只尝试这三个单元,所以不可能找到某些空白单元,最终算法导致崩溃。如果数组容量为13, 质数,探测序列最终会访问所有单元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一个空位,就可以探测到它。下面看看再哈希法的代码:
public class HashTableDouble { private DataItem[] hashArray; private int arraySize; private int itemNum; private DataItem nonItem; public HashTableDouble() { arraySize = 13; hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); } public void displayTable() { System.out.print("Table:"); for (int i = 0; i < arraySize; i++) { if (hashArray[i] != null) { System.out.print(hashArray[i].getKey() + " "); } else { System.out.print("** "); } } System.out.println(""); } public int hashFunction1(int key) { // first hash function return key % arraySize; } public int hashFunction2(int key) { // second hash function return 5 - key % 5; } public boolean isFull() { return (itemNum == arraySize); } public boolean isEmpty() { return (itemNum == 0); } public void insert(DataItem item) { if (isFull()) { System.out.println("哈希表已满,重新哈希化.."); extendHashTable(); } int key = item.getKey(); int hashVal = hashFunction1(key); int stepSize = hashFunction2(key); // 用hashFunction2计算探测步数 while (hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { hashVal += stepSize; hashVal %= arraySize; // 以指定的步数向后探测 } hashArray[hashVal] = item; itemNum++; } public void extendHashTable() { int num = arraySize; itemNum = 0; // 重新记数,因为下面要把原来的数据转移到新的扩张的数组中 arraySize *= 2; // 数组大小翻倍 DataItem[] oldHashArray = hashArray; hashArray = new DataItem[arraySize]; for (int i = 0; i < num; i++) { insert(oldHashArray[i]); } } public DataItem delete(int key) { if (isEmpty()) { System.out.println("Hash table is empty!"); return null; } int hashVal = hashFunction1(key); int stepSize = hashFunction2(key); while (hashArray[hashVal] != null) { if (hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; hashArray[hashVal] = nonItem; itemNum--; return temp; } hashVal += stepSize; hashVal %= arraySize; } return null; } public DataItem find(int key) { int hashVal = hashFunction1(key); int stepSize = hashFunction2(key); while (hashArray[hashVal] != null) { if (hashArray[hashVal].getKey() == key) { return hashArray[hashVal]; } hashVal += stepSize; hashVal %= arraySize; } return null; } } class DataItem { private int iData; public DataItem(int data) { iData = data; } public int getKey() { return iData; } }
2.链地址法
在开放地址法中,通过再哈希法寻找一个空位解决冲突问题,另一个方法是在哈希表每个单元中设置链表(即链地址法),某个数据项的关键字值还是像通常一样映射到哈希表的单元,而数据项本身插入到这个单元的链表中。其他同样映射到这个位置的数据项只需要加到链表中,不需要在原始的数组中寻找空位。下面看看链地址法的代码:
public class HashTableDouble { private SortedList[] hashArray; // 数组中存放链表 private int arraySize; public HashTableDouble(int size) { arraySize = size; hashArray = new SortedList[arraySize]; // new出每个空链表初始化数组 for (int i = 0; i < arraySize; i++) { hashArray[i] = new SortedList(); } } public void displayTable() { for (int i = 0; i < arraySize; i++) { System.out.print(i + ": "); hashArray[i].displayList(); } } public int hashFunction(int key) { return key % arraySize; } public void insert(LinkNode node) { int key = node.getKey(); int hashVal = hashFunction(key); hashArray[hashVal].insert(node); // 直接往链表中添加即可 } public LinkNode delete(int key) { int hashVal = hashFunction(key); LinkNode temp = find(key); hashArray[hashVal].delete(key);// 从链表中找到要删除的数据项,直接删除 return temp; } public LinkNode find(int key) { int hashVal = hashFunction(key); LinkNode node = hashArray[hashVal].find(key); return node; } }
这里用的链表是有序链表LinkNode
public class SortedList { private LinkNode first; public SortedList() { first = null; } public boolean isEmpty() { return (first == null); } public void insert(LinkNode node) { int key = node.getKey(); LinkNode previous = null; LinkNode current = first; while(current != null && current.getKey() < key) { previous = current; current = current.next; } if(previous == null) { first = node; } else { node.next = current; previous.next = node; } } public void delete(int key) { LinkNode previous = null; LinkNode current = first; if(isEmpty()) { System.out.println("chain is empty!"); return; } while(current != null && current.getKey() != key) { previous = current; current = current.next; } if(previous == null) { first = first.next; } else { previous.next = current.next; } } public LinkNode find(int key) { LinkNode current = first; while(current != null && current.getKey() <= key) { if(current.getKey() == key) { return current; } current = current.next; } return null; } public void displayList() { System.out.print("List(First->Last):"); LinkNode current = first; while(current != null) { current.displayLink(); current = current.next; } System.out.println(""); } } class LinkNode { private int iData; public LinkNode next; public LinkNode(int data) { iData = data; } public int getKey() { return iData; } public void displayLink() { System.out.print(iData + " "); } }
在没有冲突的情况下,哈希表中执行插入和删除操作可以达到O(1)的时间级。
数据结构是哈希表(hashTable)的更多相关文章
- C#中哈希表(HashTable)的用法详解以及和Dictionary比较
1. 哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对, ...
- Java中哈希表(Hashtable)是如何实现的
Java中哈希表(Hashtable)是如何实现的 Hashtable中有一个内部类Entry,用来保存单元数据,我们用来构建哈希表的每一个数据是Entry的一个实例.假设我们保存下面一组数据,第一列 ...
- 哈希表(hashtable)的javascript简单实现
javascript中没有像c#,java那样的哈希表(hashtable)的实现.在js中,object属性的实现就是hash表,因此只要在object上封装点方法,简单的使用obejct管理属性的 ...
- 哈希表(Hashtable)简述
一,哈希表(Hashtable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似keyvalue的键值对,其中 ...
- c/c++ 哈希表 hashtable
c/c++ 哈希表 hashtable 概念:用key去查找value 实现hash函数有很多方法,本文用除留余数法. 除留余数法的概念: 取一个固定的基数的余数,注意不能用偶数,用偶数的话,分布会不 ...
- 转 C#中哈希表(HashTable)的用法详解
看了一遍有关哈希表的文字,作者总结的真是不错 .收藏起来 1. 哈希表(HashTable)简述 在.NET Framework中,Hashtable是System.Collections命名空间提 ...
- 数据结构 5 哈希表/HashMap 、自动扩容、多线程会出现的问题
上一节,我们已经介绍了最重要的B树以及B+树,使用的情况以及区别的内容.当然,本节课,我们将学习重要的一个数据结构.哈希表 哈希表 哈希也常被称作是散列表,为什么要这么称呼呢,散列.散列.其元素分布较 ...
- C# 哈希表HashTable的简单使用
本人C#程序菜鸟级别的存在,写博客一方面是为了知识的共享,另一方面也是为了督促自己:大神,可以忽略这篇文文的.废话到此...... 哈希表是可以直接进行访问的数据结构,在形式上是类似字典的.不同的是, ...
- python数据结构之哈希表
哈希表(Hash table) 众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry.这些个键值对(Entry)分散存储在一个数组当中,这个数组就是Has ...
随机推荐
- [图论]最大流问题(Maximum flow)的定义
首先定义网络(network)N =(V,E), V表示顶点(Vertices)集合, E表示边(Edges)集合. s,t是V中的两个顶点,分别表示网络N中的源点(source)和汇点(sink). ...
- Swift中的可选协议和方法的历史渊源
@objc protocol Transaction { func commit() -> Bool optional func isComplete() -> Bool } 以上协议被标 ...
- 基于Nginx服务器和iOS9的HTTPS安全通信
简介 在网络通信中,使用抓包软件可以对网络请求进行分析,并进行重放攻击,重放攻击的解决方案一般是使用一个变化的参数,例如RSA加密的时间戳,但考虑到网络传输时延,时间戳需要有一定的误差容限,这样仍然不 ...
- SKSpriteNode对象初始化在iPhone 6 plus中显示不正确的分析及解决
一个SpriteKit项目在其他设备上运行都无问题(无论是真机或是模拟器),但是在iPhone6 Plus上会出现精灵对象纹理被过度放大的现象: 从上图中大家可以看到无论是主角或是道具球都过大了. 看 ...
- 剑指Offer——分治算法
剑指Offer--分治算法 基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更 ...
- 带你深入理解STL之Vector容器
C++内置了数组的类型,在使用数组的时候,必须指定数组的长度,一旦配置了就不能改变了,通常我们的做法是:尽量配置一个大的空间,以免不够用,这样做的缺点是比较浪费空间,预估空间不当会引起很多不便. ST ...
- JavaScript介绍-javaScript学习之旅(一)
javaScript简介 1.javaScript是互联网上最流行的脚本语言,这门可用于web和html,更可广泛用于服务器端,pc端,移动端. 2.javaScript脚本语言: javaScrip ...
- 【一天一道LeetCode】#299. Bulls and Cows
一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 You are ...
- 03 RadioButton 单选按钮
>概念:从多个互斥选项中选择一个 如果是选项全部展开 RadioButton 不是展开的Spinner(下拉列表) >属性: android:checked="tr ...
- Git Bash+EGit在项目中配合使用最常用方法总结(根据场景使用)
最近在项目中使用Git进行代码管理,之前一直用SVN进行管理,现在谈一谈Git在项目中如何与EGit插件配合使用,高效同步开发. 使用过SVN一段时间的人,初识Git一定感觉很别扭,发现会遇到各种各样 ...