一。对于查找表,我使用最多的莫过于Java中HashMap或者Python中的字典,即所谓的‘散列表’或者‘哈希表’, 是一种‘key-value ‘形式的树结构,记得大三时参加上海微创的面试,问了我一个算法题,然后我习惯性的使用HashMap进行了解答,之后面试官很鄙视的问我‘为什么要使用HashMap,而不使用数组’,当时我回答说‘因为HashMap是键值对的数据结构,处理起来方便一些’,如今回想起来,只能叹当时之才疏学浅,虽然现今也是功力尚浅。
二。查找表,底层可以是链表或者顺序表进行实现。查找表的分类:
(1)顺序查找表:
这种类型的其中一种实现,它的概念有一点像操作系统中的分页置换算法,即把最新查询数据放到表尾,就像在一个书架,你肯定是把刚看过的书放在最边上,之后要找书的话就从最边上开始查。
(2)有序查找表:
对于有序查找表,顾名思义,其必定先是有序的,然后使用三个指针,依次进行查找,如:
有 1 3 4 7 8 12 16 18 25 共9个元素 的有序表,
当你要查找18的位置时,过程为:
(i)三个指针刚开始分别指向1(a指针),8(b指针),25(c指针) 即分别是最左边,最中间,最右边
(ii)因为18是在8和25之间即b指针和c指针之间,所以把a指针指向b,c的中间数字即16
(iii)因为18又是在16和25之间即a和c之间,所以把b指针指向a,c的中间数字即18,而此时正好找到需要 查找的数字
示意图实在难于恭维,但也算是可以说明问题,望理解。
(3)静态查找树表:有关于最优二叉树,不再详述
(4)动态查找树表:不再详述
(5)hash表:hash表是本文的重点
三。hash:
什么是哈希?不可否认,哈希应用范围非常广,如在IP分类递增的转发算法中即使用了hash算法,而在网络信息安全的安全加密则有更多的身影即MD4,MD5,和SHA1,又如Oracle中的索引以及之前我习惯性使用的只懂怎么用不懂其原理的HashMap,而nosql技术中也多少使用了hash。那么究竟什么是hash?
在所有的数据结构中,应该来说数组的效率是最快的,因为当你声明一个一定尺寸的数组,则内存会为其开辟一个空间,而且是连续分配的,对于内存,我们想象它就是一个格子状的东西,每一个格子即每一个地址对应一个内容,而HashMap实质就是使用了数组进行表示,再实质一点就是‘用空间换时间’,不过应该很多所谓的提高效率的方法无非都是符合这个原则的,我们用例子来说明一下(以HashMap为例):
比如你要储存 ‘abc’,‘acb’,‘bcd‘三个字符串,则你可以先定义一个hash函数,一般来说hash函数是根据你要存储的数据的结构来定义的,在这里我们假设hash函数为
int hash(key){
return x*12+y*7+z //x,y,z分别为字符串的字符,在这里只是说明情况,没有考虑任何相关问题
}
假设我们的HashMap使用的数组为String str = new String[3];
再假设'abc’,'acb’,'bcd’的hash值即hash('abc') ,hash('acb'),hash(’bcd‘)的值分别为3,4,5,为了不让下标超过数组尺寸,再对其分别取余(以数组长度为除数),即可以得出0,1,2, 则把这三个字符串分别放入str数组的0,1,2下标对应的位置
数据已经完成存储过程,这时要对数据进行查询,如要查询'abc',则只要再做一遍hash与取余过程,得到数字0,则直接定位到数组的第0个下标处,至此整个过程结束。
整个过程关键的无非是hash函数的确定,以得到一个‘好’hash值,另外也是为了防止‘冲突’,即有时候key1 != key2,但很可能hash(key1)==hash(key2),所以应该避免出现这种冲突情况,而万一出现的冲突,也有几个解决方案,如再hash法即再生成一个不一样的hash值,或者使用‘挂链法’,即对出现冲突情况,把这些数据在同一下标之后再使用链表连接起来,总之,冲突情况只能尽量避免不可能完全杜绝,而且冲突情况也会对整个效率有很大的影响,还有一个很重要的问题就是‘装载因子’,因为当底层的数组容量已满时,肯定需要增容,而装载因子就是一个增容幅度即‘加速度’,而这也会很大程度影响效率,因为当装载因子太大,则可能会使空间浪费过大,而装载因子太小,则可能出现频繁增容的情况,即频繁的数组内容转移。
四。直接贴代码:
package com.ds.test4;
public class HashMap<K,V> {
private Entry[] table ; //底层的数组
private int initCapacity ; // 初始容量
private double loadFactor ; // 装载因子,一般是0-1之间
private int size = 0; //元素个数
public HashMap(){
this.initCapacity = 16;
this.loadFactor = 0.75;
table = new HashMap.Entry[initCapacity]; //内部类必须注明是哪个类,即Out.in
}
public void put(K k,V v){
int hash = hash(k);
checkCapcacity(); //判定容量
size++;
table[hash] = new Entry(hash,k,v);
}
private void checkCapcacity() {
if(size>=initCapacity){
initCapacity+=initCapacity*loadFactor;//使用装载因子进行增容
Entry[] newTable = new HashMap.Entry[initCapacity];
for(int i = 0;i<size;i++){
newTable[i] = table[i];
}
}
}
public V get(K k){
int hash = hash(k); //先产生hash值,再查找
return table[hash].v;
}
public int hash(K k){
int hash = k.hashCode();//使用的jdk的hashCode方法
return hash;
}
class Entry{
private int hash;
private K k;
private V v;
public Entry(int hash,K k,V v){
this.hash = hash;
this.k = k;
this.v = v;
}
}
}
(1)自己的代码只是简单的说明了一下问题,对于hash函数直接使用了jdk的hashCode方法。
(2)也没有对get方法进行检查,因为当出现没有查询到元素的情况应该进行处理
六。总结:
知其然而不知其所以然,永远不会进步!