JAVA哈希表的构建(拉链法)

  哈希表是一个用途很广泛的数据结构,常用于需要进行大集合搜索的地方,比如腾讯QQ。对于上线的用户我们需要将其添加到一个集合中,以便对其进行各种处理。那么这个集合该采取哪种数据结构呢?最基本的数据结构就两种:链表和数组。在前面的文章中,我们曾经比较过链表和数组的优缺点。链表适用于插入和删除操作较多的集合,但是不适用取值操作多的集合。而数组不适用于插入和删除操作较多的集合,但是适用于取值操作较多的集合。但很不幸的是,对于QQ而言。它既有很多插入删除操作也有很多取值操作。每当用户上下线,我们都需要立即将这个用户从集合中添加删除。而当用户上线时,我们需要将它与所有已经上线的用户比较一遍,来确定这个账号是不是已经在线了,防止重新登陆。这样一来,无论是链表还是数组,都无法很好地适用于这个情景。因此,今天我们就来介绍一个介于数组和链表之间的数据结构——哈希表。

一、哈希表的结构

哈希表又被称为数组链表。当插入删除操作和取值操作都较频繁时,我们可以采用哈希表来作为集合的数据结构。

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。大致结构如下

特点:

1.第一列是一个数组。也就是我们在查找每一个链表头结点的时间复杂度都是常数1;

2.每一行都是一个链表。理论上,这个链表可以无限扩大。实际当然是不行的,我们可以设想两种极端情况。一种是链表的长度远远大于头结点数组的长度,那么这时这个哈希表其实就相当于一个链表,它取值操作的时间复杂度还是接近n。另一种情况就是链表的长度远远小于头结点数组的长度,那么这时这个哈希白哦其实就相当于一个数组,它插入和删除操作的时间复杂度还是接近n。为了避免这两种极端情况的出现,我们引入了一个控制变量(当前哈希表的数据个数/数组长度)。如果这个值超过了某一界限,我们就对当前的哈希表进行重构。

3.每一次存放和取出数据,都是先找到对应的行号,再去遍历每一行中的各个数组

二、哈希表的构建思路

基本思路:首先我们需要开辟一个连续的数组来储存每个头结点,这个数组的大小是固定的。每当我们取到一个待加入的键值对时,首先将其封装成一个节点。然后根据key计算出相应的hashcode,这个hashcode会定位到唯一的一个链表头。最后就把数据放到这个链表里面。

需要实现的方法

1.添加数据put()

2.获取数据get()

3.返回当前哈希表的大小size()

4.展示当前的哈希表构成show()

5.哈希表的重构rehash()——私有方法

6.具体的添加数据的方法input()——私有方法

这个方法里面实现了具体的数据添加方法,其实就是把rehash()和put()两个方法的共同部分给提取了出来,实现代码的复用

三、源代码

//构建一个Hashtable类
public class Hashtable {
	
	//定义一个节点类,里面定义了每一个节点所需要的数据
	public class Node {
		Node next;//指向下一节点
		Object key;//键值
		Object data;//数据域
		
		//节点的构造函数
		public Node(Object key,Object data) {
			this.key=key;
			this.data=data;
		}
	}
	
	public Node[] Headlist=new Node[1];//申请一个定长数组
	public int size=0;//记录当前hash表的元素个数
	public float peakValue=1.7f;//定义一个峰值,如果当前hash存储的元素个数超过这个峰值就进行rehash
        
        //主函数入口
	public static void main(String[] args) {
		//定义一定数量的键值对
		String[] key= {"a","b","c","d","e","f","g","i"};
		String[] data= {"1","2","3","4","5","6","7","8"};
		
		//初始化哈希表
		Hashtable table=new Hashtable();
		for(int i=0;i<key.length;i++) {
			//将每一个键值对一一加到构造好的哈希表中
			table.put(key[i], data[i]);
			System.out.println("展示当前的hash表");
			//展示每一次添加数据之后的哈希表构成
			table.show();
		}
		for(int i=0;i<key.length;i++) {
			//根据键值从哈希表中获取相应的数据
			String data1=(String)table.get(key[i]);
			System.out.print(data1+" ");
		}
	}
	 
	//往哈希表中添加一个键值对
	public void put(Object key,Object data) {
		//判断当前的哈希表容量是否已经达到峰值,如果达到峰值,就hash表的重构
		if((size*1.0)/Headlist.length>peakValue) rehash();
		//调用hash函数获取键值对应的hashcode,从而定位到相应的头结点
		int index=hash(key,Headlist.length);
		//把当前的节点封装成Node节点类
		Node node=new Node(key,data);
		//加入哈希表
		input(node,Headlist,index);	
		size++;
	}
	
