哈希表是一种数据结构,它能够提供快速的插入操做和查找操做,不论哈希表中有多少数据,插入和删除只须要接近常量的时间,即O(1)的时间级。java
哈希表的缺点:它是基于数组的,数组建立后难于扩展。某些哈希表被基本填满时,性能降低得很是严重,因此程序员必需要清楚表中将要存储多少数据(或者准备好按期地把数据转移到更大的哈希表中,这是个费时的过程。)程序员
假设想在内存中存储50000个英文单词。起初可能考虑每一个单词占据一个数组单元,那么数组大小是50000,同时可使用数组下标存取单词。这样,存取确实很快。可是数组下标和单词有什么关系呢?例如给出一个单词cats,怎么能找到它的数组下标呢?算法
把单词转化为数组下标数据库
把数字相加c=3,a=1,t=20,s=19,3+1+20+19=43,那么单词cats存储在数组下标为43的单元中。编程
第一个单词a的编码是0+0+0+0+0+0+0+0+0+1=1,字典最后一个可能的单词是zzzzzzzzzz,编码是26+26+26+26+26+26+26+26+26+26=260。所以,单词编码的范围是从1到260。不幸的是,字典中有50000个单词,因此没有足够的数组下标数来索引那么多的单词。每一个数组数据项大概要存储192个单词。用一个单词占用一个数组单元的方案会发生问题。也许能够考虑每一个数组数据项包含一个子数组或链表。不幸的是,这个办法严重下降了存取速度。存取数据项确实很快,可是要在192个单词中找到其中一个,速度就很慢。、数组
幂的连乘cats=3*273 + 1*272 + 20*271 + 19*270=60337。若是是十位字符串,数字值会很是大。若是为每一个可能的单词分配一个数组单元,无论这个单词是否是真正的英语单词。从aaaaaaaaaa到zzzzzzzzzz,这些单元只有一小部分存放了存在的英语单词,而大多数单元是空的。在内存中的数组也根本不可能有这么多的单元。数据结构
第一种方案(数字相加求和)产生的数组下标太少。第二种方案(与27的幂相乘并求和)产生的数组下标又太多。less
哈希化dom
如今须要一种压缩方法,把数位幂的连乘系统中获得的巨大的整数范围压缩到可接受的数组范围中。函数
假如只有50000个单词,须要容量为100000的数组(多一倍的空间效率更高)。有个简单的方法,使用取余操做符,把0到7000000000000的范围压缩为0到100000。
arrayIndex = hugeNumber % arraySize
这就是一种哈希函数。它把一个大范围的数字哈希(转化)成一个小范围的数字。这个小的范围对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就称为哈希表。
冲突
把巨大的数字空间压缩成较小的数字空间,必然要付出代价,即不能保证,每一个单词都映射到数组的空白单元。以前设置了数组的大小是须要存储的数据量的两倍。所以,可能一半的单元是空的。当冲突发生时,一个方法是经过系统的方法找到数组的一个空位,并把这个单词填入,而再也不用哈希函数获得的数组下标。这个方法叫作开放地址法。例如,若是cats哈希化的结果是5421,但它的位置已经被parsnip占用,那么可能会考虑把cats放在5422的位置上。第二种方法是建立一个存放单词链表的数组,数组内不直接存储单词。这样,当发生冲突时,新的数据项直接接到这个数组下标所指的链表中。这种方法叫作链地址法。
开放地址法
在开放地址法中,若数据不能直接放在由哈希函数计算出来的数组下标所指的单元时,就要寻找数组的其余位置。下面要探索开放地址法的三种方法,它们在找下一个空白单元时使用的方法不一样。这三种方法分别是线性探测,二次探测和再哈希法。
线性探测
在线性探测中,线性地查找空白单元。若是5421是要插入数据的位置,它已经被占用了,那么就使用5422,而后是5423,依次类推,数组下标一直递增,直到找到空位。
汇集
当哈希表变得愈来愈满时,汇集变得愈来愈严重。这致使产生很是长的探测长度。意味着存取序列最后的单元会很是耗时。
二次探测
在开放地址法的线性探测中会发生汇集。一旦汇集造成,它会变得愈来愈大。那些哈希化后的落在汇集范围内的数据项,都要一步一步移动,而且插在汇集的最后,所以使汇集变得更大。汇集越大,它增大得也越快。二次探测是防止汇集产生的一种尝试。思想是探测相隔较远的单元,而不是和原始位置相邻的单元。步骤是步数的平方。在线性探测中,若是哈希函数计算的原始下标是x,线性探测就是x+1,x+2,x+3,依次类推。而在二次探测中,探测的过程是x+1,x+4,x+9,x+16,x+25,依次类推。
二次探测的问题
二次探测消除了在线性探测中产生的汇集问题,这种汇集问题叫作原始汇集。然而,二次探测产生了另一种,更细的汇集问题。之因此会发生,是由于全部映射到同一个位置的关键字在寻找空位时,探测的单元都是同样的。好比将184,302,420和544依次插入到表中,它们都映射到7。那么302须要以一为步长的探测,420须要以四为步长的探测,544须要以九为步长的探测。只要有一项,其关键字映射到7,就须要更长步长的探测。这个现象叫作二次汇集。
// to run this program: C:>java HashTableApp import java.io.*; class DataItem { // (could have more data) private int iData; // data item (key) public DataItem(int ii) // constructor { iData = ii; } public int getKey() { return iData; } } class HashTable { private DataItem[] hashArray; // array holds hash table private int arraySize; private DataItem nonItem; // for deleted items public HashTable(int size) // constructor { arraySize = size; hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); // deleted item key is -1 } 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 hashFunc(int key) { return key % arraySize; // hash function } public void insert(DataItem item) // insert a DataItem // (assumes table not full) { int key = item.getKey(); // extract key int hashVal = hashFunc(key); // hash the key // until empty cell or -1, while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { ++hashVal; //线性探测法 // go to next cell //hashVal+=i*i 二次探测法 hashVal %= arraySize; // wraparound if necessary } hashArray[hashVal] = item; // insert item } public DataItem delete(int key) // delete a DataItem { int hashVal = hashFunc(key); // hash the key while(hashArray[hashVal] != null) // until empty cell, { // found the key? if(hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; // save item hashArray[hashVal] = nonItem; // delete item return temp; // return item } ++hashVal; //线性探测法 // go to next cell //hashVal+=i*i 二次探测法 hashVal %= arraySize; // wraparound if necessary } return null; // can't find item } public DataItem find(int key) // find item with key { int hashVal = hashFunc(key); // hash the key while(hashArray[hashVal] != null) // until empty cell, { // found the key? if(hashArray[hashVal].getKey() == key) return hashArray[hashVal]; // yes, return item ++hashVal; //线性探测法 // go to next cell //hashVal+=i*i 二次探测法 hashVal %= arraySize; // wraparound if necessary } return null; // can't find item } } class HashTableApp { public static void main(String[] args) throws IOException { DataItem aDataItem; int aKey, size, n, keysPerCell; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); keysPerCell = 10; // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data { aKey = (int)(java.lang.Math.random() * keysPerCell * size); aDataItem = new DataItem(aKey); theHashTable.insert(aDataItem); } while(true) // interact with user { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new DataItem(aKey); theHashTable.insert(aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) { System.out.println("Found " + aKey); } else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }
再哈希法
为了消除原始汇集和二次汇集,可使用另外的一个方法:再哈希法。二次汇集产生的缘由是,二次探测的算法产生的探测序列步长老是固定的:1,4,9,16,依次类推。如今须要的一种方法是产生一种依赖关键字的探测序列,而不是每一个关键字都同样。那么不一样的关键字即便映射到相同的数组下标,也可使用不一样的探测序列。方法是把关键字用不一样的哈希函数在作一遍哈希化,用这个结果做为步长,对指定的关键字,步长在整个探测中是不变的,不过不一样的关键字使用不一样的步长。
第二个哈希函数必须具有以下特色:和第一个哈希函数不一样,不能输出为0(算法会陷入死循环)
stepSize = constant - (key % constant)会工做的比较好。
// to run this program: C:>java HashDoubleApp import java.io.*; class DataItem { // (could have more items) private int iData; // data item (key) public DataItem(int ii) // constructor { iData = ii; } public int getKey() { return iData; } } class HashTable { private DataItem[] hashArray; // array is the hash table private int arraySize; private DataItem nonItem; // for deleted items HashTable(int size) // constructor { arraySize = size; hashArray = new DataItem[arraySize]; nonItem = new DataItem(-1); } 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 hashFunc1(int key) { return key % arraySize; } public int hashFunc2(int key) { // non-zero, less than array size, different from hF1 // array size must be relatively prime to 5, 4, 3, and 2 return 5 - key % 5; } // insert a DataItem public void insert(int key, DataItem item) // (assumes table not full) { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size // until empty cell or -1 while(hashArray[hashVal] != null && hashArray[hashVal].getKey() != -1) { hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound } hashArray[hashVal] = item; // insert item } public DataItem delete(int key) // delete a DataItem { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size while(hashArray[hashVal] != null) // until empty cell, { // is correct hashVal? if(hashArray[hashVal].getKey() == key) { DataItem temp = hashArray[hashVal]; // save item hashArray[hashVal] = nonItem; // delete item return temp; // return item } hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound } return null; // can't find item } public DataItem find(int key) // find item with key // (assumes table not full) { int hashVal = hashFunc1(key); // hash the key int stepSize = hashFunc2(key); // get step size while(hashArray[hashVal] != null) // until empty cell, { // is correct hashVal? if(hashArray[hashVal].getKey() == key) return hashArray[hashVal]; // yes, return item hashVal += stepSize; // add the step hashVal %= arraySize; // for wraparound } return null; // can't find item } } class HashDoubleApp { public static void main(String[] args) throws IOException { int aKey; DataItem aDataItem; int size, n; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data { aKey = (int)(java.lang.Math.random() * 2 * size); aDataItem = new DataItem(aKey); theHashTable.insert(aKey, aDataItem); } while(true) // interact with user { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new DataItem(aKey); theHashTable.insert(aKey, aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) System.out.println("Found " + aKey); else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }
链地址法
开放地址法中,经过在哈希表中再寻找一个空位解决冲突问题。另外一个方法是在哈希表每一个单元中设置链表。某个数据项的关键字值仍是像一般同样映射到哈希表的单元,而数据项自己插入到这个单元的链表中。其余一样映射到这个位置的数据项只须要加到链表中,不须要在原始的数组中寻找空位。链地址法中的装填因子(数据项和哈希表容量的比值)与开放地址法的不一样。在链地址法中,须要在有N个单元的数组中装入N个或更多的数据项;所以,装填因子通常为1,或比1大。这没有问题,由于,某些位置包含的链表中包含两个或两个以上的数据项。
// to run this program: C:>java HashChainApp import java.io.*; class Link { // (could be other items) private int iData; // data item public Link next; // next link in list public Link(int it) // constructor { iData= it; } public int getKey() { return iData; } public void displayLink() // display this link { System.out.print(iData + " "); } } class SortedList { private Link first; // ref to first list item public void SortedList() // constructor { first = null; } public void insert(Link theLink) // insert link, in order { int key = theLink.getKey(); Link previous = null; // start at first Link current = first; // until end of list, while( current != null && key > current.getKey() ) { // or current > key, previous = current; current = current.next; // go to next item } if(previous==null) // if beginning of list, first = theLink; // first --> new link else // not at beginning, previous.next = theLink; // prev --> new link theLink.next = current; // new link --> current } public void delete(int key) // delete link { // (assumes non-empty list) Link previous = null; // start at first Link current = first; // until end of list, while( current != null && key != current.getKey() ) { // or key == current, previous = current; current = current.next; // go to next link } // disconnect link if(previous==null) // if beginning of list first = first.next; // delete first link else // not at beginning previous.next = current.next; // delete current link } public Link find(int key) // find link { Link current = first; // start at first // until end of list, while(current != null && current.getKey() <= key) { // or key too small, if(current.getKey() == key) // is this the link? return current; // found it, return link current = current.next; // go to next item } return null; // didn't find it } public void displayList() { System.out.print("List (first-->last): "); Link current = first; // start at beginning of list while(current != null) // until end of list, { current.displayLink(); // print data current = current.next; // move to next link } System.out.println(""); } } class HashTable { private SortedList[] hashArray; // array of lists private int arraySize; public HashTable(int size) // constructor { arraySize = size; hashArray = new SortedList[arraySize]; // create array for(int j=0; j<arraySize; j++) // fill array hashArray[j] = new SortedList(); // with lists } public void displayTable() { for(int j=0; j<arraySize; j++) // for each cell, { System.out.print(j + ". "); // display cell number hashArray[j].displayList(); // display list } } public int hashFunc(int key) // hash function { return key % arraySize; } public void insert(Link theLink) // insert a link { int key = theLink.getKey(); int hashVal = hashFunc(key); // hash the key hashArray[hashVal].insert(theLink); // insert at hashVal } public void delete(int key) // delete a link { int hashVal = hashFunc(key); // hash the key hashArray[hashVal].delete(key); // delete link } public Link find(int key) // find link { int hashVal = hashFunc(key); // hash the key Link theLink = hashArray[hashVal].find(key); // get link return theLink; // return link } } public class HashChainApp { public static void main(String[] args) throws IOException { int aKey; Link aDataItem; int size, n, keysPerCell = 100; // get sizes System.out.print("Enter size of hash table: "); size = getInt(); System.out.print("Enter initial number of items: "); n = getInt(); // make table HashTable theHashTable = new HashTable(size); for(int j=0; j<n; j++) // insert data { aKey = (int)(java.lang.Math.random() * keysPerCell * size); aDataItem = new Link(aKey); theHashTable.insert(aDataItem); } while(true) // interact with user { System.out.print("Enter first letter of "); System.out.print("show, insert, delete, or find: "); char choice = getChar(); switch(choice) { case 's': theHashTable.displayTable(); break; case 'i': System.out.print("Enter key value to insert: "); aKey = getInt(); aDataItem = new Link(aKey); theHashTable.insert(aDataItem); break; case 'd': System.out.print("Enter key value to delete: "); aKey = getInt(); theHashTable.delete(aKey); break; case 'f': System.out.print("Enter key value to find: "); aKey = getInt(); aDataItem = theHashTable.find(aKey); if(aDataItem != null) System.out.println("Found " + aKey); else System.out.println("Could not find " + aKey); break; default: System.out.print("Invalid entry\n"); } } } public static String getString() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static char getChar() throws IOException { String s = getString(); return s.charAt(0); } public static int getInt() throws IOException { String s = getString(); return Integer.parseInt(s); } }
使用质数做为取模的基数
若是许多关键字共享一个数组容量做为除数,它们会趋向于映射到相同的位置,这会致使汇集。使用质数,能够消除这种可能性。使用质数能够保证关键字会较平均地映射到数组中。
public static int getPrime(int min){ for(int j = min+1;true; j++) if(isPrime(j)) return j; } public static boolean isPrime(int n){ for(int j=2;(j*j<=n);j++) if(n %j == 0) return false; return true; }
扩展数组
当哈希表变得太满时,一个选择是扩展数组。在java中,数组有固定的大小,并且不能扩展。编程时只能另外建立一个新的更大的数组,而后把旧数组的全部内容插入到新的数组中。哈希函数根据数组大小计算给定数据项的位置,因此这些数据项不能再放在新数组中和老数组相同的位置上。所以,不能简单地从一个数组向另外一个数组拷贝数据。须要按顺序遍历老数组,用insert()方法向新数组中插入每一个数据项。这叫作从新哈希化。这是一个耗时的过程,但若是数组要进行扩展,这个过程就是必要的。
哈希化字符串
把短小的字符串转换成数字,方法是每一个数位乘以对应的一个常数的幂。cats转化为一个数字,key=3*273 + 1*272 +20*271 +19*270
public static long hashFunc2(String key){ long hashVal = key.charAt(0) - 96; for(int j=1;j<key.length();j++){ int letter = key.charAt(j) - 96; hashVal = hashVal*27 + letter; hashVal %=101;//先求余,而不是最后一次求余,防止溢出 } return hashVal%101; }
哈希化和外部存储
文件指针表
外部哈希化的关键部分是一个哈希表,它包含块成员,指向外部存储器中的块。哈希表有时叫作索引。它能够存储在内存中,若是它太大,也能够存储在磁盘上,而把它的一部分放在内存中。即便把哈希表都放在内存中,也要在磁盘中保存一个备份,文件打开时,把它读入内存。
未满的块
在外部哈希化中,重要的是块不要填满。所以,每一个块平均存储8个记录。有的块多些,有的块少些。全部关键字映射为同一个值的记录都定位到相同块。为找到特定关键字的一个记录,搜索算法哈希化关键字,用哈希值做为哈希表的下标,获得某个下标中的块号,而后读取这个块。这个过程是有效的,由于定位一个特定的数据项,只须要访问一次块。缺点是至关多的磁盘空间被浪费了,由于设计时规定,块是不容许填满的。为了实现这个方案,必须仔细选择哈希函数和哈希表的大小,为的是限制映射到相同的值关键字的数量。
填满的块
即便一个好的哈希函数,块偶尔也会填满。这时,可使用在内部哈希表中讨论的处理冲突的不一样方法:开放地址法和链地址法。
开放地址法中,插入时,若是发现一个块是满的,算法在相邻的块插入新记录。在线性探测中,这是下一个块,但也能够用二次探测或再哈希法选择。在链地址法中,有一个溢出块,当发现已满时,新纪录插在溢出块中。填满的块是不合须要的,由于有了它就须要额外的磁盘访问,这就须要两倍的访问时间。然而,若是这种状况不常常发生,也能够接受。
小结
哈希表基于数组。
关键字值的范围一般比数组容量大。
关键字值经过哈希函数映射为数组的下标。
英文字典是一个数据库的典型列子,它能够有效的用哈希表来处理。
一个关键字哈希化到已占用的数组单元,这种状况叫作冲突。
冲突能够用两种方法解决:开放地址法和链地址法。
在开放地址法中,把冲突的数据项放在数组的其余位置。
在链地址法中,每一个数组单元包含一个链表。把全部映射到同一个数组下标的数据项都插在这个链表中。
讨论了三种开放地址法:线性探测、二次探测和再哈希法。
在线性探测中,步长老是1,因此若是x是哈希函数计算获得的数组下标,那么探测序列就是x,x+1,x+2,x+3,依次类推。
找到一个特定项须要通过的步数叫作探测长度。
在线性探测中,已填充单元的长度不断增长。它们叫作首次汇集,这会下降哈希表的性能。
二次探测中,x的位移是步数的平方,因此探测序列就是x,x+1,x+4,x+9,x+16,依次类推。
二次探测消除了首次汇集,可是产生了二次汇集,它比首次汇集的危害略小。
二次汇集的发生是由于全部映射到同一个单元的关键字,在探测过程当中执行了相同的序列。
发生上述状况是由于步长只依赖于哈希值,与关键字无关。
在再哈希法中,步长依赖于关键字,且从第二个哈希函数中获得。
在再哈希法中,若是第二个哈希函数返回一个值s,那么探测序列就是x,x+s,x+2s,x+3s,依次类推,这里s由关键字获得,但探测过程当中保持常量。
装填因子是表中数据项数和数组容量的比值。
开放地址法中的最大装填因子应该在0.5附近,若具备相同的装填因子,对于再哈希法来讲,查找的平均探测长度是2。
在开放地址法中,当装填因子接近1时,查找时间趋于无限。
在开放地址法中,关键是哈希表不能填的太满。
对于链地址法,装填因子为1比较合适。这时,成功的探测长度平均是1.5,不成功的是2.0。
字符串能够这样哈希化,每一个字符乘以常数的不一样次幂,求和,而后用取模操做符%缩减结果,以适应哈希表的容量。
若是在Horner方法中用多项式表达哈希化,每一步中都应用取模操做符,以避免发生溢出。
哈希表的容量一般是一个质数。这在二次探测和再哈希法中很是重要。
哈希表可用于外部存储,一种作法是用哈希表的单元存储磁盘文件的块号码。