专栏:数据结构
标题:哈希表
哈希表
概念
通过某种函数使元素的存储位置与它的关键码之间能够建⽴⼀ 映射的关系,在查找时通过该函数可以很快找到该元素。可以不经过任何⽐较,⼀次直接从表中得到要搜索的元素。
哈希方法
1.插入元素:根据待插⼊元素的关键码,以此函数计算出该元素的存储位置并按此位置进⾏存放
2.搜索元素:对元素的关键码进⾏同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素⽐较,若关键码相等,则搜索成功;
3.哈希⽅法中使⽤的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表;
哈希冲突
不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
哈希避免
方法一:负载因子调节
载荷因子a=填入表中元素个数 / 散列表的长度
(由于散列表长度的定值:所以负载因子和填入表中元素成正比)
结论:
方法一:当冲突率达到⼀个⽆法忍受的程度时,我们需要通过降低负载因⼦来变相的降低冲突率。
方法二:已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的⼤⼩。
方法二:闭散列
闭散列:也叫开放地址法,当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的"下⼀个" 空位置中去。
如果找不到一下空位置:线性探测:从发⽣冲突的位置开始,依次向后探测,直到寻找到下⼀个空位置为⽌。
二次探测:线性探测的缺陷是产⽣冲突的数据堆积在⼀块,此⼆次探测为了避免该问题,找下⼀个空位置的⽅法为:通过散列函数Hash(x)对元素的关键码key 进⾏计算得到的位置
方法三:开散列(哈希桶)
开散列法⼜叫链地址法(开链法),⾸先对关键码集合⽤散列函数计算散列地址,具有相同地址的关键码归于同⼀⼦集合,每⼀个⼦集合称为⼀个桶,各个桶中的元素通过⼀个单链表链接起来,各链表的头结点存储在哈希表中。
(开散列,可以认为是把⼀个在⼤集合中的搜索问题转化为在⼩集合中做搜索了);
哈希桶的实现
整体思路
1.用数组做 "桶",每个数组位置存一条链表的头节点
2.存数据时:用key % 数组长度算出下标,把数据放到对应下标的链表里
3.如果两个 key 算出同一个下标(哈希冲突),就串在同一条链表上
4.当元素太多、链表太长时,扩容数组,重新摆放所有元素
实现类
java
public class HashBuck {
// 1.静态内部类:代表链表的每一个节点
//2,static class Node:静态内部类,
// 不依赖外部 HashBuck 对象就能创建节点,专门用来存键值对。
//3.每个 Node 就是链表的一个 "小格子",
// 装着 key、value,还有指向下一个格子的 "绳子"(next)。
static class Node {
private int key;// 键:用来计算数组下标、标识唯一性
private int val;// 值:key对应的数据
private Node next;// 下一个节点的地址,用来把节点串成链表
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
private Node[] array = new Node[10];
//array:哈希表的 "桶数组",数组的每一个位置对应一条链表。
// 底层桶数组,初始长度10,每个位置存链表头
private int usedSize;
// 记录当前哈希表里有多少个有效键值对.
// 计数器,每新增一个键值对就 + 1,更新已有的 key 不增加。
private static final double DEFAULT_LOAD_FACTOR = 0.75;////负载因子阈值,超过就扩容
// 添加/更新键值对
public void put(int key, int val) {
//步骤1:用key计算数组下标(哈希函数)
int index = (key & 0x7fffffff) % array.length;
//负数 key 数组越界:key%数组长度如果 key 是负数,结果也是负数,数组下标不能为负,会报错。
//修复:int index = (key & 0x7fffffff) % array.length;(位运算把负数转成正数)
// 步骤2:遍历对应下标的链表,检查key是否已经存在
Node cur = array[index];// 拿到链表头节点
while (cur != null) {
if (cur.key == key) {
// key已存在:更新value,直接结束,不新增节点
cur.val = val;
return;
}
cur = cur.next;// 没找到就往后走一个节点
}
// 步骤3:key不存在,用头插法新增节点
Node node = new Node(key, val);// 创建新节点
node.next = array[index];// 新节点的next指向原来的头节点
array[index] = node;// 数组对应位置换成新节点(新节点变成新头)
usedSize++;// 元素个数+1
// 步骤4:检查负载因子,超过阈值就扩容
if (loadFactor() > DEFAULT_LOAD_FACTOR) {
resize();
}
}
// 计算负载因子
private double loadFactor() {
return usedSize * 1.0 / array.length;
}
// 扩容重哈希
private void resize() {
// 创建新数组,长度是原来的2倍
Node[] newArray = new Node[2 * array.length];
// 遍历旧数组的每一个桶
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
// 遍历当前桶的所有节点,重新放到新数组里
while (cur != null) {
int newIndex = (cur.key & 0x7fffffff) % newArray.length;// 算在新数组里的下标
Node curNext = cur.next;// 先存下一个节点,不然后面改了next就找不到了
cur.next = newArray[newIndex];// 头插法第一步:当前节点的next,指向新数组对应桶现有的头节点
newArray[newIndex] = cur;//头插法第二步:新数组对应桶的头,换成当前节点
cur = curNext;// 处理下一个节点
}
}
array = newArray; // 所有元素搬完再替换数组
}
// 根据key获取value
public int get(int key) {
int index = (key & 0x7fffffff) % array.length;// 先算下标
Node cur = array[index];// 拿到对应桶的链表头
while (cur != null) {
if (cur.key == key) {
return cur.val;// 找到就返回对应的值
}
cur = cur.next;// 没找到就往后遍历
}
return -1; // 找不到返回-1
}
// 判断key是否存在
public boolean containsKey(int key) {
int index = (key & 0x7fffffff) % array.length;
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
return true;
}
cur = cur.next;
}
return false;
}
// 获取元素个数
public int size() {
return usedSize;
}
// 根据key删除节点,删除成功返回true,key不存在返回false
public boolean remove(int key) {
// 1. 计算key对应的桶下标
int index = (key & 0x7fffffff) % array.length;
Node cur = array[index]; // 当前遍历的节点
Node prev = null; // 记录cur的前一个节点
// 2. 遍历当前桶的链表
while (cur != null) {
if (cur.key == key) {
// 找到目标节点,执行删除
if (prev == null) {
// 场景1:要删的是链表头节点
array[index] = cur.next;
} else {
// 场景2:要删的是中间/尾部节点
prev.next = cur.next;
}
usedSize--; // 有效元素个数减1
return true;
}
// 没找到,两个指针一起向后移动
prev = cur;
cur = cur.next;
}
// 场景3:遍历完链表都没找到key
return false;
}
}
测试类
java
public class TestHashBuck {
public static void main(String[] args) {
HashBuck hashBuck = new HashBuck();
// 测试1:正常插入+查询
hashBuck.put(1, 10);
hashBuck.put(2, 20);
hashBuck.put(11, 110); // 11%10=1,和key=1在同一个桶,测试哈希冲突
System.out.println("key=1的值:" + hashBuck.get(1)); // 预期10
System.out.println("key=11的值:" + hashBuck.get(11)); // 预期110
System.out.println("当前元素个数:" + hashBuck.size()); // 预期3
// 测试2:更新已有的key
hashBuck.put(1, 100);
System.out.println("更新后key=1的值:" + hashBuck.get(1));// 预期100
System.out.println("更新后元素个数:" + hashBuck.size()); // 预期3(不增加)
// 测试3:扩容功能验证
// 初始长度10,负载因子0.75,第8个元素会触发扩容
for (int i = 3; i <= 10; i++) {
hashBuck.put(i, i * 10);
}
System.out.println("扩容后元素个数:" + hashBuck.size()); // 预期10
// 验证扩容后之前的数据没丢
System.out.println("扩容后key=1的值:" + hashBuck.get(1)); // 预期100
System.out.println("扩容后key=11的值:" + hashBuck.get(11));// 预期110
// 测试4:查询不存在的key
System.out.println("不存在的key=99:" + hashBuck.get(99)); // 预期-1
System.out.println("是否包含key=99:" + hashBuck.containsKey(99)); // 预期false
// 测试5:负数key(修复前会报错,修复后正常)
hashBuck.put(-5, -50);
System.out.println("负数key=-5的值:" + hashBuck.get(-5)); // 预期-50
// 测试删除功能
System.out.println("删除前是否包含key=11:" + hashBuck.containsKey(11)); // 预期 true
boolean delResult = hashBuck.remove(11);
System.out.println("删除是否成功:" + delResult); // 预期 true
System.out.println("删除后是否包含key=11:" + hashBuck.containsKey(11)); // 预期 false
System.out.println("删除后元素总数:" + hashBuck.size()); // 预期比之前少1
// 测试删除不存在的key
System.out.println("删除不存在的key=99:" + hashBuck.remove(99)); // 预期 false
}
}
扩容明确
扩容后链表顺序会反转
因为用的是头插法,旧链表是 11→1,如果两个节点在新数组还分到同一个桶,顺序会变成 1→11。
这也是早期 HashMap 扩容的特点,单线程下不影响功能,只是顺序反过来。
为什么不能整条链表直接搬过去?
因为数组长度变了,同一个旧桶里的节点,在新数组里可能分到不同的桶(比如例子里的 1 和 11,旧桶都是 1,新桶分到 1 和 11)。
所以必须逐个节点重新计算位置,不能整条链表直接平移。
性能分析
哈希表的插⼊/删除/查找时间复杂度是 O(1) 。
相关算法题
只出现一次的数字

算法思路
异或运算三条核心数学性质(算法基石)
相同数异或为 0:x ^ x = 0
任何数异或 0 等于自身:x ^ 0 = x
交换律、结合律:异或顺序不影响最终结果
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
算法完整逻辑
1.初始化 result = 0,作为异或累加器。
2.遍历数组每一个元素,不断执行 result = result ^ numsi。
3.所有成对出现的数字,会两两异或,结果变成 0;
4.全部异或完成后,等价于:0 ^ 唯一的那个数,结果就是目标数字。
java
class Solution {
public int singleNumber(int[] nums) {
int result=0;
for(int i=0;i<nums.length;i++){
result^=nums[i];
}
return result;
}
}
