哈希表(hash table,也叫散列表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫散列函数,存放记录的数组叫散列表。
哈希表可以提供快速的插入和查找工作,哈希表运算的非常快,而且编程实现也比较容易。哈希表是数组和链表结构。
1.哈希表是链表和数组实现的(数组里面存储的元素是链表的头结点)
2.哈希表是环形数组实现的
什么是环形数组呢?
就是近似于环,数组的最大索引后面就是0索引,这种思想主要是通过取模计算实现的,
主要公式: index = index % arr.length
就按照上图举例吧: 例如 计算出hash值为 10 10 % 8 = 2 所以应该存放 2 索引的位置
3.为什么Jdk会在链表长度超过8时转换为红黑树?
正常数据情况下,就算数据量比较大,也不会出现超过8的情况!
但是为什么会用这种机制呢?
这种机制呢,主要是为了防范于未然,防止被攻击,有些人专门造一些攻击的Hash数据(这些hash值都会相互之间冲突),就会形成一个非常长的链表,会使你整个服务器下降,
主要目的:为了防止被恶意攻击
4.为什么Jdk的底层数组长度都是2^n?
.位运算符(&)比模运算(%),效率高,这样做可以提高效率。
它对应的二进制表示中只有一位是1,其余位都是0。在这种情况下,计算哈希值h与哈希表长度m的取模运算等价于对2^n取模,可以使用位运算(&)实现,即 h & (m - 1)。
所以长度为2 ^ n就会有一个规律: [hash & (数组长度-1)] 等价于 [hash % 数组长度]
这样的做法的好处是:当哈希表长度是2^n时,可以用位运算来代替较慢的取模运算,从而提高哈希表的性能。并且由于2^n的二进制表示中只有一位是1,因此在使用位运算计算哈希表索引时,可以保证结果均匀分布,减少哈希碰撞的概率。
需要注意的是,即使哈希表的长度不是2^n,仍然可以使用位运算来代替取模运算。但是,这样做会使得位运算的效率降低,并且不能保证哈希值均匀分布,容易导致哈希冲突,影响哈希表的性能。
1.hash表在添加的使用key.hashCode()计算出的键的hash值
2.再利用位运算符&数组长度 - 1(% 数组长度)计算出应该要存的下标
3.接下俩就是数组中有元素吗?
3.1 没有则直接存
3.2 有的话,一直往后找,找到最后一个元素,添加在它的尾部
删除的原理:主要思想就是找到删除元素上一个节点(如果是头部,则头部下一个节点为空)
1.根据要删除的元素,计算其hash值,并找到对应的数组索引。
2.在该索引位置查找元素,如果存在,则执行删除操作。
3.删除元素后,根据具体情况可能需要进行以下操作:
3.1 如果删除后该位置没有其他元素,则直接将该位置设为null,表示该位置为空。
3.2如果删除后该位置有其他元素(可能是发生了哈希冲突),则可能需要进行链表或其他数据结构的调整,以保持哈希表的正确性。
具体删除时的操作流程可能因不同的哈希表实现而有所不同。例如,对于开放寻址法的哈希表实现,在删除元素时可能会使用查找下一个空槽的方式来处理哈希冲突。而对于拉链法的哈希表实现,在删除元素时可能需要遍历链表结构并进行节点的删除操作。
需要注意的是,哈希表的删除操作可能会导致哈希表的负载因子过低或链表过长等问题,影响哈希表的性能。为了保持哈希表的高效性,可能需要根据具体情况进行动态缩容、重新哈希或其他优化操作。
哈希表的扩容是为了保持哈希表的负载因子,在一个可接受的范围内,从而保持哈希表的性能稳定。
当哈希表中的元素数量达到一定阈值(0.75)时,就会触发扩容操作。具体的扩容原理如下:
1.创建一个新的更大的数组,通常将数组长度扩大为原来的两倍。
2.遍历原来的哈希表,将每个元素重新计算哈希值,并放入新的数组中的对应位置
3.这个过程被称为重新哈希(Rehashing),它会根据新数组大小重新计算元素的存储位置,以确保元素在新数组中的分布更为均匀,减少哈希冲突。
4.将新的数组设置为哈希表的底层数组,用于存储元素。
注意:这里我怕不好理解我没有用位运算符(&)全部用了%
定义节点类,和定义成员变量
// 哈希表的代码实现
public class HashTable{
// 定义哈希表节点
class HashNode{
private final K key;//键
private V value;//值
private HashNode next;//下一个节点的引用
public HashNode(K key, V value) {
this.key = key;
this.value = value;
this.next = null;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
public HashNode getNext() {
return next;
}
public void setNext(HashNode next) {
this.next = next;
}
}
private int SIZE;//定义数组的长度
private final double LOAD_FACTOR = 0.75; // 负载因子,用于决定何时进行扩容
private int size; // 哈希表中节点的数量
private HashNode[] table;//定义一个存链表的数组
public HashTable(int initialCapacity) {//初始化数组
this.SIZE = initialCapacity;
table = new HashNode[SIZE];
this.size = 0;//初始化节点数量为0
}
}
private int getHash(K key) {
//利用jdk的哈希算法来产生一个随机数
return Math.abs(key.hashCode() % SIZE);//获得键的hash值应放入的索引,取绝对值以保证正数
}
//扩容
private void resize() {
int newSize = SIZE * 2; // 新的哈希表大小为原来的两倍
HashNode[] newTable = new HashNode[newSize]; // 创建新的数组
for (int i = 0; i < SIZE; i++) {
HashNode current = table[i]; // 获取原数组的每个链表的头节点
while (current != null) {
K key = current.getKey();
V value = current.getValue();
// 根据新的数组大小重新计算哈希值
int hash = Math.abs(key.hashCode() % newSize);
// 插入节点到新的数组中
if (newTable[hash] == null) {//头结点
newTable[hash] = new HashNode<>(key, value);
} else {//非头结点
HashNode newNode = newTable[hash];
while (newNode.getNext() != null) {//找到next不为null的节点进行存储
newNode = newNode.getNext();
}
newNode.setNext(new HashNode<>(key, value));
}
current = current.getNext(); // 继续遍历原数组的下一个节点
}
}
table = newTable; // 将引用指向新的数组
SIZE = newSize; // 更新数组大小
}
public void put(K key,V value){//向hash表中插入键值对
int hash = getHash(key);//计算键的hash值放入的索引
if (table[hash] == null){ //为空
table[hash] = new HashNode<>(key,value);//直接插入节点
}else {//不为空
HashNode current = table[hash];
while (current.getNext() != null) { // 遍历链表,直到找到最后一个节点
if (current.getKey().equals(key)) { // 如果找到键相同的节点
current.setValue(value); // 更新值
return;
}
current = current.next;
}
//找不到就判断最后一个是不是
if (current.getKey().equals(key)) { // 如果最后一个节点的键与目标键相同
current.setValue(value); // 更新值
} else { // 如果最后一个节点的键与目标键不同
current.setNext(new HashNode<>(key, value)); // 插入新节点作为最后一个节点的下一个节点
}
}
// 检查负载因子是否超过阈值,如果超过则进行扩容
if ((double) size / SIZE > LOAD_FACTOR) {
resize();
}
}
public void remove(K key) { // 移除指定键的节点
int hash = getHash(key); // 计算键的哈希值
HashNode previous = null; // 记录前一个节点
HashNode current = table[hash]; // 获取对应索引位置的节点
while (current != null) { // 遍历链表
if (current.getKey().equals(key)) { // 如果找到键相同的节点
if (previous == null) { // 如果当前节点为链表的第一个节点
table[hash] = current.getNext(); // 将下一个节点设为新的头节点
} else { // 如果当前节点不是链表的第一个节点
previous.setNext(current.getNext()); // 将上一个节点的next指向当前节点的下一个节点,跳过当前节点
}
return;
}
previous = current; // 更新前一个节点
current = current.getNext(); // 继续遍历下一个节点
}
}
public boolean isEmpty() { // 判断哈希表是否为空
for (int i = 0; i < SIZE; i++) { // 遍历数组
if (table[i] != null) { // 如果存在非空的节点
return false; // 表示哈希表不为空
}
}
return true; // 如果数组中所有元素都为空,则返回true表示哈希表为空
}
public V get(K key) { // 根据键获取值
int hash = getHash(key);
HashNode current = table[hash];//获取对应索引的位置
while (current != null) { // 遍历链表
if (current.getKey().equals(key)) { // 如果找到键相同的节点
return current.getValue(); // 返回对应的值
}
current = current.getNext(); // 继续遍历下一个节点
}
return null; // 如果未找到对应的键,则返回null
}
public int size() { // 获取哈希表中节点的数量
int count = 0; // 计数器
for (int i = 0; i < SIZE; i++) { // 遍历数组
HashNode current = table[i]; // 获取对应索引位置的节点
while (current != null) { // 遍历链表
count++; // 计数器加一
current = current.getNext(); // 继续遍历下一个节点
}
}
return count; // 返回计数器的值,即哈希表中节点的数量
}