太久不写技术博客了OTZ才发现已经写过链表和数组队列的实现了。
在大家嫌弃链表的遍历太慢数组的增删太麻烦的时候!当当当——哈希表 就可以派上用场了。
就像上面说的,人们理想的情况是一次就能存取要的元素,所以就在该元素的存储的位置和它的关键字之间建立了一个的特定的对应关系,使他们一一对应,这个关系就叫做
哈希函数或散列函数。只要根据这个对应关系f就能找到关键字K的像f(k)。假如不同的关键字对应了同样的存储地址的时候,这就是发生了
冲突!根据散列函数f(k)和处理碰撞的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的像f(k)作为记录在表中的存储位置,这种表便称为散列表或哈希表。
一般呢处理冲突的方法有下面几种:
1.开放寻址法:
Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
1.1. di=1,2,3,…,m-1,称线性探测再散列;
1.2. di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)称二次探测再散列;
1.3. di=伪随机数序列,称伪随机探测再散列。
2. 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生。
3. 链地址法(拉链法):在数组内建立链表,拉链是一个特别形象的说法,如图:
4. 建立一个公共溢出区
这里简单实现了一下第一种和第三种方法
第一种:
package com.hnu.wk0412HashMap;
public class MyHashTable {
private Object[] key; //存关键字的数组
private int size; //数组大小
/*
* 构造函数初始化
*/
public MyHashTable(){
key= new Object[16]; //默认数组长度是16
size = 0;
}
public MyHashTable(int arraylenght,int size){
key=new Object[arraylenght];
this.size=size;
}
/*
* 哈希函数
*/
public int hash(Object obj){
return Math.abs(obj.hashCode())%key.length;
//注:调用的是JDK里自有的hashcode方法实现映射
}
/*
* 处理冲突的函数
*/
public int handle(Object obj){
int newhash = Math.abs(obj.hashCode())%(key.length-1);
if(newhash%2==0){
return newhash + 1;
}
return newhash;
}
//判断哈希表里面是否已有某对象
public boolean contains(Object obj){
int i = hash(obj);
while (key[i] != null){
if(key[i].equals(obj)){
return true;
}
i = (i + handle(obj))%key.length;
}
return false;
}
//添加新元素
public void add(Object obj){
//先判断是否需要扩容,若已有数据占原数组一半,则扩容
if(size>=key.length/2){
this.rehash();
}
int i = hash(obj);//获取其哈希值
while(key[i] != null){
if(obj.equals(key[i])){
return;
} //判断该索引处是否已占用,若占用则调用handle生成新地址
i = (i + handle(obj))%key.length;
}
key[i] = obj;
size ++;
}
//扩大哈希表为原表的四倍,并把原来的哈希表添加到新表中
public void rehash(){
MyHashTable ht = new MyHashTable();
ht.key = new String[this.key.length * 4];
for(int i = 0; i < this.key.length; i ++){
if((this.key[i] != null)){
ht.add(this.key[i]);
}
}
this.key = ht.key;
this.size = ht.size;
}
//删除某个元素
public void remove(Object obj){
if(this.contains(obj)){
int i = this.getIndex(obj);
this.key[i] = null;
}
}
//得到某对象在哈希表中的位置
public int getIndex(Object obj){
int i = this.hash(obj);
while(this.key[i] != null){
if(this.key[i].equals(obj)){
return i;
}
i = (i + this.handle(obj))%this.key.length;
}
return -1;
}
//获取哈希表中某元素
public Object get(Object obj){
if(this.contains(obj)){
int i = this.getIndex(obj);
return key[i] ;
}
return null;
}
//输出哈希表中所有元素
public void print(){
for(int i = 0; i < key.length; i ++){
System.out.println(i+":"+key[i]);
}
}
//哈希表存储元素的个数
public int size(){
return this.size;
}
//哈希表的长度
public int length(){
return this.key.length;
}
}
第三种拉链法:
package com.hnu.wk0413哈希表;
public class MyHashTable<K, V> {
private int size;// 当前大小
private static int INIT_CAPACITY = 16;// 初始默认大小
private Entry<K, V>[] container;// 实际存储数据的数组对象
private static float LOAD_FACTOR = 0.75f;// 装载因子
private int max;// 能存储的最大的数目=capacity*factor
// 构造函数初始化
public MyHashTable(int init_Capaticy, float load_factor) {
if (init_Capaticy < 0)
throw new IllegalArgumentException("Illegal initial capacity: "
+ init_Capaticy);
if (load_factor <= 0 || Float.isNaN(load_factor))
throw new IllegalArgumentException("Illegal load factor: "
+ load_factor);
this.LOAD_FACTOR = load_factor;
max = (int) (init_Capaticy * load_factor);
container = new Entry[init_Capaticy];
}
// 默认无参的构造器
public MyHashTable() {
this(INIT_CAPACITY, LOAD_FACTOR);
}
/**
* 存储的方法 put
*
* @param k,v
* @return true/false
*/
public boolean put(K k, V v) {
// 此处调用JDK给出的hashCode()方法来计算hash值
int hash = k.hashCode();
//将所有信息封装为一个Entry
Entry<K,V> temp=new Entry(k,v,hash);
//若指定位置能够存储进去则返回true,否则false
if(setEntry(temp, container)){
size++;
return true;
}
return false;
}
/**
* 扩容的方法 rehash
* @param newSize 扩充后大小
*/
private void rehash(int newSize) {
// 声明新数组
Entry<K, V>[] newTable = new Entry[newSize];
max = (int) (newSize * LOAD_FACTOR);
// 遍历已经存储的元素,存放进新数组
for (int j = 0; j < container.length; j++) {
Entry<K, V> entry = container[j];
while (entry!=null) {
setEntry(entry, newTable);
entry = entry.next;
}
}
// 改变指向
container = newTable;
}
/**
* 将指定的结点temp添加到指定的hash表table当中的方法
* @param temp,table
* @return true-添加成功/false-该位置已存在结点
*/
private boolean setEntry(Entry<K,V> temp,Entry[] table){
// 根据hash值找到下标
int index = indexFor(temp.hash, table.length);
//根据下标找到对应元素
Entry<K, V> entry = table[index];
if (entry != null) {
// 遍历整个链表,判断是否相等
while (entry != null) {
//判断相等的条件时应该注意,除了比较地址相同外,引用传递的相等用equals()方法比较
//相等则不存,返回false
if ((temp.key == entry.key||temp.key.equals(entry.key)) && temp.hash == entry.hash&&(temp.value==entry.value||temp.value.equals(entry.value))) {
return false;
}
//不相等则比较下一个元素
else if (temp.key != entry.key && temp.value != entry.value) {
//到达队尾,中断循环
if(null==entry.next){
break;
}
// 没有到达队尾,继续遍历下一个元素
entry = entry.next;
}
}
//当遍历到了队尾,如果都没有相同的元素,则将该元素挂在队尾
addEntry2Last(entry,temp);
}
// 若不存在,直接设置初始化元素
setFirstEntry(temp,index,table);
return true;
}
/**
* 将元素直接添加在队尾
* @param entry,temp
*/
private void addEntry2Last(Entry<K, V> entry, Entry<K, V> temp) {
if (size > max) {
rehash(container.length * 4);
}
entry.next=temp;
}
/**
* 将指定结点temp,添加到指定的hash表table的指定下标index中
* @param temp ,index,table
*/
private void setFirstEntry(Entry<K, V> temp, int index, Entry[] table) {
// 1.判断当前容量是否超标,如果超标,调用扩容方法
if (size > max) {
rehash(table.length * 4);
}
// 2.不超标,或者扩容以后,设置元素
table[index] = temp;
//!!!!!!!!!!!!!!!
//因为每次设置后都是新的链表,需要将其后接的结点都去掉
//NND,少这一行代码卡了哥哥7个小时(代码重构)
temp.next=null;
}
/**
* 取出元素
*
* @param k
* @return
*/
public V get(K k) {
Entry<K, V> entry = null;
// 计算K的hash值
int hash = k.hashCode();
// 根据hash值找到下标
int index = indexFor(hash, container.length);
// 根据index找到链表
entry = container[index];
// 若链表为空,返回null
if (null == entry) {
return null;
}
// 若不为空,遍历链表,比较k是否相等,如果k相等,则返回该value
while (null != entry) {
if (k == entry.key||entry.key.equals(k)) {
return entry.value;
}
entry = entry.next;
}
// 如果遍历完了不相等,则返回空
return null;
}
/**
* 根据hash码,容器数组的长度,计算该哈希码在容器数组中的下标值
*
* @param hashcode , containerLength
* @return
*/
public int indexFor(int hashcode, int containerLength) {
return hashcode & (containerLength - 1);
}
/**
* 用来实际保存数据的内部类
* @param <K>key , <V> value
*/
class Entry<K, V> {
Entry<K, V> next;// 下一个结点
K key;// key
V value;// value
int hash;// 这个key对应的hash码,作为一个成员变量,当下次需要用的时候可以不用重新计算
// 构造方法
Entry(K k, V v, int hash) {
this.key = k;
this.value = v;
this.hash = hash;
}
}
}
关于其中的装载因子部分有一篇博文说的很清晰:
HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
http://blog.csdn.net/ghsau/article/details/16890151
虽然说哈希表在增删查改四个操作方面效率高,但是它的建立过程所花的时间是要比他们更多的,所以说,一切都是平衡哒,在时间上高效,却会增加了空间上的开销,在操作上高效,但是整个表的建立上却会花费更多时间。
(PS:这篇博客在写得过程中断了两次网,代码由于浏览器的原因一直不能插入……我要去冷静冷静……这次的总结就先到这里……下次再将链表数组队列哈希表在时间上的开销做一个对比……)