关注公众号

关注公众号

手机扫码查看

手机查看

喜欢作者

打赏方式

微信支付微信支付
支付宝支付支付宝支付
×

hash表原理

2023.7.31

想想一下,我们有一个数组,数组长度是100个,现在的需求是:给出这个数组是否包含一个对象obj?

如果这是个无序的数组,那么我们只能用遍历的方法来查找是否包含这个对象obj了。这是我们的时间复杂度就是O(n)。

这种查找效率是很低的,所以hash表应运而生。

hash表其实也是一个数组,区别数组的地方是它会建立 存储的值 到 存储的下标 索引的一个映射,也就是散列函数。

我们来举一个通俗易懂的例子:

现在我们有个hash表,表长度count = 16,现在我们依次把3,12,24,30依次存入hash表中。

首先我们来约定一个简单的映射关系:存储的索引下表(index) = 存储值(value) % hash表长度(count);

算下来hash表的存储分布是这样的:hash[3] = 3、hash[12] = 12、hash[8] = 24、hash[14] = 30

还是一样的需求,当我们给出24的时候,求出hash表中是否存有24?

此时,按照原先约定的映射关系:index = 24 % 16 = 8,然后我们在hash[8]查询等于24。这样,通过数组需要O(n)的时间复杂度,通过hash表只需要O(1);

上面提到的hash表在存入3,12,24,30后,如果要面临存入19呢?

此时index = 19 % 16 = 3,而之前hash[3] 已经存入了3这个值了!这种情况就是发送了散列碰撞。

此时,我们可以改进一下我们的hash表,让它存储的是一个链表。这样发送散列碰撞的元素就可以以链表的形式共处在hash表的某一个下标位置了。

所以,只要发生了散列碰撞,我们查找的时间复杂度就不能像O(1)这么小了,因为还要考虑链表的查找时间复杂度O(n)。

哈希表还有一个重要的属性: 负载因子(load factor),它用来衡量哈希表的 空/满 程度

当存储的元素个数越来越多,在hash表长度不变的前提下,发生散列碰撞的概率就会变大,查找性能就变低了。所以当负载因子达到一定的值,hash表会进行自动扩容。

哈希表在自动扩容时,一般会扩容出一倍的长度。元素的hash值不变,对哈希表长度取模的值也会改变,所以元素的存储位置也要相对应重新计算,这个过程也称为重哈希(rehash)。

哈希表的扩容并不总是能够有效解决负载因子过大而引起的查询性能变低的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。所以,设计一个合理有效的散列函数显得相当的有必要,这个合理有效应该体现在映射之后各元素均匀的分布在hash表当中。

说回NSDictionary

字典是开发中最常见的集合了。当我们调用

我们来探究下字典存储键值对的过程,有两个方法对hash存储起着关键的影响:

demo1
@interface KeyType : NSObject<NSCopying>

@property (nonatomic, copy) NSString *keyName;

@end

@implementation KeyType

//直接电影父类hash方法

//直接调用父类isEqual方法

@end

@implementation ViewController

@end

控制台打印:
for value
1
2
for key
hash func
copy func
2

分析:

dic.count = 1,说明{key1 : @"object1"}已经存储进去了。然而通过这个key去获取竟然返回null?

从打印也可以看出来,现在isEqual函数开始被调用了。

分析:

//我们可以强制重写KeyType的isEqual:返回YES,demo2的返回值就不是null了

由此可见,当一个类需要作为字典的key,重写hash和isEqual:方法显得很有必要。
重写hash方法
为什么要重写hash方法?

我们先来看看NSObject的hash方法返回什么:
KeyType *key1 = [[KeyType alloc] initWithKeyName:@"key1"];
NSLog(@"%p",key1);
NSLog(@"%lx",[key1 hash]);
控制台打印:
0x600000640610
600000640610

由此可见,NSObject是把对象的内存地址作为hash值返回。

以内存地址作为hash可以保证唯一性,但是这样好不好?

这样不好!

来看下这个场景:
@interface KeyType : NSObject<NSCopying>

@property (nonatomic, copy) NSString *keyName;

@end

@implementation KeyType

//强制返回YES

@end

@implementation ViewController

很明显,最后打印是null。

但是在一般的业务场景,因为key1和key2的keyName属性都一样,所以应该被看为同一个key。

所以我们要重新hash方法。
如何重写hash方法

一个合理的hash方法要尽量让hash表中的元素均匀分布,来保证较高的查询性能。

如果两个对象可以被视为同一个对象,那么他们的hash值要一样。

mattt在文章Equality 中给出了一个普遍的算法:

Instagram在开源IGListKit的同时,鼓励这么写hash方法:

如何写一个合理高效的判等方法?

首先对内存地址进行判断,地址相等return YES;
进行判空处理,self == nil || object == nil ,return NO;
类型判断,![object isKindOfClass:[self class]] , return NO;
对对象的其他属性进行判断
根据这四个步骤,我们可以发现,我们都是先判断时间开销最少的属性。所以对于第4个步骤,如果对象有很多属性,我们也要依照这个原则来!比如[self.array isEqual:other.array] && self.intVal == other.intVal这种写法是不合理的,因为array的判等会去遍历元素,时间开销大。如果intVal不相等的话就可以直接return NO了,没必要进行数组的判等。应该这么写: self.intVal == other.intVal && [self.array isEqual:other.array]

推荐
热点排行
一周推荐
关闭