筆記-資料結構之 Hash(OC的粗略實現)

佐籩發表於2019-01-29

什麼是Hash表

先看一下hash表的結構圖:

筆記-資料結構之 Hash(OC的粗略實現)

陣列 + 連結串列

雜湊表(Hash table,也叫雜湊表),是根據鍵(Key)而直接訪問在記憶體儲存位置的資料結構。也就是說,它通過計算一個關於鍵值的函式,將所需查詢的資料對映到表中一個位置來訪問記錄,這加快了查詢速度。這個對映函式叫做雜湊函式,存放記錄的陣列叫做雜湊表

白話一點的說就是通過把Key通過一個固定的演算法函式(hash函式)轉換成一個整型數字,然後就對該數字對陣列的長度進行取餘,取餘結果就當作陣列的下標,將value儲存在以該數字為下標的陣列空間裡

當使用hash表查詢時,就是使用hash函式將key轉換成對應的陣列下標,並定位到該下標的陣列空間裡獲取value,這樣就充分利用到陣列的定位效能進行資料定位。

(上面的話,需細細品味)

先了解一下下面幾個常說的幾個關鍵字是什麼:

key:我們輸入待查詢的值

value:我們想要獲取的內容

hash值:key通過hash函式算出的值(對陣列長度取模,便可得到陣列下標)

hash函式(雜湊函式):存在一種函式F,根據這個函式和查詢關鍵字key,可以直接確定查詢值所在位置,而不需要一個個遍歷比較。這樣就預先知道key在的位置,直接找到資料,提升效率。

地址index=F(key)
hash函式就是根據key計算出該儲存地址的位置,hash表就是基於hash函式建立的一種查詢表。

Hash函式的構造方法

方法

方法有很多種,比如直接定址法、數字分析法、平方取中法、摺疊法、隨機數法、除留餘數法等,網上相關介紹有很多,這裡就不重點說這個了

hash函式設計的考慮因素

  • 計算hash地址所需時間(沒有必要搞一個很複雜的函式去計算)
  • 關鍵字的長度
  • 表長
  • 關鍵字分佈是否均勻,是否有規律可循
  • 儘量減少衝突

hash衝突

什麼是hash衝突

對不同的關鍵字可能得到同一雜湊地址,即k1≠k2,而f(k1)=f(k2),或f(k1) MOD 容量 =f(k2) MOD 容量,這種現象稱為碰撞,亦稱衝突。

通過構造效能良好的hash函式,可以減少衝突,但一般不可能完全避免衝突,因此解決衝突是hash表的另一個關鍵問題。

建立和查詢hash表都會遇到衝突,兩種情況下解決衝突的方法應該一致

解決hash衝突

  • 開放定址法

這種方法也稱再雜湊法,基本思想是:當關鍵字key的hash地址p=F(key)出現衝突時,以p為基礎,產生另一個hash地址p1,如果p1仍然衝突,再以p為基礎,再產生另一個hash地址p2,。。。知道找出一個不衝突的hash地址pi,然後將元素存入其中。

通用的再雜湊函式的形式:

H = (F(key) + di) MOD m

其中i=1,2,。。。,m-1 為碰撞次數

m為表長。

F(key)為hash函式。

di為增量序列,增量序列的取值方式不同,相應的再雜湊方式也不同。

1) 線性探測再雜湊

di = 1,2,3,。。。,m-1

衝突發生時,順序檢視錶中下一單元,直到找出一個空單元或查遍全表。

2)二次探測再雜湊

di = 1^2, -1^2, 2^2, -2^2,..., k^2, -k^2 (k <= m-1)

發生衝突時,在表的左右進行跳躍式探測,比較靈活。

3)偽隨機數探測再雜湊

di = 偽隨機序列

下面有個網上的示列: 現有一個長度為11的雜湊表,已填有關鍵字分別為17,60,29的三條記錄。其中採用的雜湊函式為f(key)= key MOD 11。現有第四個記錄,關鍵字為38。根據以上雜湊演算法,得出雜湊地址為5,跟關鍵字60的雜湊地址一樣,產生了衝突。根據增量d的取法的不同,有一下三種場景:

