数据结构7-哈希表

哈希表的基本原理

首先回顾一下前面实现的数组和链表,不管是动态数组、链表、仍是循环链表,并且若是是从其中查询特定值,都不可避免的要遍历依次遍历全部元素依次去作比较才能够。node

作为面向对象开发语言的使用者,必定用过相似于Map的对象,经过 key: value 存储一个键值对。Objective-C中这种对象就是字典:NSDictionary、NSMutableDictionary,Swift中对于Dictionary。并且经过不少介绍的文章都说过,经过key取值的时间复杂度是O(1)。以前咱们都只是使用它们,如今咱们本身思考,如何利用咱们以前已经了解的数据结构,本身实现一个自定义字典。git

上面提到,经过数组和链表查询值的时候,时间复杂度都是O(n)级别,那么如何实现仅须要O(1)级别就可以查询到值呢?首先回顾以前的几个数据结构:数组、单向链表、双向链表,哪一种数据结构可以作到在任意位置取值都是O(1)?github

答案就是数组,数据能够作到经过下标从数组中的任意位置取值都是O(1)的时间复杂度。既然是这样,咱们就先决定用静态数组来作为存放数据的底层结构。既然是这样,咱们的键值对都是存放在数组中的。可是数组只有经过下标取值才是O(1)的时间复杂度,直接查找值依然须要依次对比元素是否相等。数组

那么接下来的问题就是,如何将key的遍历查找转换成直接经过index去数组中取值。要作到这一点,咱们假设一个函数,这个函数可以作到:将不一样的key转换成对应数组长度范围内的一个index,且不一样的key转换成的index都不同。并且这个函数是O(1)级别的数据复杂度,那么当须要将一个键值对存入数组时,只须要经过这个函数计算一下对应在数组中下标,而后将键值对存放在数组中对应位置。当须要经过key从数组中取时,只须要在经过这个函数计算key对应在数组中下标,而后经过下标去数组中直接取出键值对就能够了。存、取、查都是O(1)级别的时间复杂度。bash

如今就不须要解释什么是哈希表了,上面就是哈希表的原理,静态数组加上一个经过key计算index的函数,就是一个哈希表。下面经过图片看一下咱们上面分析的哈希表的存放数据过程:数据结构

咱们有两对键值对须要存放在咱们的自定义字典中,key是名字,value是年龄,分别是 Whip:1八、Jack:20。app

当存储Whip:18时,首先经过哈希函数计算whip对应在哈希表中的index为2,而后建立一个哈希表的节点对象,key指向Whip,value指向18,而后将节点存储在哈希表中index为2的位置。dom

当存储Jack:20时,首先经过哈希函数计算Jack对应在哈希表中的index为6,而后建立一个哈希表的节点对象,key指向Jack,value指向20,而后将节点存储在哈希表中index为6的位置。函数

下面看一下哈希表的取值过程:post

当须要须要获得Whip的年龄时,哈希表先经过哈希函数计算出Whip在哈希表中存放的位置index = 2,而后直接在数组中index为2的位置拿到存储的节点,返回节点的value值18。

哈希函数

既然须要经过哈希函数计算不一样key对应在数组中的index,那么咱们首先就须要实现一个哈希函数。首先哈希函数须要知足如下条件才是合格的:

  • 不一样的key生成的索引尽量不一样。
  • 生成的索引要在哈希表长度范围内。

在Objective-C中,NSObject对象自带一个hash方法,能够返回一个对象的哈希值:

Person *p1 = [Person personWithAge:1];
Person *p2 = [Person personWithAge:1];
NSLog(@"%zd %zd", p1.hash, p2.hash);

// 打印
4345476000 4345477856
复制代码

能够看的即便是相同类型的而且属性值相同的对象,返回的哈希值也是不一样的。固然也可重写自定义对象的hash方法,返回咱们本身但愿返回的哈希值,作到让属性值相同的对象,返回相同的hash值:

