先看一下hash表的結構圖:前端
數組 + 鏈表java
哈希表(Hash table,也叫散列表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表node
白話一點的說就是經過把Key經過一個固定的算法函數(hash函數)轉換成一個整型數字,而後就對該數字對數組的長度進行取餘,取餘結果就看成數組的下標,將value
存儲在以該數字爲下標的數組空間裏。git
當使用hash表查詢時,就是使用hash函數將key
轉換成對應的數組下標,並定位到該下標的數組空間裏獲取value,這樣就充分利用到數組的定位性能進行數據定位。github
(上面的話,需細細品味)算法
先了解一下下面幾個常說的幾個關鍵字是什麼:後端
key:咱們輸入待查找的值數組
value:咱們想要獲取的內容bash
hash值:key經過hash函數算出的值(對數組長度取模,即可獲得數組下標)數據結構
hash函數(散列函數):存在一種函數F,根據這個函數和查找關鍵字key
,能夠直接肯定查找值所在位置,而不須要一個個遍歷比較。這樣就預先知道key
在的位置,直接找到數據,提高效率。
即
地址index=F(key)
hash函數就是根據key計算出該存儲地址的位置,hash表就是基於hash函數創建的一種查找表。
方法有不少種,好比直接定址法、數字分析法、平方取中法、摺疊法、隨機數法、除留餘數法等,網上相關介紹有不少,這裏就不重點說這個了
對不一樣的關鍵字可能獲得同一散列地址,即k1≠k2,而f(k1)=f(k2),或f(k1) MOD 容量 =f(k2) MOD 容量,這種現象稱爲碰撞,亦稱衝突。
經過構造性能良好的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的取法的不一樣,有一下三種場景:
線性探測法: 當發生衝突時,由於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表性能的重要參數。
容量: 表示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;
}
}
複製代碼
構建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表,若是有什麼錯誤,但願小夥伴們留言告知,謝謝。