筆記-資料結構之 Hash(OC的粗略實現)

線性探測法: 當發生衝突時,因為f(key) + d,所以首先5 + 1 = 6,得到下一個hash地址為6,又衝突,依次類推,最後得到空閒的hash地址是8,然後將資料填入hash地址為8的空閒區域。

二次探測法: 當發生衝突時,因為d = 1^2,所以5 + 1 = 6,得到的下一個hash地址為6,又衝突,因為d = -1^2,所以5 + (-1) = 4,得到下一個hash地址為4,是空閒則將資料填入該區域。

偽隨機數探測法: 隨機數法就是完全根據偽隨機序列來決定的,如果根據一個隨機數種子得到一個偽隨機序列{1,-2,2,。。。,k},那麼首先得到的地址為6,第二個是3,依次類推,空閒則將資料填入。

開放定址法在iOS中的應用還是有很多的,具體可參考筆記-集合NSSet、字典NSDictionary的底層實現原理

  • 鏈地址法(拉鍊法,位桶法)

將產生衝突的關鍵字的資料儲存在衝突hash地址的一個線性連結串列中。實現時,一種策略是雜湊表同一位置的所有衝突結果都是用棧存放的,新元素被插入到表的前端還是後端完全取決於怎樣方便。

筆記-資料結構之 Hash(OC的粗略實現)

負載因子(load factor)

這裡要提到兩個引數:初始容量,載入因子,這兩個引數是影響hash表效能的重要引數。

容量: 表示hash表中陣列的長度,初始容量是建立hash表時的容量。

載入因子: 是hash表在其容量自動增加之前可以達到多滿的一種尺度(儲存元素的個數),它衡量的是一個雜湊表的空間的使用程度。

loadFactor = 載入因子 / 容量

一般情況下,當loadFactor <= 1時,hash表查詢的期望複雜度為O(1).

對使用連結串列法的雜湊表來說,負載因子越大,對空間的利用更充分,然後後果是查詢效率的降低;如果負載因子太小,那麼雜湊表的資料將過於稀疏,對空間造成嚴重浪費。系統預設負載因子為0.75。

擴容

當hash表中元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對陣列進行擴容。而在陣列擴容之後,最消耗效能的點就出現了,原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是擴容

什麼時候進行擴容呢?當表中元素個數超過了容量 * loadFactor時,就會進行陣列擴容。

用OC粗略的實現了一下擴容的:

- (void)resizeOfNewCapacity:(NSInteger)newCapacity {
    NSInteger oldCapacity = _elementArray.count;
    if (oldCapacity == MAX_CAPACITY) {         // 擴容前的陣列大小如果已經達到最大2^30
        _threshold = oldCapacity - 1;       // 修改閾值為int的最大值(2^30 - 1),這樣以後就不會擴容了
        return;
    }
    
    // 初始化一個新的陣列
    NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:newCapacity];
    for (int i = 0; i < newCapacity; i ++) {
        [newArray addObject:@""];
    }
    
    [self transferWithNewTable:newArray];            // 將資料轉移到新的陣列裡
    [_elementArray removeAllObjects];
    [_elementArray addObjectsFromArray:newArray];    // hash表的陣列引用新建的陣列
    _threshold = (NSInteger)_capacity * _loadFactor; // 修改閾值
}

- (void)transferWithNewTable:(NSMutableArray *)array {
    // 遍歷舊陣列,將元素轉移到新陣列中
    for (int i = 0; i < _elementArray.count; i ++) {
        if ([[[_elementArray objectAtIndex:i] class] isEqual:[SingleLinkedNode class]]) {
            SingleLinkedNode *node = _elementArray[i];
            if (node != NULL) {
                do {
                    [self insertElementToArrayWith:array andNode:node];
                    node = node.next;
                } while (node != NULL);
            }
        }
    }
}