- (NSUInteger)hash {
    NSUInteger hashCode = self.age;
    // 奇素数 hashCode * 31 == (hashCode<<5) - hashCode
    hashCode = (hashCode<<5) - hashCode;
    return hashCode;
}
复制代码

上面的计算是仿照JDK的方式,将整数乘以31得出哈希值,由于31是一个奇素数,和它相乘能够更容易达到惟一性,减小冲突。

既然重写了hash方法,还须要配套重写对象的 isEqual方法,一个正确的判断逻辑须要知足:

  • 两个对象isEqual:判断为true,那么两个对象hash返回值必定相等。
  • 哈希值相等的两个对象,isEqual: 不必定相等。

想象一下,若是两个对象isEqual:返回true,意味着两个对象相等,可是它们的哈希值不想等,意味着它们可能存放在哈希表中的不一样位置,这样就至关于Map或者NSDictionary中存放了两个key相同的键值对,这明显是不符合逻辑的。

key的哈希值咱们已经知道如何使用默认和自定义的方法返回,下面咱们经过key的哈希值计算其在数组中的index。首先key的哈希值也是一个整数,而且长度不必定。好比上面,默认返回:4345477856,咱们自定义hash方法后返回31。假设咱们的数组长度只有8,那么确定不能将key的哈希值直接看成数组中的index。

这里咱们能够经过 & 位运算来获得,前提是数组的长度是2的n次方,假设数组的长度为 2^3 = 8,8 - 1 = 7,其对应的二进制位为:0111。

下面咱们和0111作 & 位运算的效果:

// 十进制253
1111 1101 
&    0111
---------
     0101 = 5
     
     
// 十进制31
0001 1111
&    0111
---------
     0111 = 7
     
     
// 十进制8
0000 1000
&    0111
---------
     0000 = 0
复制代码

能够看到,任何数字和7作&位运算的结果都不会大于8,即key的哈希值经过和数组长度-1的值(数组长度为2的幂)作&位运算就能够获得key哈希值对应在数组中的index:

- (NSUInteger)indexWithKey:(id)key {
    if (!key) return 0;
    NSUInteger hash = [key hash];
    return hash & (self.array.length - 1);
}
复制代码

如今这个函数还不是很好用,好比下面的状况,假定哈希表长度为2^6,2^6 - 1 = 63,二进制位为:0011 1111

// 6390901416293117903
0101 1000 1011 0001 
0000 1010 1111 1010 
0100 1000 1011 0001 
0010 1111 1100 1111
&          011 1111
----------------------
           000 1111 = 15
     
     
// 6390901416293511119
0110 1000 1011 0010 
0000 1010 1111 1010
0100 1000 1011 0111 
0010 1111 1100 1111            
&          011 1111
----------------------
           000 1111 = 15
复制代码

6390901416293117903 和 6390901416293511119 因为最后7位2进制位都是 1001111,因此和 011 1111 作位运算以后结果都是 000 1111 = 15,高位都没有参与运算,致使只要末 7 位同样的哈希值的key在数组中index都相同,而咱们应该尽可能让全部的哈希值位数都参与运算。

下面将哈希值右移16位,而后和原来的哈希值作 ^ 运算,而后在与数组长度 -1 作 & 运算,以后的结果:

(6390901416293117903 ^ (6390901416293117903 >> 16)) & 63); // 62
(6390901416293511119 ^ (6390901416293511119 >> 16)) & 63); // 56
复制代码

下面是优化后最终哈希表的哈希函数:

- (NSUInteger)indexWithKey:(id)key {
    if (!key) return 0;
    NSUInteger hash = [key hash];
    return (hash ^ (hash >> 16)) & (self.array.length - 1);
}
复制代码

