Java中的哈希表
一、什么是哈希表?
- 数组更利于元素的查找;
- 链表更利于元素的插入和删除;
那么有没有一种数据结构可以同时具有数组和链表的优点呢?即能快速地查找又能高效地插入删除元素?很明显,本文的主角“哈希表”就能很好的满足这个要求。
那么哈希表是怎么做到两者的优点兼具的呢?这主要归功于它独特的数据结构。哈希表是由一块地址连续的数组空间构成的,其中每个数组都是一个链表,数组的作用在于快速寻址查找,链表的作用在于快速插入和删除元素,因此,哈希表可以被认为就是链表的数组,下图是一个哈希表的简单示意图:
哈希表结构示意图
按照上图的示例,我们就有一个问题,这些元素是怎么确定需要放置到哪个数组空间和其对应的链表上的呢?
一般而言,我们在存储元素的时候,会用到“散列方法”,它的作用就是尽量均衡地将元素分配到数组空间中,避免出现某个数组空间中的链表元素大大超过其它数组空间中链表元素个数的情况。
散列方法对比示意图
比较经典的散列函数有“除法散列法”、“平方散列法”、“斐波那契散列法”,这些散列方法在《数据结构》的教材中可以找到,但现在基本都不使用这些了,本文也不再重点讲述这些内容。但为了动态描述散列表是如何工作的,下面给出一个使用“除法散列法”的例子:
散列存储实例
除法散列法:index = value % arraySize
待插入数据:1,9,18,22,29
现在我们在内存中创建一个大小为5的数组空间,那么arraySize就为5。
- 插入元素1时,index=1%5=1,插入第一个数组空间;
- 插入元素9时,index=9%5=4,插入第四个数组空间;
- 插入元素18时,index=18%5=3,插入第三个数组空间;
- 插入元素22时,index=22%5=2,插入第二个数组空间;
- 插入元素29时,index=29%5=4,插入第四个数组空间,发现已经存在一个元素了,那么就在该元素后面增加一个链表节点进行存储;
当需要查找某个元素的时候,比如查找18,那么首先对其值进行散列,18%5=3,得到了数组空间地址为3,那么就去该地址对应的链表上逐个去寻找元素。
由此可以看出,当某个链表长度过长的话,查找效率就会急剧下降。因此,一个好的散列方法能确保各个元素均匀地分布到各个数组空间的地址上,避免某个链表过长,这对于哈希表的查找效率是非常重要的。
然而,即使我们拥有了一个好的哈希方法,倘若散列后还是会有很多元素对应同一个数组地址(哈希冲突),则还是会出现单个数组空间中链表元素过多过长的问题,这种情况下怎么解决查询效率慢的问题呢?
一种通用的做法是,将过长的链表转化为二叉查找树、平衡二叉树、红黑树,关于树的数据结构本文不再展开,只需要了解,它们是为了解决单条链表元素过多时查找效率慢的问题,详情可以参考《数据结构》。
代码演示:
构建学生结点
package com.ma.HashTable;
public class Student {
public int id;
public String name;
public Student next;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", next=" + next +
'}';
}
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
构建学生链
package com.ma.HashTable;
public class StudentLinkedList {
private Student head;
//添加结点
public void add(Student newStudent){
//如果添加的是第一个学生对象,则直接复制给第一个结点
if (head == null){
head = newStudent;
return;
}
Student temp = head;
while (true){
//判断下一个结点是否为空
if (temp.next == null){
break;
}
//继续往后找
temp = temp.next;
}
//直到下一个结点为空,将新学生对象复制给下一个结点
temp.next = newStudent;
}
//查询整个链表
public void list(int no){
System.out.println(head);
//判断是否为空
if (head == null){
System.out.println("第"+(no+1)+"条链表是空链表");
return;
}
//不为空则打印
Student temp = head;
while (true){
System.out.println("编号:"+temp.id+"\t姓名:"+temp.name);
if (temp.next == null){
break;
}
temp = temp.next;
}
System.out.println();
}
//根据学员编号查询结点
public Student findById(int id){
if (head == null){
return null;
}
Student temp = head;
while (true){
if (temp.id == id){
break;
}
if (temp.next == null){
temp = null;
break;
}
temp = temp.next;
}
return temp;
}
}
构建哈希表
package com.ma.HashTable;
public class HashTable {
private StudentLinkedList[] studentLinkedLists;
private int size;
public HashTable(int size) {
this.size = size;
studentLinkedLists = new StudentLinkedList[size];
//数组中添加链表对象
for (int i = 0; i < size; i++) {
studentLinkedLists[i] = new StudentLinkedList();
}
}
//哈希函数 每输入一个key 产生一个哈希值
public int hashCode(int sid){
return sid % size;
}
//添加学员
public void add(Student newStudent){
//决定数组中的下标
int hashVal = hashCode(newStudent.id);
//添加到指定的链表中
studentLinkedLists[hashVal].add(newStudent);
}
//查看哈希表中的数据
public void list(){
for (int i = 0; i < size; i++) {
studentLinkedLists[i].list(i);
}
}
//根据学员编号查询
public void findByStudentId(int sid){
int hashVal = hashCode(sid);
Student student = studentLinkedLists[hashVal].findById(sid);
if (student != null){
System.out.println("在第"+hashVal+"条链表中找到了该学员");
studentLinkedLists[hashVal].list(sid);
}else {
System.out.println("未找到该学员");
}
}
}
测试
package com.ma.HashTable;
public class HashTableTest {
public static void main(String[] args) {
Student student1 = new Student(1,"马一");
Student student2 = new Student(2,"马二");
Student student3 = new Student(3,"马三");
Student student4 = new Student(4,"马四");
Student student5 = new Student(5,"马五");
Student student6 = new Student(5,"马六");
HashTable hashTable = new HashTable(10);
hashTable.add(student1);
hashTable.add(student2);
hashTable.add(student3);
hashTable.add(student4);
hashTable.add(student5);
hashTable.add(student6);
hashTable.list();
hashTable.findByStudentId(5);
}
}
d(student2);
hashTable.add(student3);
hashTable.add(student4);
hashTable.add(student5);
hashTable.add(student6);
hashTable.list();
hashTable.findByStudentId(5);
}
}