- (void)insertElementToArrayWith:(NSMutableArray *)array andNode:(SingleLinkedNode *)node {
    NSInteger index = [node.key integerValue] % _capacity;                      // 計算每個元素在新陣列中的位置
    if (![[[array objectAtIndex:index] class] isEqual:[SingleLinkedNode class]]) {
        [array replaceObjectAtIndex:index withObject:node];
    }else {
        SingleLinkedNode *headNode = [array objectAtIndex:index];
        while (headNode != NULL) {
            headNode = headNode.next;
        }
        // 直接把元素插入
        headNode.next = node;
    }
}
複製程式碼

OC語言的實現

構建HashTable物件

HashTable.h檔案

#import <Foundation/Foundation.h>
@class SingleLinkedNode;

NS_ASSUME_NONNULL_BEGIN

@interface HashTable : NSObject

@property (nonatomic, strong) NSMutableArray *elementArray;
@property (nonatomic, assign) NSInteger capacity;       // 容量  陣列(hash表)長度
@property (nonatomic, assign) NSInteger modCount;       // 計數器,計算put的元素個數(不包括重複的元素)
@property (nonatomic, assign) float threshold;          // 閾值
@property (nonatomic, assign) float loadFactor;         // 載入因子

/**
 初始化Hash表

 @param capacity 陣列的長度
 @return hash表
 */
- (instancetype)initWithCapacity:(NSInteger)capacity;

/**
 插入

 @param newNode 存入的鍵值對newNode
 */
- (void)insertElementByNode:(SingleLinkedNode *)newNode;

/**
 查詢

 @param key key值
 @return 想要獲取的value
 */
- (NSString *)findElementByKey:(NSString *)key;

@end

NS_ASSUME_NONNULL_END
複製程式碼

HashTable.m檔案:

#define MAX_CAPACITY pow(2, 30)

#import "HashTable.h"
#import "SingleLinkedNode.h"

@implementation HashTable

- (instancetype)initWithCapacity:(NSInteger)capacity {
    self = [super init];
    if (self) {
        _capacity = capacity;
        _loadFactor = 0.75;
        _threshold = (NSInteger) _loadFactor * _capacity;
        _modCount = 0;
        // 直接初始化陣列,這裡為了方便理解hash,所以就直接給定capacity,java中預設是16
        _elementArray = [NSMutableArray arrayWithCapacity:capacity];
        for (int i = 0; i < capacity; i ++) {
            [_elementArray addObject:@""];
        }
    }
    return self;
}

- (void)insertElementByNode:(SingleLinkedNode *)newNode {
    if (newNode.key.length == 0) {
        return;
    }
    
    // 判斷是否需要擴容
    if (_threshold < _modCount * _capacity) {
        _capacity *= 2;
        [self resizeOfNewCapacity:_capacity];
    }
    
    // 計算儲存位置
    NSInteger keyValue = [newNode.key integerValue]; // F(x) = x; 得到hash值
    NSInteger index = keyValue % _capacity;         // hash值 MOD 容量 = 陣列下標
    
    newNode.hashValue = keyValue;
    
    
    // 如果插入的區域是空閒的,則直接把資料存入該空間區域
    if (![[[_elementArray objectAtIndex:index] class] isEqual:[SingleLinkedNode class]]) {
        [_elementArray replaceObjectAtIndex:index withObject:newNode];
        _modCount++;
    }else {
        // 發生衝突,通過連結串列法解決衝突
        SingleLinkedNode *headNode = [_elementArray objectAtIndex:index];
        while (headNode != NULL) {
            // 插入的key重複,則覆蓋原來的元素
            if ([headNode.key isEqualToString:newNode.key]) {
                headNode.value = newNode.value;
                return;
            }
            headNode = headNode.next;
        }
        _modCount++;
        // 直接把元素插入
        headNode.next = newNode;
    }
}