虽然上面的优化能够必定程度避免不一样的哈希值计算出相同的index,可是依然不能彻底避免,好比:6390901416293117903 和 6681946542211936207,由于它们的二进制的最后八位同样,而右移后的最后八位仍然同样,最终和63作位运算的最后几位也是同样的值,这样就不可避免的出现的不一样key经过哈希函数计算后,在数组中的index是相同的状况。

哈希冲突

对键值对须要存放如哈希表,经过key计算出index后,发现哈希表中该位置已经存放了其它键值对。数组中index位置键值对的key和当前正在添加的键值对的key,经过哈希函数计算出得index是同样的,就会出现这种问题。出现这种问题分为两种状况:

  • 当前的key和数组中index位置的key是相等的,这种只须要将新键值覆盖就能够了。
  • 当前的key和数组的index位置的key是不相等的,即两个不一样的key,经过哈希函数计算得出的index相同。

出现第二种状况就是所谓的哈希冲突,这种冲突是不可避免的,解决哈希冲突的办法有不少种:

  • 再次经过其它规则计算index,直到找到一个空的位置。
  • 找到index前面或者后面第一个不为空的位置。
  • 仍在index位置放置元素,并将两个元素以链表等形式存储。

咱们这里采用第三种方式,即发现冲突的时候,以链表的形式将一个index内的全部元素串起来,以下图:

存储Jack:20 的时候,经过哈希函数计算出得index = 2,发现哈希表中index为2的位置已经有了其它节点,这时就将最后一个节点的next指向新的节点。

这种状况下的取值以下图:

首先经过哈希函数计算key对应的index,而后找到数组中对应位置的第一个节点,比较该节点存储的key和查询的key是否相等,若是不相等则经过该节点next指针找到下一个节点,重复判断过程,直到找到key相等的节点。

哈希表的基本结构

经过上面的分析,哈希表的结构其实已经很清晰了,首先哈希表是一个静态数组,数据中存放哈希表的节点,哈希表的节点是一个单向链表的结构,每个节点经过next指针指向下一个节点,节点另外须要两个指针指向key和value。咱们还须要实现一个哈希函数,能够经过key计算出其对应在哈希表中的位置,这个函数咱们前面已经实现了。

我这里将哈希表封装成一个相似字典的类,外部接口彻底和系统的NSMutableDictionary一致,实现的功能也是同样的。首先建立一个JKRHashMap_LinkedList类。

_size来保存当前哈希表存放的键值对的数量,注意区分这里的_size是存放键值对的数量,而不是哈希表的长度。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JKRHashMap_LinkedList<KeyType, ObjectType> : NSObject{
@protected
    /// 节点个数
    NSUInteger _size;
}

/// 元素个数
- (NSUInteger)count;
/// 清空全部元素
- (void)removeAllObjects;
/// 删除元素
- (void)removeObjectForKey:(KeyType)key;
/// 添加一个元素
- (void)setObject:(nullable ObjectType)object forKey:(nullable KeyType)key;
/// 获取元素
- (nullable ObjectType)objectForKey:(nullable KeyType)key;
/// 是否包含元素
- (BOOL)containsObject:(nullable ObjectType)object;
/// 是否包含key
- (BOOL)containsKey:(nullable KeyType)key;

@end

@interface JKRHashMap_LinkedList<KeyType, ObjectType> (JKRExtendedHashMap)

- (nullable ObjectType)objectForKeyedSubscript:(nullable KeyType)key;
- (void)setObject:(nullable ObjectType)obj forKeyedSubscript:(nullable KeyType)key;

@end


NS_ASSUME_NONNULL_END
复制代码

而后建立哈希表的节点对象:

@interface JKRHashMap_LinkedList_Node : NSObject

@property (nonatomic, strong) id key;
@property (nonatomic, strong) id value;
@property (nonatomic, strong) JKRHashMap_LinkedList_Node *next;

@end
复制代码

哈希表的基本功能

初始化

