iOS 面试 - 哈希核心要点(必背版)
🎯 一、基础概念(必答)
1. 什么是哈希?
答案:
- 哈希 = 把任意数据转换成固定长度数字的机制
- 哈希值 = 数据的"编号",用于快速定位
- 哈希表 = 用哈希值作为索引的数据结构
一句话总结: 哈希就是给数据一个编号,通过编号快速找到数据
🎯 二、iOS 中的哈希应用(必考)
1. NSDictionary(字典)
NSDictionary *dict = @{@"name": @"张三"};
dict[@"name"]; // 内部使用哈希表,O(1) 查找
要点:
- 使用哈希表存储 key-value
- 查找时间复杂度:O(1)
- key 的哈希值用于定位存储位置
2. NSSet(集合)
NSSet *set = [NSSet setWithObjects:@"A", @"B", @"A", nil];
// 自动去重,因为使用哈希表
要点:
- 使用哈希表去重
- 查找时间复杂度:O(1)
3. Runtime 方法缓存(最重要!)
[obj doSomething]; // 第一次:慢速查找 + 存入 cache
[obj doSomething]; // 第二次:哈希查找 cache,快速返回
要点:
- 每个类有
cache_t结构,存储 selector → IMP 映射 - 使用哈希表实现快速查找
- 缓存命中率通常 >90%
- 这是 iOS 底层最重要的哈希应用
🎯 三、Runtime 方法缓存详解(高频考点)
数据结构
// cache_t(简化)
struct cache_t {
bucket_t *_buckets; // 哈希桶数组
uint32_t _mask; // 容量 - 1
uint32_t _occupied; // 已占用数量
};
// bucket_t
struct bucket_t {
SEL _key; // selector(方法名)
IMP _imp; // 方法实现(函数指针)
};
查找流程
objc_msgSend(receiver, selector)
↓
1. 检查 receiver 是否为 nil
↓
2. 获取 receiver 的 isa → class
↓
3. 在 class 的 cache 中哈希查找
↓
4. 命中 → 直接调用 IMP(快速路径,90%+)
↓
5. 未命中 → 慢速查找(method_list)
为什么快?
- 哈希表查找:O(1) 时间复杂度
- 缓存命中率高:>90% 的方法调用走快速路径
- 汇编优化:
objc_msgSend用汇编实现
🎯 四、哈希冲突(必考)
什么是哈希冲突?
答案: 不同的数据计算出相同的哈希值
例子:
"张三" 的哈希值 = 12345
"李四" 的哈希值 = 12345 // 冲突了!
解决方法(必答)
1. 开放寻址法(Runtime 使用)
如果位置被占用,向后找下一个空位置
位置 45:已被占用
位置 46:空 ✅ 存储
2. 链地址法(NSDictionary 可能使用)
每个位置存储一个链表
位置 45:→ 数据1 → 数据2 → 数据3
🎯 五、对象的 hash 方法(常考)
默认实现
// 对象默认用内存地址作为哈希值
Person *p = [[Person alloc] init];
NSUInteger hash = [p hash]; // 返回内存地址
如何重写(面试可能要求手写)
- (NSUInteger)hash {
// 使用属性的哈希值组合
return [self.name hash] ^ [@(self.age) hash];
}
// 必须同时重写 isEqual
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isKindOfClass:[self class]]) return NO;
Person *other = (Person *)object;
return [self.name isEqualToString:other.name]
&& self.age == other.age;
}
要点:
hash和isEqual必须一起重写- 相等的对象必须有相同的哈希值
- 不同对象尽量有不同的哈希值
🎯 六、性能对比(必背)
| 操作 | 数组 | 哈希表 |
|---|---|---|
| 查找 | O(n) | O(1) |
| 插入 | O(1) | O(1) |
| 删除 | O(n) | O(1) |
例子:
// 数组查找:O(n) - 需要遍历
NSArray *arr = @[@"A", @"B", @"C", @"D", @"E"];
[arr indexOfObject:@"E"]; // 需要检查 5 个元素
// 哈希表查找:O(1) - 直接定位
NSDictionary *dict = @{@"A": @1, @"B": @2, @"C": @3, @"D": @4, @"E": @5};
dict[@"E"]; // 计算哈希值,直接定位
🎯 七、面试高频问题(标准答案)
Q1: 什么是哈希?哈希表是什么?
答案:
- 哈希:把数据转换成数字的机制
- 哈希表:用哈希值作为索引的数据结构
- 优势:查找速度快(O(1))
Q2: iOS 中哪些地方用到了哈希?
答案:
- NSDictionary:key-value 存储
- NSSet:去重和快速查找
- Runtime 方法缓存:cache_t 存储 selector → IMP(最重要)
- 对象的 hash 方法:用于字典 key、集合元素
Q3: Runtime 的方法缓存为什么快?
答案:
- 使用哈希表存储最近调用的方法
- 查找时间复杂度 O(1)
- 缓存命中率通常 >90%
- 大部分方法调用走快速路径
Q4: 什么是哈希冲突?如何解决?
答案:
- 冲突:不同数据计算出相同哈希值
- 解决方法:
- 开放寻址法:向后找空位置(Runtime 使用)
- 链地址法:每个位置存链表(NSDictionary 可能使用)
Q5: 如何重写对象的 hash 方法?
答案:
- (NSUInteger)hash {
return [self.name hash] ^ [@(self.age) hash];
}
// 必须同时重写 isEqual 方法
Q6: 哈希表的查找时间复杂度是多少?
答案:
- 平均情况:O(1) - 直接定位
- 最坏情况:O(n) - 所有数据冲突,退化成链表
Q7: NSDictionary 的底层实现原理?
答案:
- 使用哈希表存储 key-value
- key 的哈希值用于定位存储位置
- 解决冲突可能使用链地址法
- 查找、插入、删除都是 O(1) 平均时间复杂度
🎯 八、记忆口诀(快速回忆)
核心概念
哈希 = 编号机制
哈希表 = 快速定位
查找 = O(1) 时间复杂度
iOS 应用
字典、集合、方法缓存
都用哈希表实现
Runtime 缓存
cache_t 存 selector → IMP
哈希查找,快速返回
命中率 >90%
哈希冲突
不同数据,相同哈希值
开放寻址 or 链地址
🎯 九、面试回答模板
模板1:什么是哈希?
哈希是把数据转换成数字的机制。
在 iOS 中,NSDictionary、NSSet 和 Runtime 方法缓存都使用哈希表。
哈希表的优势是查找速度快,时间复杂度是 O(1)。
模板2:Runtime 方法缓存
Runtime 的方法缓存使用哈希表存储 selector 到 IMP 的映射。
每次调用方法时,先在 cache 中哈希查找。
如果命中,直接调用 IMP,这是快速路径。
缓存命中率通常 >90%,所以方法调用很快。
模板3:哈希冲突
哈希冲突是指不同数据计算出相同哈希值。
解决方法有两种:
1. 开放寻址法:向后找空位置(Runtime 使用)
2. 链地址法:每个位置存链表(NSDictionary 可能使用)
🎯 十、必背清单(考前检查)
- 哈希 = 把数据转换成数字
- 哈希表查找 = O(1) 时间复杂度
- iOS 中:字典、集合、方法缓存都用哈希表
- Runtime 方法缓存使用 cache_t 结构
- 缓存命中率 >90%,大部分走快速路径
- 哈希冲突的两种解决方法
- 如何重写对象的 hash 方法
- hash 和 isEqual 必须一起重写
🎯 十一、加分项(高级回答)
1. 哈希函数的设计原则
- 分布均匀:不同输入产生不同哈希值
- 计算快速:O(1) 时间复杂度
- 确定性:相同输入总是产生相同哈希值
2. Runtime cache 的扩容机制
- 当占用率超过阈值时,扩容
- 重新哈希所有数据
- 容量通常是 2 的幂次
3. 哈希表的负载因子
- 负载因子 = 已占用数量 / 总容量
- 负载因子过高会导致冲突增多
- 通常保持在 0.75 左右
📝 总结(30秒快速回忆)
- 哈希 = 编号机制,用于快速定位
- iOS 应用 = 字典、集合、方法缓存
- Runtime 缓存 = cache_t,哈希查找,命中率 >90%
- 哈希冲突 = 开放寻址法 or 链地址法
- 性能 = O(1) 查找,比数组快
记住:面试时先说概念,再说 iOS 应用,最后说 Runtime 缓存!