- (NSString *)findElementByKey:(NSString *)key {
    if (key.length == 0) {
        return nil;
    }
    
    // 計算儲存位置
    NSInteger keyValue = [key integerValue];
    NSInteger index = keyValue % _capacity;    // hash函式keyValue % _capacity (0~9)
    
    if (index >= _capacity) {
        return nil;
    }
    
    if (![[[_elementArray objectAtIndex:index] class] isEqual:[SingleLinkedNode class]]) {
        return nil;
    }else {
        // 遍歷連結串列,知道找到key值相等的node,然後返回value
        SingleLinkedNode *headNode = [_elementArray objectAtIndex:index];
        while (headNode != NULL) {
            if ([headNode.key isEqualToString:key]) {
                return headNode.value;
            }
            headNode = headNode.next;
        }
        return nil;
    }
}

- (void)resizeOfNewCapacity:(NSInteger)newCapacity {
    NSInteger oldCapacity = _elementArray.count;
    if (oldCapacity == MAX_CAPACITY) {         // 擴容前的陣列大小如果已經達到最大2^30
        _threshold = oldCapacity - 1;       // 修改閾值為int的最大值(2^30 - 1),這樣以後就不會擴容了
        return;
    }
    
    // 初始化一個新的陣列
    NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:newCapacity];
    for (int i = 0; i < newCapacity; i ++) {
        [newArray addObject:@""];
    }
    
    [self transferWithNewTable:newArray];            // 將資料轉移到新的陣列裡
    [_elementArray removeAllObjects];
    [_elementArray addObjectsFromArray:newArray];    // hash表的陣列引用新建的陣列
    _threshold = (NSInteger)_capacity * _loadFactor; // 修改閾值
}

- (void)transferWithNewTable:(NSMutableArray *)array {
    // 遍歷舊陣列,將元素轉移到新陣列中
    for (int i = 0; i < _elementArray.count; i ++) {
        if ([[[_elementArray objectAtIndex:i] class] isEqual:[SingleLinkedNode class]]) {
            SingleLinkedNode *node = _elementArray[i];
            if (node != NULL) {
                do {
                    [self insertElementToArrayWith:array andNode:node];
                    node = node.next;
                } while (node != NULL);
            }
        }
    }
}

- (void)insertElementToArrayWith:(NSMutableArray *)array andNode:(SingleLinkedNode *)node {
//    下面這個方法沒有成功的獲取到新陣列中的位置
//    NSInteger index = [self indexForHashValue:node.hashValue andNewCapacity:array.count];
    NSInteger index = [node.key integerValue] % _capacity;                      // 計算每個元素在新陣列中的位置
    if (![[[array objectAtIndex:index] class] isEqual:[SingleLinkedNode class]]) {
        [array replaceObjectAtIndex:index withObject:node];
    }else {
        SingleLinkedNode *headNode = [array objectAtIndex:index];
        while (headNode != NULL) {
            headNode = headNode.next;
        }
        // 直接把元素插入
        headNode.next = node;
    }
}

- (NSInteger)indexForHashValue:(NSInteger)hash andNewCapacity:(NSInteger)newCapacity {
    return hash & (newCapacity - 1);
}

@end
複製程式碼

SingleLinkedNode.h檔案:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SingleLinkedNode : NSObject <NSCopying>

@property (nonatomic, strong) NSString *key;
@property (nonatomic, strong) NSString *value;
@property (nonatomic, strong) SingleLinkedNode *next;
@property (nonatomic, assign) NSInteger hashValue;

- (instancetype)initWithKey:(NSString *)key value:(NSString *)value;

@end

NS_ASSUME_NONNULL_END
複製程式碼

SingleLinkedNode.m檔案:

#import "SingleLinkedNode.h"

@implementation SingleLinkedNode

- (instancetype)initWithKey:(NSString *)key value:(NSString *)value {
    if (self = [super init]) {
        _key = key;
        _value = value;
    }
    return self;
}

@end
複製程式碼

上面程式碼看起來或許有些亂,感興趣的小夥伴可以在這裡下載demo傳送門

總結: 這篇文章主要是瞭解hash表,以及hash表的實現特性,並且使用OC語言簡單的粗略的實現了Hash表,如果有什麼錯誤,希望小夥伴們留言告知,謝謝。

相關文章