首先哈希表中应该有一个静态数组,因为Objective-C不提供,因此在第一篇文章中已经提早实现了一个静态数组,它须要在哈希表初始化的时候建立,而且长度为2的幂:

@interface JKRHashMap_LinkedList ()

@property (nonatomic, strong) JKRArray *array;

@end

@implementation JKRHashMap_LinkedList

- (instancetype)init {
    self = [super init];
    self.array = [JKRArray arrayWithLength:1 << 4];
    return self;
}

@end
复制代码

哈希函数

前面已经实现了,这里直接就可使用:

- (NSUInteger)indexWithKey:(id)key {
    if (!key) return 0;
    NSUInteger hash = [key hash];
    return (hash ^ (hash >> 16)) & (self.array.length - 1);
}
复制代码

经过key查找节点

正如上面分析的查找过程,

  • 1,首先经过哈希函数找到key对应的index。
  • 2,经过index获取哈希表中对应位置的节点。
  • 3,若是节点为空,则key不在哈希表中,返回null。
  • 4,若是节点存在,则比较节点的key的查询key是否相等,相等直接返回该节点,不然进入下一步。
  • 5,获得该节点的下一个节点,重复第三步。
- (JKRHashMap_LinkedList_Node *)nodeWithKey:(id)key {
    NSUInteger index = [self indexWithKey:key];
    JKRHashMap_LinkedList_Node *node= self.array[index];
    while (node) {
        if (node.key == key || [node.key isEqual:key]) {
            return node;
        } else if (key && node.key && [key class] == [node.key class] && [key respondsToSelector:@selector(compare:)] && [key compare:node.key] == 0){
            return node;
        }
        node = node.next;
    }
    return node;
}
复制代码

经过key获取object

上面已经实现了经过key获取节点,这里只须要将返回的节点的value返回:

- (id)objectForKey:(id)key {
    JKRHashMap_LinkedList_Node *node = [self nodeWithKey:key];
    return node ? node.value : nil;
}
复制代码

是否存包含key

上面已经实现了经过key获取节点,这里只须要判断返回的节点是否为空:

- (BOOL)containsKey:(id)key {
    return [self nodeWithKey:key] != nil;
}
复制代码

返回哈希表的键值对数量

只须要返回_size:

- (NSUInteger)count {
    return _size;
}
复制代码

添加键值对

添加键值对的步骤:

  • 1,经过key计算出index。
  • 2,经过index取出哈希表对应位置的节点。
  • 3,若是节点为空,直接建立一个新节点并存储传入的key和value,并将节点指针存入哈希表, _size++。不然进入下一步。
  • 4,判断节点的key和传入的key是否相等,若是相等,则须要进行覆盖操做,将传入的key和value覆盖节点的key和value。不然进入下一步。
  • 5,保存一下该节点为preNode,而后获取该节点的下一个节点,判断节点是否为空,若是为空,建立一个新节点,将key和value存入新节点的key和value,并将preNode的next指向新节点,_size++。不然返回步骤4。
- (void)setObject:(id)object forKey:(id)key {
    NSUInteger index = [self indexWithKey:key];
    JKRHashMap_LinkedList_Node *node = self.array[index];
    if (!node) {
        node = [JKRHashMap_LinkedList_Node new];
        node.key = key;
        node.value = object;
        self.array[index] = node;
        _size++;
        return;
    }
    
    JKRHashMap_LinkedList_Node *preNode = nil;
    while (node) {
        if (node.key == key || [node.key isEqual:key]) {
            break;
        } else if (key && node.key && [key class] == [node.key class] && [key respondsToSelector:@selector(compare:)] && [key compare:node.key] == 0) {
            break;
        }
        preNode = node;
        node = node.next;
    }
    
    if (node) {
        node.key = key;
        node.value = object;
        return;
    }
    
    JKRHashMap_LinkedList_Node *newNode = [JKRHashMap_LinkedList_Node new];
    newNode.key = key;
    newNode.value = object;
    preNode.next = newNode;
    _size++;
}
复制代码

