《算法4》散列表
来源:互联网 发布:淘宝人生全本阅读 编辑:程序博客网 时间:2024/05/29 15:04
散列表也是一种符号表,主要特征是可以将键通过散列函数映射为一个数组索引,然后利用这个数组索引就可以做很多东西。
散列函数
当我们输入一个对象,不论这是个什么东西,经过散列函数处理之后输出一个0到M-1的范围之内的整数。
对于散列函数有一些要求:
1. 相等的对象(使用equals()函数)的散列值是相同的
2.同样的散列值不同的两个对象不相等
3.在输出范围之内尽量均匀分布
但是哈希函数是和对象类型有关的,一般来说对于每种类型的键我们都需要与之对应的哈希函数。对于Java来说,每个Object对象都有一个hashCode()函数,但是它的默认实现是返回对象内存地址,所以是没有用处的,对于一些常见的类型比如,Integer,Double,String,File,URL,Java重写了hashCode(),这里我不管它具体怎么实现的,只需要用就好了,值得注意的是hashCode()返回的可能有负数。
一个hash函数的实现方式
private int hash(Key key){ return (key.hashCode() & 0x7fffffff)%M; }
其中之所以要和0x7fffffff进行与运算就是要去掉符号位的影响,这样就不会有负数的问题了,然后就将结果对M取余数,一般这个M就是一个比较大的质数,之所以是质数,是因为这样可以将结果均匀地散列到0到M-1之间。对于自定义的对象,可以采用组合的方式得到自己的hash函数,比如对于Date类型,我们有
int hash = (((day*R+month)%M)*R+year)%M;
均匀性对于散列函数来说是很重要的,但是这里我们不仔细考虑,只是假设它能够均匀且独立地将所有的键散步到0和M-1之间
下面介绍两种实现散列表的方式,分别基于拉链发和线性探测法。
基于拉链法的散列表(SeparateChaining)
假设键的数目为N,数组大小为M,一般对于拉链法,N是大于M的。我们将某个键散列到0到M-1中的一个数,那么随着键的数目的增加,两个键之间一定会有重复的索引,这就发生了所谓的碰撞冲突,拉链法解决碰撞冲突的方法就是每个数组位置保存一个链表的引用,每个新加入的键先找到数组的位置,然后插入对应的链表。查找的时候同样的,先对要查找的键进行散列,然后到相应位置的链表中查找。对于拉链法,每个链表的平均长度为
下面是相应的代码实现:
public class SeparateChainingHashST<Key, Value> { private int N;//键的数量 private int M;//数组容量 private SequentialSearchST<Key, Value>[] st; public SeparateChainingHashST(){ this(997);//数组容量为997 } public SeparateChainingHashST(int M){ this.M = M; st =(SequentialSearchST<Key, Value>[]) new SequentialSearchST[M]; for (int i = 0;i<M;i++){ st[i] = new SequentialSearchST<Key, Value>(); } } private int hash(Key key){ return (key.hashCode() & 0x7fffffff)%M; } public Value get(Key key){ return (Value)st[hash(key)].get(key); } public void put(Key key ,Value val) { st[hash(key)].put(key, val); } }
这里利用的是线性列表,需要的可以参考下面的代码:
public class SequentialSearchST<Key, Value> { private Node first; private class Node{ Key key; Value val; Node next; public Node (Key key,Value val, Node next){ this.key = key; this.val = val; this.next = next; } } public Value get(Key key){ for (Node x= first;x!=null;x=x.next){ if (key.equals(x.key)) return x.val; } return null; } public void put(Key key, Value val){ for (Node x= first;x!=null;x=x.next){ if (key.equals(x.key)) {x.val = val;return ;} } first = new Node(key, val, first);//new一个节点,它的next是first然后将first指向它。 }}
因为每个链表的平均长度为
基于线性探测法的散列表(LinearProbing)
对于线性探测法,数组容量是大于键的数量的,并且在后面可以看到,数组不能太满,否则影响性能。主要思想是,我们维护两个数组,一个是键的数组,一个是值得数组,当我们将一个键散列到数组中的时候,如果当前位置是空的,那么就直接插入,如果已经有了元素,那么就往下一个位置插入,如果还是被占了,那就继续,直到找到一个空位置,然后再插入。查找的时候也是一样,根据键散列的位置我们去查找,如果当前位置的键和要查找的键不相同,那么就继续往后查找,要么找到,要么又碰到空的位置,那么此时就是查找未命中。看着下面的图,就能对这个过程有着清楚地了解。
删除
线性探测法的一个重要的操作是删除,但是删除不能仅仅将某个键置为null,因为这样如果它后面本来还有的键就可能因为这个null键而访问不到,我们的做法是将这个置为null之后直到下一个null键之间的数据重新加入散列表。代码见后面的delete()方法。
调整大小
对于线性探测甚至拉链法,我们都需要调整数组大小来保证性能。对于线性探测法,我们需要新建一个LinearProbingHashST()对象,只是新建对象的时候要扩大容量,然后把当前对象的数据重新put()进新的对象里面,最后把新对象的两个数组的引用传给当前数组。
下面是线性探测法的代码
public class LinearProbingHashST<Key, Value> { private static final int INIT_CAPACITY = 4; private int n; private int m; private Key[] keys; private Value[] vals; public LinearProbingHashST(){ this(INIT_CAPACITY); } public LinearProbingHashST(int capacity){ m = capacity; n=0; keys = (Key[]) new Object[m]; vals = (Value[]) new Object[m]; } public int size(){ return n; } public boolean isEmpty(){ return size()==0; } public boolean contains(Key key) { if (key == null) throw new IllegalArgumentException("argument to contains() is null"); return get(key) != null; } private int hash(Key key){ return (key.hashCode() & 0x7fffffff)%m; } private void resize(int capacity){ LinearProbingHashST<Key, Value> temp =new LinearProbingHashST<Key, Value>(capacity); for(int i=0;i<m;i++){ if(keys[i] != null){ temp.put(keys[i], vals[i]); } } keys = temp.keys; vals = temp.vals; m = temp.m; } public void put(Key key, Value val){ if (key == null) throw new IllegalArgumentException("first argument to put() is null"); if (val == null){ delete(key); return; } if (n>m/2) resize(2*m); int i; for(i = hash(key);keys[i]!=null;i=(i+1)%m){ if (keys[i].equals(key)){ vals[i] = val; return; } } keys[i] = key; vals[i] =val; n++; } public Value get(Key key){ if (key == null) throw new IllegalArgumentException("first argument to put() is null"); for(int i = hash(key);keys[i]!=null;i=(i+1)%m){ if (keys[i].equals(key)){ return vals[i]; } } return null; } public void delete(Key key){ if (key == null) throw new IllegalArgumentException("argument to delete() is null"); if(!contains(key)) return ; int i = hash(key); while(!key.equals(keys[i])) i=(i+1)%m; keys[i] = null; vals[i] = null; i=(i+1)%m; while(keys[i]!=null){ Key keyRedoKey = keys[i]; Value valReDoValue = vals[i]; keys[i] = null; vals[i] = null; n--; put(keyRedoKey, valReDoValue); i = (i+1)%m; } n--; if (n>0 && n==m/8) resize(m/2); assert check(); } private boolean check(){ if (m<2*n){ System.err.println("Hash table size m = " + m + "; array size n = " + n); return false; } for (int i=0; i<m;i++){ if (keys[i] ==null) continue; else if (get(keys[i])!= vals[i]){ System.err.println("get[" + keys[i] + "] = " + get(keys[i]) + "; vals[i] = " + vals[i]); return false; } } return true; } public Iterable<Key> keys(){ Queue<K``y> queue = new Queue<Key>(); for (int i=0;i<m;i++) if (keys[i]!=null) queue.enqueue(keys[i]); return queue; }}
分析总结
在一张大小为M并且含有
可以看出当
- 《算法4》散列表
- 算法导论-----散列表
- 《算法》—散列表
- 《算法导论》散列表
- 算法---散列表
- 算法-----链接法散列表
- 算法导论之散列表
- 算法导论小结-散列表
- 算法导论—散列表
- 算法--查找--散列表查找
- 查找算法—散列表
- 算法原理系列:散列表
- 算法(3.4 散列表)
- Hash算法与散列表基础算法
- 实用算法实践-第 4 篇散列表
- 算法列表
- 《算法导论》第11章 散列表 (2)散列表
- 算法----乘法连接法散列表
- 第二次作业
- SpringMVC重定向RedirectAttributes的使用
- Android基础篇——聊聊Activity
- Hbase伪分布式环境搭建
- 解析json数据
- 《算法4》散列表
- PMP引论之OPM-20171030
- NodeJs学习之路(1)
- jQuery选择器之层级选择器
- Hibernate配置文件详解
- 数据结构与算法分析—循环队列的数组实现(C语言)
- javascript--闭包
- 汇编——在屏幕中间显示不同颜色的字符串
- MapReduce中map与reduce的个数