哈希算法详解(附带 iOS 开发中实际应用)

Posted 数据结构与算法

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈希算法详解(附带 iOS 开发中实际应用)相关的知识,希望对你有一定的参考价值。


作者丨ZhengYaWei
https://www.jianshu.com/p/2a71b027b723


前言


哈希(Hash)或者说散列表,它是一种基础数据结构。Hash 表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,但它又是是数组和链表的基础上演化而来,既具有数组的有点,又具有链表的有点。能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。应用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。


哈希算法详解(附带 iOS 开发中实际应用)一、Hash设计思想


试想如果我们对一个数组进行查询,这个数组里,每一个元素都是一个字符串。我们知道数组最快的检索办法是通过数组的下标进行检索,但是对于这种场景,我们无能为力,只能从头查到尾,从而查询出目标元素。


哈希算法详解(附带 iOS 开发中实际应用)


如果我们要根据名字找到其中的任何一个元素,就需要遍历整个数组。最坏情况下时间复杂度是O(n) ,但是借助 Hash 可以将时间复杂度降为O(1)。



所谓的 hash 算法就是将字符串转换为数字的算法。为了更好说明这种设计思想,笔者先设计出一种最笨的 Hash 函数,将所有字符串中的字符转化为数字后相加。


哈希算法详解(附带 iOS 开发中实际应用)


上表中数组的下标就是字符串对应的数字值。根据对应的数字值,我们就能轻易找到任何想要的对象,时间复杂度为O(1)。


哈希算法详解(附带 iOS 开发中实际应用)二、Hash函数设计


所谓的 hash 算法就是将字符串转换为数字的算法。通常有以下几种构造 Hash 函数的方法:


2.1 直接定址法



2.2 平方取中法



2.3 折叠法



2.4 除留取余法


如果知道 Hash 表的最大长度为 m,可以取不大于m的最大质数 p,然后对关键字进行取余运算,address(key)=key % p。这里 p 的选取非常关键,p 选择的好的话,能够最大程度地减少冲突,p 一般取不大于m的最大质数。


哈希算法详解(附带 iOS 开发中实际应用)三、Hash表大小的确定


Hash 表的空间如果远远大于实际存储的记录数据的个数,则造成空间浪费;如果过小,则容易造成冲突。Hash 表大小确定通常有这两种思路:


如果最初知道存储的数据量,则需要根据存储个数 和 关键字的分布特点来确定 Hash 表的大小。



哈希算法详解(附带 iOS 开发中实际应用)四、Hash 冲突及解决方案


4.1 Hash冲突产生


有这样一个问题:因为我们是用数组大小对哈希值进行取模,有可能不同键值所得到的索引值相同,这里就是冲突。如在最初的实例中,如果多出了sizhang这样一个元素,那么就存在两个 756。


哈希算法详解(附带 iOS 开发中实际应用)



4.2 Hash 冲突解决


4.2.1 开放定址法



4.2.2 链地址法


哈希算法详解(附带 iOS 开发中实际应用)



所以针对之前案列冲突的解决方案如下:


哈希算法详解(附带 iOS 开发中实际应用)


检索的时候可以这样检索,首先找到gaofei后,之后再遍历链表,找到feigao了。同理对于 sizhang 的冲突也是如此解决。


哈希算法详解(附带 iOS 开发中实际应用)五、Hash 表的用处以及优劣


5.1 Hash 表的实际应用


上述说了这么多关于 Hash 表的知识点,但是 Hash 表在代码的世界中,实际上又有什么应用场景,可能有些读者会一头雾水,这里笔者就以简单的三个例子来说明 Hash 表的实际应用场景。


1 、找出两文件找出重复的元素


假设有两个文件,文件中均包含一些短字符串,字符串个数分别为n。它们是有重复的字符串,现在需要找出所有重复的字符串。


最笨的解决办法可能是:遍历文件 1 中的每个元素,取出每一个元素分别去文件 2 中进行查找,这样的时间复杂度为O(n^2)。


但是借助 Hash 表可以有一种相对巧妙的方法,分别遍历文件 1 中的元素和文件 2 中的元素,然后放入 Hash Table 中,对于遍历的每一个元素我们只要简单的做一下计数处理即可。最后遍历整个 Hash 列表,找出所有个数大于 1 的元素即为重复的元素。