删除键值对

  • 1,经过key计算出index。
  • 2,经过index取出当前哈希表对应的节点。
  • 3,判断该节点是否为空,若是节点为空,直接返回。不然进入下一步。
  • 4,判断该节点的key是否和传入的key相等,若是相等,删除该节点,_size--。 不然进入下一步。
  • 5,拿到该节点的下一个节点,重复第4步。
- (void)removeObjectForKey:(id)key {
    NSUInteger index = [self indexWithKey:key];
    JKRHashMap_LinkedList_Node *node= self.array[index];
    JKRHashMap_LinkedList_Node *preNode = nil;
    while (node) {
        if (node.key == key || [node.key isEqual:key]) {
            if (preNode) {
                preNode.next = node.next;
            } else {
                self.array[index] = node.next;
            }
            _size--;
            return;
        } else if (key && node.key && [key class] == [node.key class] && [key respondsToSelector:@selector(compare:)] && [key compare:node.key] == 0){
            if (preNode) {
                preNode.next = node.next;
            } else {
                self.array[index] = node.next;
            }
            _size--;
            return;
        }
        preNode = node;
        node = node.next;
    }
}
复制代码

是否包含某元素

由于哈希表的value存放在节点中,而且没法直接找到其位置,只能经过遍历哈希表全部节点实现:

- (BOOL)containsObject:(id)object {
    if (_size == 0) {
        return NO;
    }
    
    for (NSUInteger i = 0; i < self.array.length; i++) {
        JKRHashMap_LinkedList_Node *node= self.array[i];
        while (node) {
            if (node.value == object || [node.value isEqual:object]) {
                return YES;
            }
            node = node.next;
        }
    }
    return NO;
}
复制代码

让自定义的哈希表支持字典运算符

- (id)objectForKeyedSubscript:(id)key {
    return [self objectForKey:key];
}

- (void)setObject:(id)obj forKeyedSubscript:(id)key {
    [self setObject:obj forKey:key];
}
复制代码

重写打印方便查看哈希表结构

- (NSString *)description {
    NSMutableString *string = [NSMutableString string];
    [string appendString:[NSString stringWithFormat:@"<%@, %p>: \ncount:%zd length:%zd\n{\n", self.className, self, _size, self.array.length]];
    for (NSUInteger i = 0; i < self.array.length; i++) {
        [string appendString:[NSString stringWithFormat:@"\n\n--- index: %zd ---\n\n", i]];
        JKRHashMap_LinkedList_Node *node= self.array[i];
        if (node) {
            while (node) {
                [string appendString:[NSString stringWithFormat:@"[%@:%@ -> %@%@] ", node.key , node.value, node.next ? [NSString stringWithFormat:@"%@:", node.next.key] : @"NULL", node.next ? node.next.value : @""]];
                node = node.next;
                if (i) {
                    [string appendString:@", "];
                }
            }
        } else {
            [string appendString:@" "];
            [string appendString:@"NULL"];;
        }
    }
    [string appendString:@"\n}"];
    return string;
}

复制代码

哈希表的功能测试

JKRHashMap_LinkedList *dic = [JKRHashMap_LinkedList new];
for (NSUInteger i = 0; i < 30; i++) {
    NSString *key = getRandomStr();
    dic[key] = [NSString stringWithFormat:@"%zd", i];
}
NSLog(@"%@", dic);

