哈希表基础理论
哈希表(Hash table)
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表一般都是用来判断一个元素是否出现在集合里。要枚举的话,时间复杂度是O(n),用哈希表可以做到O(1)查询。
哈希表在内存布局中,可以起到缓存层的作用,不用频繁的访问数据库。
哈希函数
通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数就叫做散列函数,存放记录的数组就做散列表(哈希表)。
通过hashCode把要存储的内容转换为数值,通过特定编码方式,将其他数据格式转化为不同的数值,就可以把存储内容映射到哈希表上的索引数字。
这里有这样一个问题:
如果hashCode计算得到的数值大于了哈希表的大小了,那么久可以进行一个取模的操作,让它映射到哈希表上。
那么当然也就会出现存储数目大于哈希表的情况,这时候,就涉及到了哈希碰撞。
哈希碰撞
哈希碰撞,两个存储内容都映射到一个索引,就会发生哈希碰撞。
两个解决办法:
拉链法
两个按照链表的形式,都映射在一个索引。
拉链法要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了a,那么就向下找一个空位放置b的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。
常见的哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
set
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 是否能更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
str:unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
map
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 是否能更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可以重复 | key不可以修改 | O(log n) | O(log n) |
std::multimap | 红黑树 | key有序 | key可以重复 | key不可以修改 | O(log n) | O(log n) |
str:unordered_map | 哈希表 | key无序 | key不可以重复 | key不可以修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
题目
使用哈希表管理雇员信息
将公司的新员工信息加入到表中,要求能通过id查找该员工的所有信息。
要求:不使用数据库,速度越快越好
添加时,保证id是从小到大
代码实现增删改查
在实现这道题是,其中的hashcode,是用了很简单的取模法。
代码
public class HashTabDemo {
public static void main(String[] args) {
//创建一个哈希表
HashTable hashTab = new HashTable(7);
//写一个菜单
String key = "";
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("add: 添加雇员");
System.out.println("list: 显示雇员");
System.out.println("find: 查找雇员");
System.out.println("exit: 退出系统");
key = scanner.next();
switch (key){
case "add":
System.out.println("input id");
int id = scanner.nextInt();
System.out.println("input name");
String name = scanner.next();
//创建雇员
Emp emp = new Emp(id, name);
hashTab.add(emp);
break;
case "list":
hashTab.HashTable_dis();
break;
case "exit":
scanner.close();
System.exit(0);
case "find":
System.out.println("please input the id of emp:");
id = scanner.nextInt();
hashTab.findEmpById(id);
default:
break;
}
}
}
}
class HashTable {
private EmpLinkedList[] empLinkedListArray; //数组,存放链表的
private int size; //表示哈希表的大小
//构造器
public HashTable(int size) {
this.size = size;
//初始化哈希表大小
empLinkedListArray = new EmpLinkedList[size];
//不要忘了初始化链表
for (int i = 0; i < size; i++) {
empLinkedListArray[i] = new EmpLinkedList();
}
}
public void add(Emp emp) {
//根据员工的id,得到该员工应当添加到哪条链表
int empLinkedListNO = hashCode(emp.id);
//将emp 加入到对应的链表中
empLinkedListArray[empLinkedListNO].add(emp);
}
//遍历所有哈希表
public void HashTable_dis() {
for (int i = 0; i < size; i++) {
empLinkedListArray[i].listdis(i);
}
}
//根据输入的id,查找雇员
public void findEmpById(int id) {
int empLinkedListNO = hashCode(id);
Emp emp = empLinkedListArray[empLinkedListNO].finfEmpById(id);
if (emp != null){
System.out.printf("在第%d条链表中找到雇员 id = %d\n", (empLinkedListNO + 1), id);
}else {
System.out.println("there is not this employee");
}
}
//简单的取模法,哈希函数
public int hashCode(int id) {
return id % size;
}
}
//雇员信息
class Emp {
public int id;
public String name;
public Emp next; //默认为null
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
}
//create EmpLinkedList
class EmpLinkedList {
//头指针,指向第一个Emp,head是直接指向第一个Emp
private Emp head;
//添加雇员到链表
//添加雇员时,id是自增长的,id的分配是从小到大
//因此直接将雇员加入到本链表的最后
//
public void add(Emp emp){
//如果添加第一个雇员
if (head == null) {
head= emp;
return;
}
//如果不是第一个,则使用一个辅助的指针,帮助定位到最后
Emp curEmp = head;
while(curEmp.next != null) {
curEmp = curEmp.next;
}
//退出时加入emp
curEmp.next = emp;
}
//遍历链表的雇员信息
public void listdis(int number){
if (head == null) {
//说明链表为空
System.out.println("第 "+ (number+1) + " 链表为空 ");
return;
}
System.out.println("第" + (number+1) + "链表的信息为:");
Emp 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();
}
//根据id查找雇员
public Emp finfEmpById(int id) {
//判断链表是否为空
if (head == null) {
System.out.println("nothing!");
return null;
}
Emp curEmp = head;
while (true){
if (curEmp.id == id) {
break; //这是curEmp就指向要查找的雇员
}
if (curEmp.next == null) {
curEmp = null;
break;
}
curEmp = curEmp.next;
}
return curEmp;
}
}