	//设计一个添加函数,实现代码的复用
	private void input(Node node,Node[] list,int index) {
		//如果头结点位置为空,则把当前节点赋值给头结点
		if(list[index]==null) {
			list[index]=node;
		}else {
			//否则,遍历该链表,并判断该键值是否已经存在于哈希表中,如果没有就将其加到链表尾部
			Node temp=list[index];
			//判断表头元素的键值是否和我们即将输入的键值一样
			if(temp.key==node.key) {
				System.out.println(temp.key+"--该键值已存在!");
			}else {
				while(temp.next!=null) {
					temp=temp.next;
					if(temp.key==node.key) {
						System.out.println(temp.key+"--该键值已存在!");
						break;
					}
				}
				temp.next=node;
			}
		}
	}
	
	
	//hash函数计算出键值对应的hashcode,也就是头结点的位置
	private Integer hash(Object key,int lenth) {
		Integer index=null;
		if(key!=null) {
			//进来的可能是一个字符串,而不是数字
			//先转化为字符数组
			char[] charlist=key.toString().toCharArray();
			int number=0;
			//依次计算出每个字符对应的ASCII码
			for(int i=0;i<charlist.length;i++) {
				number+=charlist[i];
			}
			//对哈希表的数组长度取余,得到头结点的位置
			index=Math.abs(number%lenth);
		}
		return index;
	}
	
	//rehash函数对当前的hash表进行扩展,重新定位当前表中的所有
	public void rehash() {
		//每次扩展都把当前的哈希表增大一倍
		Node[] newnode=new Node[Headlist.length*2];
		//遍历原来的哈希表,依次把每个数据重新添加到新的哈希表中
		for(int i=0;i<Headlist.length;i++) {
			if(Headlist[i]!=null) {
				//先把每个列表的头结点重新hash进去
				int headposition=hash(Headlist[i].key,newnode.length);
				//这个地方一定要用new重新构建一个新的节点来保存原来哈希表中节点的键值对。
				Node rehashheadnode=new Node(Headlist[i].key,Headlist[i].data);
				//设置它的下一个节点为空,这条代码不写也可以,这里为了强调它的重要性,特意将其写了出来
				//这条代码的作用就是去除原来哈希表中各个节点的关联关系
				rehashheadnode.next=null;
				input(rehashheadnode,newnode,headposition);
				Node temp=Headlist[i];
				while(temp.next!=null) {
					temp=temp.next;
					//定义一个Node类型的数据来储存需要rehash的数据
					Node rehashnextnode=new Node(temp.key,temp.data);
					rehashnextnode.next=null;
					int nextposition=hash(temp.key,newnode.length);
					input(rehashnextnode,newnode,nextposition);
				}
			}
		}
		//重新设置节点数组的引用
		Headlist=newnode;
	}
	
	//显示当前的hash表
	public void show() {
		for(int i=0;i<Headlist.length;i++) {
			if(Headlist[i]!=null) {
				System.out.print(Headlist[i].key+":"+Headlist[i].data+"-->");
				Node temp=Headlist[i];
				while(temp.next!=null) {
					temp=temp.next;
					System.out.print(temp.key+":"+temp.data+"-->");
				}
				System.out.println();
			}
		}
	}
	
	//获取键值相对应的数据
	public Object get(Object key) {
		//先获取key对应的hashcode
		int index=hash(key,Headlist.length);
		Node temp=Headlist[index];
		//先判断相应的头结点是否为空
		if(temp==null) return null;
		else {
			//判断节点中的key和待查找的key是否相同
			if(temp.key==key) return temp.data;
			else {
				while(temp.next!=null) {
					temp=temp.next;
					if(temp.key==key) return temp.data;
				}	
			}	
		}
		return null;
	}
	
        //返回当前hash表的大小
        public int length(){
                return size;
        }
 }
/*
*巨坑,原来的节点的下一个节点需要重新设置为null
*/

四、运行结果

五、总结反思

1.把一个String字符串转化为int型整数有两种意思。A.字符串本身是0-9的数值,我们把这个数值由字符串类型变为int类型。B.每个字符都有相应的ASCII码,而字符串是由字符组成的,因此我们可以获取它对应的ASCII的值。在这里我们要做的自然是第二种,而这个值我们就把它作为hashcode,通过这个值我们可以唯一找到每个键值key对应的头结点。

问题:对应字符相同,但是序列不同的字符串会不会出错。比如,“abbb”和“baaa”。这两个键值,他们对应的hashcode肯定是一样的,那么他们就会被映射到同一个头结点中,但是由于我们在判断重新的时候是直接比较键值,显然"abbb"!=“bbba”。所以即使哈希表中已经有了"abbb"这个键值,"bbba"也还是可以正常被加入到hash表中。

2.巨坑:在重构hash表的时候,一定要把原来hash表中各个节点的关联关系去掉!!!并且只能通过新建节点的方法,不能通过简单赋值。因为JAVA的引用和C++的指针不同。简单说就是如果我们令a=b;在JAVA中的意思是,a和b指向同一个内存地址的引用,如果你改变了a,b同样会被改变。而C++中的意思则是,额外开辟一个内存来保存b地址中的数据,这时不管你如何改变a的值,b的值都不会受影响。因此这里如果我们仅仅是通过节点的赋值来添加数据,原来hash表的结构很可能会在过程中被破坏,导致重构失败。(关于这个问题的详细解释,可以看我的另外一篇博客《JAVA赋值和C++赋值的区别》

猜你喜欢

转载自blog.csdn.net/Alexwym/article/details/81053470
今日推荐