// 打印:
<JKRHashMap_LinkedList, 0x102814a10>: 
count:30 length:16
{


--- index: 0 ---

[Wlqvuq:2 -> Xecsbw:9] [Xecsbw:9 -> Kvfexi:11] [Kvfexi:11 -> NULL] 

--- index: 1 ---

[Ifaeuy:15 -> NULL] , 

--- index: 2 ---

[Bmitqy:3 -> Ynqbcw:12] , [Ynqbcw:12 -> NULL] , 

--- index: 3 ---

[Djwmew:0 -> Epzzlc:4] , [Epzzlc:4 -> Jqjrvq:22] , [Jqjrvq:22 -> NULL] , 

--- index: 4 ---

[Myvwre:28 -> NULL] , 

--- index: 5 ---

[Mrgpfv:8 -> Ltdazq:25] , [Ltdazq:25 -> Tzweni:27] , [Tzweni:27 -> NULL] , 

--- index: 6 ---

   NULL

--- index: 7 ---

[Eyvque:5 -> Ltmzik:24] , [Ltmzik:24 -> NULL] , 

--- index: 8 ---

[Rvnupm:7 -> NULL] , 

--- index: 9 ---

[Ryrort:16 -> NULL] , 

--- index: 10 ---

[Rsdkaw:1 -> Hgszuk:20] , [Hgszuk:20 -> Jtrtes:26] , [Jtrtes:26 -> NULL] , 

--- index: 11 ---

[Txonlm:29 -> NULL] , 

--- index: 12 ---

[Bvbdbe:14 -> NULL] , 

--- index: 13 ---

[Pszvix:6 -> Dtizif:19] , [Dtizif:19 -> Czkxyj:21] , [Czkxyj:21 -> Kzatxv:23] , [Kzatxv:23 -> NULL] , 

--- index: 14 ---

[Ustobp:10 -> Erqclk:13] , [Erqclk:13 -> Fbliqs:17] , [Fbliqs:17 -> Jpcvbm:18] , [Jpcvbm:18 -> NULL] , 

--- index: 15 ---

   NULL

}
复制代码

能够看到,哈希表中的值平均分散在数组中,出现哈希冲突时,以单向链表的形式存储。

和NSMutableDictionary对比哈希表的性能测试

既然要作性能的对比测试,首先须要数据,这里使用苹果公布的objc4源代码,官方下载地址:opensource.apple.com/tarballs/ob…,将其中runtime的源码文件夹看成资源文件:

读取其中全部文件的代码,取出其中的全部单词,计算不一样单词出现的次数。

这个需求恰好能够利用字典或者咱们自定义的哈希表,将单词作为key,将单词出现的次数作为value,计算逻辑以下:

  • 首先读取全部文件并截取出全部单词(包括重复的)。
  • 遍历全部单词,将单词作为key,依次添加到哈希表中。
  • 添加前,先经过key从哈希表中取值,若是取到,则value为取到的值+1,不然为1,将key、value存入哈希表中。

这样当依次遍历添加完全部单词后,哈希表中存放的就是每一个单词出现的次数。

首先取出全部单词:

NSMutableArray * allFileStrings() {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *fileManagerError;
    NSString *fileDirectory = @"/Users/Lucky/Documents/SourceCode/runtime";
    
    NSArray<NSString *> *array = [fileManager subpathsOfDirectoryAtPath:fileDirectory error:&fileManagerError];
    if (fileManagerError) {
        NSLog(@"读取文件夹失败");
        nil;
    }
    NSLog(@"文件路径: %@", fileDirectory);
    NSLog(@"文件个数: %zd", array.count);
    NSMutableArray *allStrings = [NSMutableArray array];
    [array enumerateObjectsUsingBlock:^(NSString *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSString *filePath = [fileDirectory stringByAppendingPathComponent:obj];
        NSError *fileReadError;
        NSString *str = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&fileReadError];
        if (fileReadError) {
            return;
        }
        [str enumerateSubstringsInRange:NSMakeRange(0, str.length) options:NSStringEnumerationByWords usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
            [allStrings addObject:substring];
        }];
    }];
    NSLog(@"全部单词的数量: %zd", allStrings.count);
    return allStrings;
}
复制代码

而后依次遍历并加入哈希表中:

