文章目录
前言
今天学习的内容是哈希表,然后通过手写一个简易的动态哈希表来检验自己对于哈希表的理解。
一、分析哈希表的组成
通过学习哈希表,我们可以知道,哈希表本质上就是根据key值寻找对应的value值的数据结构。key到value的过程中需要一个散列函数来联系两个值,而散列函数的好坏很大程度上决定了冲突发生的次数。
在本文中,我们使用链地址法解决冲突问题。
因此我们的设计是:动态数组+链表+哈希函数
二、实现非动态哈希表
1.数组定义+链表定义
数组定义代码如下(示例):
public int initiatesize = 16;//定义初始容量
public int length =initiatesize;//如果没有发生二次装载,就等于初始大小为16
public int buksize=0;//这个代表现在哈希表里的元素个数
public list[] linkArr=new list[length];//数组
public void initialize(int lenth){//初始化
//每一个数组里存储一个链表头结点
buksize=0;
for(int i = 0; i< lenth; i++){
linkArr[i]=new list();
linkArr[i].size=0;
}
}
链表定义与基本功能实现代码(示例如下):
这个地方的基本功能大家可以详细看一看,以便理解后面的代码
class node{
public int data;
public Object key;
public node next;
public node(int data,Object key) //数据初始化
{
this.data=data;
this.key=key;
}
public node(){}//方法重写
}
class list {
public node root;//根结点
public node last;//尾结点
public int size=0;
public list() {
root = new node();
}
//在尾部增加元素
public int add(int data,Object key,int buksize) {
node temp = new node(data,key);
node head = root.next;//头结点
node p=head;
int flag=0;
if (head == null) {
root.next = temp;//开辟的新空间作为头
last = temp;
buksize++;
} else {
while(p!=null)
{//如果两次输入同一个key不同的value我们就用后面的value替换前面的
if((key!=null&&key.equals(p.key)))
{
p.data=data;
flag=1;
}
p=p.next;
}
if(flag==0)
{
buksize++;
last.next = temp;
last = temp;
}
}
size++;
return buksize;
}
//返回指定位置的节点
public int getnode(Object key) {
node p = root.next;
for (int i = 0;; i++) {
if(p==null)
break;
else if((key!=null&&key.equals(p.key)))
return p.data;
else if(p.key==null&&key==null)//这个是为了读取key值为null时对应的value
return p.data;
p=p.next;
}
return -1;
}
public int getSize(){
return size;
}
//输出链表
public void printf(int n) {
node p = root.next;
for (int i = 0; i < n; i++) {
if(p==null)
{
//System.out.println("null!");
break;
}
System.out.println("key:"+p.key+" data:"+p.data);
p = p.next;
}
}
}
2.哈希函数的设计
哈希函数设计代码如下(示例):
//哈希函数
public int hash(Object key,int lenth){
int hashcode=key.hashCode();
int index=hashcode%lenth;//取余数法
//System.out.println(index+"+"+lenth);
return Math.abs(index);//abs是取绝对值
}
3.基本功能put与get
在实现这两个功能的时候都要注意这里的哈希表允许key为null的情况出现,所以多加一层判定讨论
以下是put功能代码示例:
public void put(Object key,int value,list[] linkArr){
int index;
if(key!=null)
{
index= hash(key,length);
this.buksize=linkArr[index].add(value,key,buksize);
}
else//key为空的时候默认存放到linkArr[0]的链表后面
{
this.buksize=linkArr[0].add(value,key,buksize);
}
}
以下是get功能方法示例:
//获取元素
public void get(Object key){
if(key!=null)
{
int index = hash(key, length);
System.out.println("value="+linkArr[index].getnode(key));
}
else
{
System.out.println("value="+linkArr[0].getnode(key));
}
}
三、修改为动态哈希表
1.装载因子与二次散列
在动态的设计上,我们采用的方式是引入装载因子来进行是否二次散列动态扩容的判断。
装载因子:反映了哈希表汇总元素填满的程度
二次装载:通常我们会选择将哈希表扩容为2倍,对原先的键值对进行二次散列以完成动态扩容
1)定义
public int initiatesize = 16;//定义初始容量为16
public int num=100;//现在需要放进的数量
public int buksize=0;//目前哈希表里的元素个数
public int length =initiatesize;//如果没有发生二次装载,就等于初始大小
public float load=0.75f;//定义装载因子
2)存放时的处理
因为现在要建设动态哈希表,所以我们需要在put方法添加一行代码判断是否需要resize
public void put(Object key,int value,list[] linkArr){
//加上对是否需要扩容的判断
int index;
if(buksize>=linkArr.length*load)
{
linkArr=resize() ;//二次装载
}
if(key!=null)
{
index= hash(key,length);
this.buksize=linkArr[index].add(value,key,buksize);
}
else
{
this.buksize=linkArr[0].add(value,key,buksize);
}
}
3)resize扩容功能的实现
先将长度修改为两倍,创建一个新的数组并对新数组进行初始化;
再调用rehash函数调整原有键值对的位置;
最后将原来的数组指针修改为新数组,并返回原数组;
//扩容
public list[] resize()
{
length *=2;
list[] newlinkArr = new list[length];
// 对新数组的初始化
for(int i = 0; i< length; i++)
newlinkArr[i] = new list();
reharsh(newlinkArr);
System.out.println("完成 rehash");
linkArr = newlinkArr;
// System.out.println(linkArr.length);
return linkArr;
}
4)rehash功能的实现
//重新散列
public void reharsh(list[] newlinkArr)
{
buksize=0;//更新现在的元素个数
for(int i=0;i<linkArr.length;i++)
{
if(linkArr[i].root.next==null)
continue;
node temp =linkArr[i].root.next;
while(temp!=null)
{
put(temp.key,temp.data,newlinkArr);
//非空就将键值对都放进新数组里面
temp=temp.next;
}
}
}
现在一个动态的哈希表就完成了,我们可以自行控制输入数据的数量,哈希表的初始容量为16但是遇见超出装载因子的情况时,可以自行扩容。
四、主函数与所有代码
1.主函数
主函数这里为了方便测试,我直接使用了18,12的倍数输入,可能没有办法很好的反映哈希表的效果
然后就是添加了(null,5)对实现key为空时哈希表功能的检测
以及添加了(198,140)对实现key相同时哈希表覆盖功能的检测
//主函数
public static void main (String[] args)
{
HashMap hm = new HashMap();
hm.initialize(hm.length);
for(int i = 1; i< hm.num; i++){
//System.out.println("put:"+"key:"+i*18+" value:"+i*12);
hm.put(i*18,i*12, hm.linkArr);
}
hm.put(198,140, hm.linkArr);//重叠就修改数据
hm.put(null,5, hm.linkArr);//key值为空的数据
for(int i = 0; i<hm.length; i++)
{
if(hm.linkArr[i]!=null) {
System.out.println(i+":");
hm.linkArr[i].printf(hm.linkArr.length);
}
}
Object a=null;
System.out.println("get key "+a +" :");
hm.get(a);
System.out.println("get key "+"198" +" :");
hm.get(198);
}
2.所有的代码
public class HashMap {
public int initiatesize = 16;//定义初始容量为16
public int num=32;//现在需要放进的数量
public int buksize=0;
public int length =initiatesize;//如果没有发生二次装载,就等于初始大小
public float load=0.75f;//定义装载因子
public list[] linkArr=new list[length];//数组
public void initialize(int lenth){//初始化
buksize=0;
for(int i = 0; i< lenth; i++){
linkArr[i]=new list();
linkArr[i].size=0;
}
}
//增加元素
public void put(Object key,int value,list[] linkArr){
//加上对是否需要扩容的判断
int index;
if(buksize>=linkArr.length*load)
{
linkArr=resize() ;//二次装载
}
if(key!=null)
{
index= hash(key,length);
this.buksize=linkArr[index].add(value,key,buksize);
}
else
{
this.buksize=linkArr[0].add(value,key,buksize);
}
}
//扩容
public list[] resize()
{
length *=2;
list[] newlinkArr = new list[length];
// initialize(lenth);
for(int i = 0; i< length; i++)
newlinkArr[i] = new list();
reharsh(newlinkArr);
System.out.println("完成 rehash");
linkArr = newlinkArr;
// System.out.println(linkArr.length);
return linkArr;
}
//重新散列
public void reharsh(list[] newlinkArr)
{
buksize=0;
for(int i=0;i<linkArr.length;i++)
{
if(linkArr[i].root.next==null)
continue;
node temp =linkArr[i].root.next;
while(temp!=null)
{
put(temp.key,temp.data,newlinkArr);
temp=temp.next;
}
}
}
//获取元素
public void get(Object key){
if(key!=null)
{
int index = hash(key, length);
System.out.println("value="+linkArr[index].getnode(key));
}
else
{
System.out.println("value="+linkArr[0].getnode(key));
}
}
//散列函数
public int hash(Object key,int lenth){
int hashcode=key.hashCode();
int index=hashcode%lenth;
//System.out.println(index+"+"+lenth);
return Math.abs(index);
}
//主函数
public static void main (String[] args)
{
HashMap hm = new HashMap();
hm.initialize(hm.length);
for(int i = 1; i< hm.num; i++){
//System.out.println("put:"+"key:"+i*18+" value:"+i*12);
hm.put(i*18,i*12, hm.linkArr);
}
hm.put(198,140, hm.linkArr);//重叠就修改数据
hm.put(null,5, hm.linkArr);//key值为空的数据
for(int i = 0; i<hm.length; i++)
{
if(hm.linkArr[i]!=null) {
System.out.println(i+":");
hm.linkArr[i].printf(hm.linkArr.length);
}
}
Object a=null;
System.out.println("get key "+a +" :");
hm.get(a);
System.out.println("get key "+"198" +" :");
hm.get(198);
}
}
class node{
public int data;
public Object key;
public node next;
public node(int data,Object key) //数据初始化
{
this.data=data;
this.key=key;
}
public node()//方法重写
{
}
}
class list {
public node root;//根结点
public node last;//尾结点
public int size=0;
public list() {
root = new node();
}
//在尾部增加元素
public int add(int data,Object key,int buksize) {
node temp = new node(data,key);
node head = root.next;//头结点
node p=head;
int flag=0;
if (head == null) {
root.next = temp;//开辟的新空间作为头
last = temp;
buksize++;
} else {
while(p!=null)
{
if((key!=null&&key.equals(p.key)))
{
p.data=data;
flag=1;//重复替换完成
}
p=p.next;
}
if(flag==0)
{
buksize++;
last.next = temp;
last = temp;
}
}
size++;
return buksize;
}
//返回指定位置的节点
public int getnode(Object key) {
node p = root.next;
for (int i = 0;; i++) {
if(p==null)
break;
if((key!=null&&key.equals(p.key)))
return p.data;
if(p.key==null&&key==null)
return p.data;
p=p.next;
}
return -1;
}
public int getSize(){
return size;
}
//输出函数
public void printf(int n) {
node p = root.next;
for (int i = 0; i < n; i++) {
if(p==null)
{
//System.out.println("null!");
break;
}
System.out.println("key:"+p.key+" data:"+p.data);
p = p.next;
}
}
}
总结
以上就是今天要讲的内容,如果有什么问题欢迎评论区里指出。然后后面如果有时间我可能会再写一篇手写实现链表的一些功能的博客。