首先回顧一下前面實現的數組和鏈表,不管是動態數組、鏈表、仍是循環鏈表,並且若是是從其中查詢特定值,都不可避免的要遍歷依次遍歷全部元素依次去作比較才能夠。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,那麼咱們首先就須要實現一個哈希函數。首先哈希函數須要知足如下條件纔是合格的:
在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,意味着兩個對象相等,可是它們的哈希值不想等,意味着它們可能存放在哈希表中的不一樣位置,這樣就至關於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是同樣的,就會出現這種問題。出現這種問題分爲兩種狀況:
出現第二種狀況就是所謂的哈希衝突,這種衝突是不可避免的,解決哈希衝突的辦法有不少種:
咱們這裏採用第三種方式,即發現衝突的時候,以鏈表的形式將一個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);
}
複製代碼
正如上面分析的查找過程,
- (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獲取節點,這裏只須要將返回的節點的value返回:
- (id)objectForKey:(id)key {
JKRHashMap_LinkedList_Node *node = [self nodeWithKey:key];
return node ? node.value : nil;
}
複製代碼
上面已經實現了經過key獲取節點,這裏只須要判斷返回的節點是否爲空:
- (BOOL)containsKey:(id)key {
return [self nodeWithKey:key] != nil;
}
複製代碼
只須要返回_size:
- (NSUInteger)count {
return _size;
}
複製代碼
添加鍵值對的步驟:
- (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++;
}
複製代碼
- (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
}
複製代碼
能夠看到,哈希表中的值平均分散在數組中,出現哈希衝突時,以單向鏈表的形式存儲。
既然要作性能的對比測試,首先須要數據,這裏使用蘋果公佈的objc4源代碼,官方下載地址:opensource.apple.com/tarballs/ob…,將其中runtime的源碼文件夾看成資源文件:
讀取其中全部文件的代碼,取出其中的全部單詞,計算不一樣單詞出現的次數。
這個需求恰好能夠利用字典或者咱們自定義的哈希表,將單詞作爲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];
}
}
複製代碼
從新排列節點的邏輯以下:
完整擴容邏輯以下:
- (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開源的哈希表解決方案中,單鏈表長度超過必定值時,將鏈表轉換成紅黑樹的方法解決這個問題,後面也會採用紅黑樹的方式從新實現一遍哈希表。可是在這以前,會先介紹隊列和棧的實現,由於它們這是二叉樹操做的基礎。