JKRHashMap_LinkedList *map = [JKRHashMap_LinkedList new];
[JKRTimeTool teskCodeWithBlock:^{
    NSMutableDictionary *map = [NSMutableDictionary new];
    [allStrings enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSNumber *count = map[obj];
        if (count) {
            count = [NSNumber numberWithInteger:count.integerValue+1];
        } else {
            count = [NSNumber numberWithInteger:1];
        }
        map[obj] = count;
    }];
    NSLog(@"NSMutableDictionary 计算不重复单词数量和出现次数 %zd", map.count);
    NSLog(@"NSMutableDictionary 计算单词出现的次数NSObject: %@", map[@"NSObject"]);
    NSLog(@"NSMutableDictionary 计算单词出现的次数include: %@", map[@"include"]);
    NSLog(@"NSMutableDictionary 计算单词出现的次数return: %@", map[@"return"]);
    
    __block NSUInteger allCount = 0;
    [allStrings enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        allCount += [map[obj] integerValue];
        [map removeObjectForKey:obj];
    }];
    
    NSLog(@"NSMutableDictionary 累加计算全部单词数量 %zd", allCount);
}];

// 打印:
文件个数: 104
全部单词的数量: 165627
JKRHashMap_LinkedList 计算不重复单词数量和出现次数 10490
JKRHashMap_LinkedList 计算单词出现的次数NSObject: 34
JKRHashMap_LinkedList 计算单词出现的次数include: 379
JKRHashMap_LinkedList 计算单词出现的次数return: 2681
JKRHashMap_LinkedList 累加计算全部单词数量 165627
耗时: 14.768 s
复制代码

下面使用NSMutableDictionary测试一遍:

[JKRTimeTool teskCodeWithBlock:^{
    NSMutableDictionary *map = [NSMutableDictionary new];
    [allStrings enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSNumber *count = map[obj];
        if (count) {
            count = [NSNumber numberWithInteger:count.integerValue+1];
        } else {
            count = [NSNumber numberWithInteger:1];
        }
        map[obj] = count;
    }];
    NSLog(@"NSMutableDictionary 计算不重复单词数量和出现次数 %zd", map.count);
    NSLog(@"NSMutableDictionary 计算单词出现的次数NSObject: %@", map[@"NSObject"]);
    NSLog(@"NSMutableDictionary 计算单词出现的次数include: %@", map[@"include"]);
    NSLog(@"NSMutableDictionary 计算单词出现的次数return: %@", map[@"return"]);
    
    __block NSUInteger allCount = 0;
    [allStrings enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        allCount += [map[obj] integerValue];
        [map removeObjectForKey:obj];
    }];
    
    NSLog(@"NSMutableDictionary 累加计算全部单词数量 %zd", allCount);
}];

// 打印:
文件个数: 104
全部单词的数量: 165627
NSMutableDictionary 计算不重复单词数量和出现次数 10490
NSMutableDictionary 计算单词出现的次数NSObject: 34
NSMutableDictionary 计算单词出现的次数include: 379
NSMutableDictionary 计算单词出现的次数return: 2681
NSMutableDictionary 累加计算全部单词数量 165627
耗时: 0.058 s
复制代码

发现全部的计算结果都是同样的,证实咱们的哈希表确实能够和系统的NSMutableDictionary完成同样的功能,可是时间上有这很是大差距:14.768s VS 0.058s,好吧,简直慢到不能忍。

哈希表的优化-扩容

首先分析下为何咱们的哈希表这么慢,其实很简单,由于咱们的哈希表默认容量是 1 << 4 = 16,长度只有16的数组内,分散存放了10490条数据,平均数组的一个位置存放了655个节点,即每一个单向链表平均长度达到了655,而基于咱们上面实现的哈希表的存、取、读都是经过单向链表从头节点开始遍历,那么当哈希表元素过多,链表长度越长时,遍历所需时间必然越长。