2、找出两文件找出出现次数最多的元素


同找出两文件找出重复的元素这样的问题解决方案类似,只是在最后遍历的时找计数最大的元素,即为出现次数最多的元素。


3、路由算法


多线程处理数据的场景下,通常需要将一个数据集分给不同的线程进行处理,同时要保证,相同的元素需要分到相同的处理线程上。这
其实这个就是一个很典型的 Hash 值应用场景,对于很多的计算引擎默认都是用 Hash 算法去解决这个问题。因为相同元素的 Hash 值相同,那么我们可以取 Hash 之后进行模运算,运算结果分配到不同的线程。


哈希算法详解(附带 iOS 开发中实际应用)


5.2 Hash 表的优缺点及注意点


优点


哈希表的效率非常高,查找、插入、删除操作只需要接近常量的时间即0(1)的时间级。如果需要在一秒种内查找上千条记录通常使用哈希表,哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。如果不需要遍历数据,不二的选择。


缺点


它是基于数组的,数组创建后难于扩展。有些情况下,哈希表被基本填满时,性能下降得非常严重,所以开发者必须要清楚表中将要存储的数据量。或者也可以定期地把数据转移到更大的哈希表中,不过这个过程耗时相对比较大。


注意点


在设计Hash算法的时候。一定要保证相同字符串产生的 Hash 值相同,同时要尽量的减小Hash冲突的发生,这样才算是好的 hash 算法。


哈希算法详解(附带 iOS 开发中实际应用)六、Hash 在 ios 中的应用


这一部分的篇幅可能稍稍有点大,笔者原本打算给这一部分抽出来单独写一篇文章,但是发现没有 Hash 概念做铺垫,文章略显空洞,所以这里干脆把所有东西整合到一起,请读者耐下心来看。


这一部分的内容就以下面的一个问题为中心。并在此问题上不断的扩充,以点带面。


iOS系统API给我们提供一个自动过滤重复元素的容器  NSMutableSet/NSSet,如:当我们向该实例对象中添加字符串时,如果重复添加两个相同的字符串,集合中只会保留一个。NSMutableSet/NSSet内部一些实现机制要比我们自己写的滤重方法效率高。但是对于自定义一个类如Person,如果想利用NSMutableSet/NSSet来过滤重复元素(如多个Person实例的uid相同),我们必须要同时实现- (BOOL)isEqual:和- (NSUInteger)hash这两个方法。这里先简单介绍他们的关系:两个相等的实例,他们的hash值一定相等。但是hash值相等的两个实例,不一定相等。重点来了,利用 NSMutableSet/NSSet 具体如何实现过滤 Person 重复元素 ?


在解决这个问题之前我先用简单的篇幅 6.1 小结 和 6.2 小结 分别介绍- (BOOL)isEqual:和- (NSUInteger)hash这两个方法。具体可以参考这篇文章。


6.1  关于- (BOOL)isEqual:方法


为什么要有isEqual方法?


OC 中 == 运算符只是简单地判断是否是同一个对象, 而 isEqual 方法可以判断对象是否相同。


如何重写isEqual方法?


但对于自定义类型来说, 做判等时通常需要重写isEqual方法。


@interface Person : NSObject
@property (nonatomiccopyNSString *name;
@property (nonatomicstrongNSDate *birthday;
@end


- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[Person class]]) {
        return NO;
    }

    return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
    if (!person) {
        return NO;
    }

    BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
    BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

    return haveEqualNames && haveEqualBirthdays;
}


上述代码主要步骤如下:


1、 ==运算符判断是否是同一对象, 因为同一对象必然完全相同


2、 判断是否是同一类型, 这样不仅可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险


3、通过封装的isEqualToPerson方法, 提高代码复用性


4、 判断person是否是nil, 做参数有效性检查


5、 对各个属性分别使用默认判等方法进行判断


6、 返回所有属性判等的与结果


6.2 关于- (NSUInteger)hash方法


hash方法什么时候被调用?


如果在 Person 类中重写- (NSUInteger)hash方法,该方法只在 Person 实例对象被添加至NSSet或将Person实例对象设置为NSDictionary的 key 时会调用。注意是设置为 key 而不是 value


hash方法和判等的关系?


