本篇要介绍的是一种数据结构——哈希表。
首先回顾一下数组以及链表的内容,我们知道数组有一优点就是存储于数组中的元素便于查找,这跟它的连续存储密切相关。但是它的缺点也很明显,如果我们想添加或者删除一个元素可能需要大幅度的移动数组的元素,效率较低。再看看链表,我们知道链表这种数据结构的优点就是方便在其增加或者删除一个元素,但是查找效率又非常低(必须得从头节点遍历查找)。按照人们的惯性思考——是否能将这两种数据结构结合使用,来达到查询与增删效率上的“中和”呢?答案是可以的,所以就诞生了哈希表这种数据结构。
那么如果将两种数据结构结合使用呢?不难想到,将数组的每个存储单元存储一个链表即可。如图:
把这种数据结构想象成那种串着珠子的落地门帘是不是很形象,(具体叫啥我也不知道哈哈)。
注意:添加节点的过程就用散列函数实现随机,均匀分布在数组中的各个存储空间。如果节点大量集中在一两个存储空间上,那样哈希表这种数据结构就没有意义。
接下来给出一个具体的实例应用,通过这个实例来掌握哈希表这种数据结构:
实例内容:利用哈希表对员工进行操作(包括了添加员工,删除员工,查看员工等等操作)
提示:需要三个类实现,一个是员工类Emp,一个是操作链表的类EmpLinkedList,另一个类是用来管理所有链表上员工节点的类HashTab。
思路:每个员工分配一个编号,根据编号利用散列函数求出员工的具体操作位置,也就是在哪一条链表上进行操作(这里的散列函数用简单的取模运算实现),然后一系列等操作(增加,删除…)就在此链表上进行。
补充:对哈希表的操作实际就是对数组以及链表的操作,建议读者在了解了基本思路就可以自己动手编写,再来对比源码。
具体代码操作:
//管理所有链表上员工节点的类
class HasTab02{
int size;//数组大小
Emp2LinkedList[] emp2LinkedLists;//数组元素存放链表
//构造函数,初始化数组大小
public HasTab02(int size){
this.size = size;
emp2LinkedLists = new Emp2LinkedList[size];
//当我们创建了数组空间时,此时每个数组单元存储的元素都是null,所以要为每个单元创建对象
for (int i = 0;i < size;i ++){
emp2LinkedLists[i] = new Emp2LinkedList();
}
}
//添加
public void add(Emp2 emp2){
int no = HashFun(emp2.id);//获取添加Emp在数组中的位置
emp2LinkedLists[no].add(emp2);
}
//遍历
public void list(){
for (int i = 0;i < size;i ++){
emp2LinkedLists[i].list(i + 1);
}
}
//查找
public void find(int id){
int no = HashFun(id);
Emp2 emp2 = emp2LinkedLists[no].find(id);
if (emp2 == null){
System.out.println("没有在哈希表中找到该雇员");
}
System.out.printf("该雇员所在第%d条链表,id=%d,name=%s\n",no + 1,emp2.id,emp2.name);
}
//删除
public void del(int id){
int no = HashFun(id);
emp2LinkedLists[no].del(id);
}
//散列函数
public int HashFun(int id){
return id % size;
}
}
//雇员链表
class Emp2LinkedList{
private Emp2 head;
/**
* 添加方法
* @param emp2 传递雇员
*/
public void add(Emp2 emp2){
if (head == null){
head = emp2;
return;
}else {
Emp2 curEmp = head;//定义辅助指针
while (true){
if (curEmp.next == null){
break;
}
curEmp = curEmp.next;//后移
}
//循环结束,curEmp指向了链表最后一个元素
//默认再链表末尾加入雇员
curEmp.next = emp2;
}
}
/**
* 查找方法
* @param id 查找的id
* @return 雇员信息
*/
public Emp2 find(int id){
if (head == null){
System.out.println("链表为空");
return null;
}
//辅助指针
Emp2 curEmp = head;
while (true){
if (id == curEmp.id){
break;
}
//如果遍历到了最后还没找到直接赋值null
if (curEmp.next == null){
curEmp = null;
break;
}
curEmp = curEmp.next;
}
return curEmp;
}
/**
* 删除节点方法
* @param id 删除id
*/
public void del(int id){
if (head == null){
System.out.println("删除失败,链表为空");
return;
}
//辅助指针
Emp2 curEmp = head;
//如果要删除的是链表的第一个节点
if (head.id == id){
head = curEmp.next;
return;
}
while (true){
if (curEmp.next == null){
System.out.println("删除失败,没有该雇员");
return;
}
//找到待删除节点的前一个节点
if (curEmp.next.id == id){
break;
}
curEmp = curEmp.next;
}
//待删除节点的前一个节点指向待删除节点的后一个节点
curEmp.next = curEmp.next.next;
}
/**
* 遍历链表
*/
public void list(int no){
if (head == null){
System.out.printf("第%d条链表为空\n",no);
return;
}
System.out.printf("第%d条链表的信息为:",no);
//head不能动,创建辅助指针
Emp2 curEmp = head;
while (true){
System.out.printf("=>id=%d,name=%s\t",curEmp.id,curEmp.name);
if (curEmp.next == null){
break;
}
curEmp = curEmp.next;
}
System.out.println();
}
}
//雇员类
class Emp2{
public int id;
public String name;
public Emp2 next;
public Emp2(int id, String name) {
this.id = id;
this.name = name;
}
}
小小总结一下:哈希表就是数组跟链表的结合,将这两者的优缺点进行“中和”,得到的结果就是在效率上查询以及增删操作整体来看得到改善。如果单一来看,查询效率肯定是不如单一数组的,而增删操作效率肯定不如单一的链表。