📒数据结构专栏简介: 内容主要以介绍数据结构为主, 希望读者看了之后能有所收获🉐
👷🏻作者也是一名刚上路的小白coder🛵, 欢迎各位评论, 批评指正或私信交流👁🗨~~
文章目录
关于Map接口和Set接口在上一篇关于TreeMap文章中已经介绍过, 这里不再赘述, 详见: http://t.csdn.cn/L44vx
HashMap
HashMap
是Map接口下的一个实现子类, HashMap
与TreeMap
一样, 同样存储的是<Key, Value>
的键值对, 与 TreeMap 相同的是, HashMap中的Key
也是唯一的, 当有相同的Key的<Key, Value>, 会更新之前的Value
(若Key
已存在)
存储结构
HashMap
的存储容器通常是一个哈希桶, 在存储的元素过多时, 容器的结构会转换成一个类似于TreeMap的树状结构. 树中的元素排序依据主要是哈希值(如果元素对应的类实现了Comparable
接口则基于compareTo方法进行比较排序)
我们可以观察HashMap
的源码进行学习
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
哈希表的初始容量是16
static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量是 1 << 30(1073741824)
/** The bin count threshold for using a tree rather than list for a bin. Bins are converted to trees when adding an element to a bin with at least this many nodes. The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage. */
static final int TREEIFY_THRESHOLD = 8;
树化的阈值是8, 该阈值必须大于2, 且最小应该是8, 只有这样才匹配树容器和普通容器间去树化转换的收缩前提
/** The bin count threshold for untreeifying a (split) bin during a resize operation. Should be less than TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal. */
static final int UNTREEIFY_THRESHOLD = 6;
解除树化的阈值是6, 该阈值应该要小于树化阈值, 且也要与收缩检测相匹配
/** The smallest table capacity for which bins may be treeified. (Otherwise the table is resized if too many nodes in a bin.) Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between resizing and treeification thresholds. */
static final int MIN_TREEIFY_CAPACITY = 64;
可能树化的最小容量是64, 而哈希表的扩容大小最小值至少为4*树化的最小容量
/** The load factor used when none specified in constructor.*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的负载因子是0.75
负载因子定义:
哈希表的负载因子 α = 表 中 元 素 个 数 表 的 长 度 ( 容 量 ) α = \frac {表中元素个数}{表的长度(容量)} α=表的长度(容量)表中元素个数
当负载因子过大时, 插入新元素时就容易发生冲突
常用方法
构造方法
方法 | 简述 |
---|---|
HashMap() | 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap |
HashMap(int initialCapacity) | 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap |
HashMap(int initialCapacity, float loadFactor) | 构造一个带指定初始容量和负载因子的空 HashMap |
HashMap(Map<? extends K,? extends V> m) | 构造一个映射关系与指定 Map 相同的新 HashMap |
方法
方法 | 简述 |
---|---|
void clear() | 从map中移除所有映射关系 |
boolean containsKey(Object key) | 如果map中包含对于指定键的映射关系,则返回 true |
boolean containsValue(Object value) | 如果map中将一个或多个键映射到指定值,则返回 true |
Set<Map.Entry<K,V>> entrySet() | 返回map中所包含的映射关系的 Set 视图 |
V get(Object key) | 返回指定键所映射的值;如果对于该键来说,map中不包含任何映射关系,则返回 null |
V put(K key, V value) | 在此map中关联指定值与指定键 |
V remove(Object key) | 从此映射中移除指定键的映射关系(如果存在) |
boolean isEmpty() | 如果map中不包含键-值映射关系,则返回 true |
int size() | 返回map中的键-值映射关系数 |
Map.Entry<K,V>是一个接口, 详情可见TreeMap介绍文章
HashSet介绍
HashSet
实现了Set
接口, 存储的是Key
, Key
是唯一的
存储结构
/**
* This class implements the <tt>Set</tt> interface, backed by a hash table
* (actually a <tt>HashMap</tt> instance). It makes no guarantees as to the
* iteration order of the set; in particular, it does not guarantee that the
* order will remain constant over time. This class permits the <tt>null</tt>
* element. */
由源码的注释我们可以知道HashSet
是借HashMap
实现的, HashSet
中的元素排序顺序是不能保证的, 且允许储存null
常用方法
构造方法
方法 | 简述 |
---|---|
HashSet() | 构造一个新的空 set,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75 |
HashSet(Collection<? extends E> c) | 构造一个包含指定 collection 中的元素的新 Set |
HashSet(int initialCapacity) | 构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和默认的负载因子 |
HashSet(int initialCapacity, float loadFactor) | 构造一个新的空 Set ,其底层 HashMap 实例具有指定的初始容量和指定的负载因子 |
方法
方法 | 简述 |
---|---|
boolean add(E e) | 如果 set 中尚未包含指定元素,则添加指定元素 |
void clear() | 从 set 中移除所有元素 |
boolean contains(Object o) | 如果 set 中包含指定元素,则返回 true |
boolean isEmpty() | 如果 set 中不包含任何元素,则返回 true |
boolean remove(Object o) | 如果指定元素存在于此 set 中,则将其移除 |
int size() | 返回此 set 中的元素的数量(set 的容量) |
哈希表
一般的数据结构如列表, 链表等进行搜索操作时是需要通过遍历整个表(O(n))才能完成的, 而平衡树的时间复杂度也是与树的高度有关(O(logn)), 而我们想要的是一种更高效, 更理想的搜索模式, 就如狙击枪一般指哪打哪, 达到近似(O(1))的效率, 而Hash Table
正是为此而生的.
哈希表(又称散列表)中通过构造元素中关键码和元素的存储位置一一对应的映射关系, 使其简化搜索的过程
基本操作(CURD)
-
插入(Create) :
计算出元素对应的哈希值, 依据哈希值将元素存储到对应位置
-
搜索(Read):
根据搜索元素中的关键字计算出哈希值, 将对应的存储的元素的哈希值进行比较, 相同则返回其引用
-
更新(Update):
与搜索类似, 在搜索的基础下, 对应哈希值相同则更新
-
删除(Delete)
与搜索类似, 在搜索的基础下, 对应哈希值相同则将该引用置空
哈希冲突
当哈希表中的元素越来越多时, 总会发生的情况: 有两个或多个元素计算出的哈希值而指向同一块存储空间, 这就是发生了哈希冲突(哈希碰撞), 发生的碰撞越多自然也会降低效率, 这是我们不愿看到的
哈希函数设计
为了降低发生哈希冲突的频率, 在哈希函数设计上我们可以下一些功夫, 常用的哈希函数的设计有:直接定制法, 除留余数法, 平方取中法, 折叠法, 数学分析法, 在这就简单介绍两种常用的方法 直接定制法 和 除留余数法, 其他方法读者若有兴趣可以自行查找资料, 优秀哈希函数设计可以有效降低哈希冲突发生的频率, 但无法完全避免哈希冲突的发生
直接定制法
取元素中的关键字为变量, 通过线性函数运算得到哈希值(散列地址), 如 :
Hash (Key) = A * Key + B
这种办法的优点是比较简单, 适用于已知关键字范围, 计算出的地址要数量不大且分布比较连续
除留余数法
同理取元素中的关键字为变量, 设哈希表的容量为max, 取最接近或者等于max的质数p(p ≤ m)作为除数, 如 :
Hash (Key) = Key % p
这种方法的优缺点与上述方法相似
负载因子的调节
哈希冲突发生的频率与负载因子也有关, 当哈希表中存储的元素越多, 负载因子越大, 发生碰撞的概率也就越高. 所以要对负载因子进行调节, 从上述负载因子的定义来看, 调节负载因子可以改变哈希表的容量
要注意的是调节哈希表容量时, 若设计的哈希函数与哈希表的容量有关, 要将其中存储的元素重新进行散列地址的计算重新存储
哈希冲突的解决办法
闭散列法
当发生哈希冲突时, 若哈希表的存储元素个数还未达到最大, 我们总能找到空位存放发生冲突的元素
- 线性探测: 当发生哈希冲突时, 依次往后寻找到空位即可
如图上所示, 假设哈希函数是Hash(Key) = Key % 8, 要插入Key 为 1 的元素, Index 为 1 处已经存有 Key 为 9 的元素, 当我们想存入Key为 1 的元素时, 就会发生冲突, 此时依次往后寻找到 Index 为 2 时有空位, 插入即可
- 二次探测: 当发生哈希冲突时, 寻找下一个位置的方法为 H a s h ( K e y ) = ( H 0 + i 2 ) Hash(Key) = (H_{0} + i^{2} ) Hash(Key)=(H0+i2)% m ( H 0 H_{0} H0是初次计算出的哈希值, 式中也可以将 H 0 H_{0} H0 与 i 2 i^{2} i2 相减)
如上图所示, 依然假设哈希函数是Hash(Key) = Key % 8, 要插入Key 为 17 的元素, Index 为 1 处已经存有元素, 通过上述公式计算得下一个 Index 为 2 处, 也存有元素, 则计算第二次得Index 为 5, 为空可插入
开散列法
开散列法则是利用类似链表的结构处理冲突, 创建一个存储哈希桶引用的容器, 当发生冲突时, 只需用头插法处理即可