为了防止每条链表过长,咱们须要在哈希表元素达到必定数量就要扩展哈希表数组的长度,这很是相似于动态数组的扩容操做。同时,若是哈希表数组长度发生变化,每一个key对应哈希函数计算出的index必然发生变化,那么原来存放在哈希表中的节点还须要从新调整在哈希表中的位置。

那么何时去扩充哈希表的容量呢,据科学统计,当哈希表的存储的元素个数大于数组的长度 * 0.75时,扩容最优,咱们就采用这个规则。

首先在添加键值对的方法最开始添加一个扩容方法:

- (void)setObject:(id)object forKey:(id)key {
    [self resize];
    // ...
}
复制代码

当数组元素个数小于哈希表长度 * 0.75 时,不去扩容,不然就扩容。

- (void)resize {
    if (_size <= self.array.length * 0.75) return;
    
}
复制代码

扩容须要建立一个新的容量更大的数组,这里咱们采用扩容后的数组是原来的数组的两倍:

JKRArray *oldArray = self.array;
self.array = [JKRArray arrayWithLength:oldArray.length << 1];
复制代码

须要将原来数组中的全部节点从新排列在哈希表中,这里采用复用原来的节点,只将它们的位置从新排列,而不是依次取出值从新添加到哈希表中,由于这样须要重建建立全部节点,咱们这里节省没必要要的开销:

for (NSUInteger i = 0; i < oldArray.length; i++) {
    JKRHashMap_LinkedList_Node *node = oldArray[i];
    while (node) {
        JKRHashMap_LinkedList_Node *moveNode = node;
        node = node.next;
        moveNode.next = nil;
        // 从新排列节点
        [self moveNode:moveNode];
    }
}
复制代码

从新排列节点的逻辑以下:

  • 1,取出该节点的key计算index。
  • 2,经过index取出节点。
  • 3,判断节点是否为空。若是为空进入第4步,不然进入第5步。
  • 4,将数组index位置存放该节点。
  • 5,依次遍历到最后一个节点,将最后一个节点的next指向该节点。

完整扩容逻辑以下:

- (void)resize {
    if (_size <= self.array.length * 0.75) return;
    JKRArray *oldArray = self.array;
    self.array = [JKRArray arrayWithLength:oldArray.length << 1];
    for (NSUInteger i = 0; i < oldArray.length; i++) {
        JKRHashMap_LinkedList_Node *node = oldArray[i];
        while (node) {
            JKRHashMap_LinkedList_Node *moveNode = node;
            node = node.next;
            moveNode.next = nil;
            [self moveNode:moveNode];
        }
    }
}

- (void)moveNode:(JKRHashMap_LinkedList_Node *)newNode {
    NSUInteger index = [self indexWithKey:newNode.key];
    JKRHashMap_LinkedList_Node *node = self.array[index];
    if (!node) {
        self.array[index] = newNode;
        return;
    }
    JKRHashMap_LinkedList_Node *preNode = nil;
    while (node) {
        preNode = node;
        node = node.next;
    }
    preNode.next = newNode;
}
复制代码

扩容后性能对比

扩容后重复上面的测试,打印以下:

NSMutableDictionary 计算不重复单词数量和出现次数 10490
耗时: 0.066 s
JKRHashMap_LinkedList 计算不重复单词数量和出现次数 10490
耗时: 0.192 s
复制代码

时间已经从以前的 14.768s 减小到 0.192s。

接下来

仅仅使用单向链表实现的哈希表并不可以保证全部状况的查找速度,当哈希函数计算出现问题或者数据量特别大的时候,极可能出现某一条单向链表长度很是长。

在JDK开源的哈希表解决方案中,单链表长度超过必定值时,将链表转换成红黑树的方法解决这个问题,后面也会采用红黑树的方式从新实现一遍哈希表。可是在这以前,会先介绍队列和栈的实现,由于它们这是二叉树操做的基础。

源码

点击查看源码