为了优化判等的效率, 基于 hash 的 NSSet 和 NSDictionary 在判断成员是否相等时, 通常会这样做:


首先判断 hash 值是否和目标 hash 值相等。如果相同再进行对象之后的判等逻辑, 作为判等的结果; 如果不等, 直接判断为不相等。


简单地说:hash值是对象判等的必要非充分条件。


如何重写 hash 方法?


很多人在iOS开发中, 都是这么重写hash方法的,如果自己亲自测试一下会发现直接重写父类方法并不能实现过滤重复元素的功能。


- (NSUInteger)hash {
    return [super hash];
}


对于上面的 Person 类正确的 Hash 实现方法应该是借助位运算。代码如下:


- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}


6.3  同时实现- (BOOL)isEqual: 和 - (NSUInteger)hash方法,实现过滤自定义实例的功能


6.3.1 代码实现

@interface Person : NSObject
@property (nonatomicassignNSInteger uid;
@property (nonatomicstrongNSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}

- (NSUInteger)hash{
    NSUInteger hashValue = self.uid; //在这里只需要比较uid就行。这
    样的话就满足如果两个实例相等,那么他们的 hash 一定相等,但反过
    来hash值相等,那么两个实例不一定相等。但是在 Person 这个实例
    中,hash值相等那么实例一定相等。(不考虑继承之类的)
    NSLog(@"hash = %lu,addressValue = %lu,address = %p",(NSUInteger)hashValue,(NSUInteger)self,self);
    return hashValue;
}
@end

//调用重写hash后的方法
- (void)viewDidLoad {
    [super viewDidLoad];
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    NSLog(@"begin add %@",person1);
    [self.mutSet addObject:person1];
    NSLog(@"after add %@",person1);

    NSLog(@"begin add %@",person2);
    [self.mutSet addObject:person2];
    NSLog(@"after add %@",person2);

    NSLog(@"count = %d",self.mutSet.count);

    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    NSLog(@"begin add %@",person3);
    [self.mutSet addObject:person3];
    NSLog(@"after add %@",person3);

    NSLog(@"count = %d",self.mutSet.count);
}

6.3.2 关于一些结论

NSMutableSet/NSSet中添加 Person 对象的时候,就会调用- (NSUInteger)hash方法。


NSMutableSet/NSSet中添加 personA 对象的时候,如果NSMutableSet/NSSet 中之前就已经存在 personB对象,且 personB 对象的 - (NSUInteger)hash返回值和personA的- (NSUInteger)hash返回值相等, 则 personA 会继续调用- (BOOL)isEqual:方法  ,其中此方法以personB为参数;否则不等, 继续下一个元素判断。


具体的判等过程如下。


1、如果 personB 
的- (NSUInteger)hash返回值是否和 personA 的- (NSUInteger)hash返回值相等,则直接执行第 3 步;如果不相等,则执行第 2 步。


2、判断 NSMutableSet/NSSet 中是否存在下一个没有比较过的元素,如果有继续执行第 1 步;如果没有,则personA 会被添加到NSMutableSet/NSSet 集合中,执行结束命令。


3、调用 personA 的- (BOOL)isEqual:其中该方法 以personB为参数,如果返回结果为 NO(两者不相等), 则执行第 2 步;如果返回结果为Yes(两者是相同元素),则 NSMutableSet/NSSet 中存在和 personA 相同的元素,personA不会被添加到集合中,直接执行结束命令。


哈希算法详解(附带 iOS 开发中实际应用)七、总结


本文章主要讲解了 Hash 的设计思想、Hash 函数设计、Hash 冲突的产生和解决、 Hash 的优缺点以及应用,最后结合实际代码,说明了 Hash 在 iOS 判等过程中的实际应用。关于 Hash 实际上还有很多值得我们研究的问题,就单单是  Hash 函数设计而言,就足够我们花上很多功夫去研究,当然感兴趣的同学可以去仔细研究下。


 推荐↓↓↓ 

以上是关于哈希算法详解(附带 iOS 开发中实际应用)的主要内容,如果未能解决你的问题,请参考以下文章

一致性哈希算法原理详解

哈希算法原理和用途

如何删除 iOS 应用程序附带的敏感信息

多阶Hash算法

网络安全加解密算法最详解

一致性哈希算法在分布式场景中的应用