哈希表理论基础及训练
一.哈希表理论
1.基本样貌:能够根据特定索引值直接访问对应元素的数据结构就是哈希表.
根据特定索引值直接访问对应元素的数据结构,那数组不就是嘛!所以数组实际上就是一个哈希表.
2.能干什么?
哈希表一般都是用来快速判断一个元素是否出现在集合里,也就是所谓的查.
例如,要查询一个名字是否在学校里.枚举一个一个比的话时间复杂度是O(n),但如果使用哈希表只需要O(1)就可以做到.
我们只需要初始化一个哈希表,把这所学校里的学生姓名都存在哈希表里,查询的时候通过索引直接就可以知道这位同学在不在学校里了.
那么这是怎么做到的呢?这明显和哈希表的存入方式有关对吗?我们是怎么把学生姓名存入哈希表的呢?这就涉及到了哈希函数
3.哈希函数
哈希函数将学生的姓名直接映射到哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了.
我其实还没太懂,目前暂时的理解为:原来要遍历一遍元素,现在输入名字,经过哈希函数计算,有的话返回索引下标,通过[索引下标]直接返回元素;没有的话哈希函数应该能计算出下标不存在,所以相当于只要哈希函数计算这一步就找到了,查询的时间复杂度O(1).
Anyway,我们接着往下学,慢慢理解.
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashCode是通过特定编码方式,可以将其他数据格式映射为不同的数值,这样就把学生名字映射为哈希表上的索引数字了.
但是问题来了,哈希表底层就是一个数组(不是哥们,你还真是个数组啊?我以为哈希表是一个大类,数组符合特征所以属于哈希表,结果原来哈希表就是一个数组啊?!).
那就表明了我们哈希表不能无限长,因为数组是定长的.那我们前面hashCode把数据映射为数字映射的再均匀有什么用?最后数组存不下不都白搭?
所以我们想到的办法是hashCode(name)%tableSize,这样所有数值就都映射在哈希表底层数组的索引之内了.但这样又会有一个问题,那就是原来hashCode映射后不同的数据映射后值不一样,现在hashCode(name)%tableSize映射后,超过tableSize个数据映射后值肯定有一样的,那搜索起来不就不唯一了嘛!?
这就叫做哈希碰撞,接下来我们看看是怎么处理哈希碰撞的.
4.哈希碰撞
如图所示,这就是哈希碰撞,小李和小王都映射到了索引下标1的位置
解决方法一般有两种,拉链法和线性探测法:
- 拉链法:相当于在数组里面存链表,发生冲突的元素都存入ListNode,数组每个位置中存头节点head的指针.如图
拉链法我们需要选择适当的哈希表大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间.
- 线性探测法:感觉挺不靠谱,实际上就是一个位置发生了冲突,放了小李,那么就向下找一个空位放置小王的信息,如图
哈希碰撞还有很多细节,初学知道这些就可以了.
二.常见的三种哈希结构:数组Arr,集合Set,映射Map
1.在接着往下学习之前,我其实是有点疑惑的,这个模块叫哈希结构,里面包含了三种数据结构
- 我没明白他们和我之前学习的哈希表基础理论之间的关系,也就是他们和哈希表有什么关系?为什么叫常见的三种哈希结构?是因为他们底层是通过哈希表实现的吗?
先说第一个问题,数组本身并不是通过哈希表实现的,它是一种连续的存储结构,但是数组可以视为哈希表的一种最简单形式(索引直接映射到存储位置,通过索引直接访问对应数据).
Set 和 Map 通常是通过哈希表实现的,例如 Java 中的 HashSet 和 HashMap 底层使用了哈希表作为存储结构.因此,Set 和 Map 可以被称为"哈希结构",因为它们依赖哈希表的思想来实现高效的存储和查找.所以他俩确实是通过哈希表实现的.
具体说,Set是一个集合数据结构,用来存储不重复的元素,通过哈希表实现时,利用哈希函数映射元素到数组的一个位置,查找时我们直接访问这个位置就知道元素存不存在了.
Map是存储键值对(key-value)的数据结构,通过哈希表实现时,利用哈希函数将key映射到数组位置,数组位置可能存一个类的实例,这个类里面有key成员变量和value成员变量,这也就存储了键和值的对应关系.
- 在我的理解里,哈希表就是一种数据结构,我现在怀疑是不是我理解错了,因为从前面的哈希表基础理论来看,不同的哈希碰撞解决方法底层的结构是不同的,所以我在想是不是哈希表并不是一个具体的数据结构,而是一种思想正如我们前面哈希表基础理论介绍的.我们是要通过这种思想去实现Arr,set,map?
实践中,哈希表是一个具体的数据结构,一个具体的哈希表由数组+哈希函数+具体冲突解决机制构成,所以哈希表应该看作一个数据结构,我们可以根据具体的数据结构去实现更高级的数据结构,如Set和Map.
接下来我们具体说说,数组就不提了,从Set和Map说起.
2.Set
我们这里介绍Java的集合数据元素
| 数据结构 | 底层实现 | 功能 | 用法 |
|---|---|---|---|
| HashSet | 哈希表(HashMap 支撑) | 无序存储不重复元素,查找、插入和删除操作时间复杂度为 O(1) | 添加元素:add(E e) 删除元素: remove(Object o) 判断是否存在: contains(Object o) |
| TreeSet | 红黑树(平衡二叉搜索树) | 有序存储不重复元素,按自然顺序或自定义顺序,支持范围查询、排序操作,查找、插入和删除操作时间复杂度为 O(log n) | 按顺序遍历:iterator() 范围查询: subSet(E fromElement, E toElement) |
| LinkedHashSet | 哈希表 + 双向链表 | 保留插入顺序存储不重复元素,查找和插入时间复杂度为 O(1) | 遍历时按插入顺序输出:iterator() |
3.Map
| 数据结构 | 底层实现 | 功能 | 用法 |
|---|---|---|---|
| HashMap | 哈希表(数组 + 链表/红黑树) | 无序存储键值对,查找、插入和删除操作时间复杂度为 O(1) | 添加键值对:put(K key, V value) 删除键值对: remove(Object key) 查找键对应值: get(Object key) |
| TreeMap | 红黑树(平衡二叉搜索树) | 按键的自然顺序或自定义顺序存储键值对,支持范围查询,查找、插入和删除操作时间复杂度为 O(log n) | 获取范围:subMap(K fromKey, K toKey) 按顺序遍历键值对: entrySet() |
| LinkedHashMap | 哈希表 + 双向链表 | 按插入顺序或最近访问顺序存储键值对,查找、插入和删除操作时间复杂度为 O(1) | 保留插入顺序遍历:entrySet() 可设置最近最少使用 (LRU) 策略: LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) |