搞iOS的,面試官問Hash幹嗎?緣由遠比我下面要介紹的多

1、瞭解hash的重要性

iOS開發中 隨處可見 Hash 的身影,難道咱們很差奇嗎?html

下圖只是列出了部分知識點Hash在iOS中的應用分析整理) ios

摘自知乎的一句話:git

算法數據結構通訊協議文件系統驅動等,雖然本身不寫那些東西,可是瞭解其原理對於排錯優化本身的代碼有很大幫助,就比如雖然你不設計製造汽車,但若是你瞭解發動機、變速器、安全氣囊等幾項原理,對於你駕車如何省油延長使用壽命保證自身安全有很大好處學而不思則罔、思而不學則殆,開發人員就是個隨波而進的行業,不管什麼時候何地,保持學習的深度和廣度對於自身發展是很重要的,誰都不想60歲退休了還停留在增刪查改的層面。github

1.一、關聯對象的實現原理:

詳細的原理能夠查閱其餘資料,這裏只介紹一下實現中使用的基本數據結構。關聯對象採用的是HashMap嵌套HashMap的結構存儲數據的,簡單來講就是根據對象從第一個HashMap中取出存儲對象全部關聯對象的第二個HashMap,而後根據屬性名從第二個HashMap中取出屬性對應的值和策略。算法

設計關聯對象的初衷是,經過傳入 對象 + 屬性名字 ,就能夠找到屬性值。方案設計實現好後,查找一個對象的關聯對象的基本步驟:swift

  • - 一、 已知條件一:對象 ,所以引出第一個HashMapAssociationsHashMap),用一個能惟一表明對象的值做爲key,用存儲對象的全部關聯對象的結構(名字:值+策略)做爲value
  • - 二、 已知條件二:屬性名字 ,所以引出第二個HashMapObjectAssociationMap),用屬性名字做爲key,用屬性名字對應的結構體(值+策略)做爲value

參考資料:

iOS底層原理總結 - 關聯對象實現原理數組

關聯對象 AssociatedObject 徹底解析安全

1.二、weak的實現原理:

一樣詳細的原理能夠查閱其餘資料,這裏只介紹一下實現中使用的基本數據結構。weak採用的是一個全局的HashMap嵌套數組的結構存儲數據的,銷燬對象(weak指針指向的對象)的時候,根據對象從HashMap中找到存放全部指向該對象的weak指針的數組,而後將數組中的全部元素(weak指針)都置爲nil。bash

weak的最大特色就是在對象銷燬的時候,自動置nil減小訪問野指針的風險,這也是設計weak的初衷。方案設計實現好後,weak指針置nil的基本步驟:數據結構

  • - 一、對象dealloc的時候,從全局的HashMap中,根據一個惟一表明對象的值做爲key,找到存儲全部指向該對象的weak指針的數組

  • - 二、將數組中的全部元素都置爲nil

蘋果對於weak的實現其實相似於通知的實現,指明誰(weak指針)要監聽誰(賦值對象)什麼事件(dealloc操做)執行什麼操做(置nil)。

參考資料:

iOS 底層解析weak的實現原理(包含weak對象的初始化,引用,釋放的分析) weak實現原理

1.三、KVO實現使用的基本數據結構

比較複雜,一個對象能夠被n個對象觀察,一對象的n個屬性又能夠分別被n個對象觀察。

詳細參考: GNUstep KVC/KVO探索(二):KVO的內部實現

1.四、iOS App簽名的原理

一句話一致性哈希算法 + 非對稱加解密算法

詳細參考: iOS App 簽名的原理

1.五、對象的引用計數存儲的位置

具體參考蘋果iOS系統源碼思考:對象的引用計數存儲在哪裏?--從runtime源碼獲得的啓示

if 對象支持TaggedPointer {
	return 直接將對象的指針值做爲引用計數返回
} 
else if 設備是64位環境 && Objective-C2.0 {
	return 對象isa指針的一部分空間(bits_extra_rc)
}
else {
	return hash表
}
複製代碼

1.六、Runloop與線程的存儲關係

線程和 RunLoop 之間是一一(子線程能夠沒有)對應的,其關係是保存在一個全局的 Dictionary 裏。線程剛建立時並無 RunLoop,若是你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。

1.七、NSDictionary的原理:

解釋完Hash表後,下面簡單解釋下

2、哈希表

桶排序

2.一、哈希表定義

哈希表hash table,也叫散列表),是根據鍵(key)直接訪問訪問在內存儲存位置的數據結構。 哈希表本質是一個數組,數組中的每個元素成爲一個箱子,箱子中存放的是鍵值對。根據下標index從數組中取value。關鍵是如何獲取index,這就須要一個固定的函數(哈希函數),將key轉換成index。不論哈希函數設計的如何完美,均可能出現不一樣的key通過hash處理後獲得相同的hash值,這時候就須要處理哈希衝突。

2.二、哈希表優缺點

優勢 :哈希表能夠提供快速的操做。

缺點 :哈希表一般是基於數組的,數組建立後難於擴展。 也沒有一種簡便的方法能夠以任何一種順序〔例如從小到大)遍歷表中的數據項。

綜上,若是不須要有序遍歷數據,井且能夠提早預測數據量的大小。那麼哈希表在速度和易用性方面是無與倫比的。

2.三、哈希查找步驟

  • - 一、使用哈希函數將被查找的鍵映射(轉換)爲數組的索引,理想狀況下(hash函數設計合理)不一樣的鍵映射的數組下標也不一樣,全部的查找時間複雜度爲O(1)。可是實際狀況下不是這樣的,因此哈希查找的第二步就是處理哈希衝突。

  • - 二、處理哈希碰撞衝突。處理方法有不少,好比拉鍊法、線性探測法。

2.四、哈希表存儲過程:

  • - 一、使用hash函數根據key獲得哈希值h

  • - 二、若是箱子的個數爲n,那麼值應該存放在底(h%n)個箱子中。h%n的值範圍爲[0, n-1]。

  • - 三、若是該箱子非空(已經存放了一個值)即不一樣的key獲得了相同的h產生了哈希衝突,此時須要使用拉鍊法或者開放定址線性探測法解決衝突。

hash("張三") = 23;
hash("李四") = 30;
hash("王五") = 23;
複製代碼

2.五、經常使用哈希函數:

哈希查找第一步就是使用哈希函數將鍵映射成索引。這種映射函數就是哈希函數。若是咱們有一個保存0-M數組,那麼咱們就須要一個可以將任意鍵轉換爲該數組範圍內的索引(0~M-1)的哈希函數。哈希函數須要易於計算而且可以均勻分佈全部鍵。好比舉個簡單的例子,使用手機號碼後三位就比前三位做爲key更好,由於前三位手機號碼的重複率很高。再好比使用身份證號碼出生年月位數要比使用前幾位數要更好。

在實際中,咱們的鍵並不都是數字,有多是字符串,還有多是幾個值的組合等,因此咱們須要實現本身的哈希函數。

  • - 一、直接尋址法
  • - 二、數字分析法
  • - 三、平方取中法
  • - 四、摺疊法
  • - 五、隨機數法
  • - 六、除留餘數法

要想設計一個優秀的哈希算法並不容易,根據經驗,總結了須要知足的幾點要求:

  • 從哈希值不能反向推導出原始數據(因此哈希算法也叫單向哈希算法);
  • 對輸入數據很是敏感,哪怕原始數據只修改了一個 Bit,最後獲得的哈希值也大不相同;
  • 散列衝突的機率要很小,對於不一樣的原始數據,哈希值相同的機率很是小;
  • 哈希算法的執行效率要儘可能高效,針對較長的文本,也能快速地計算出哈希值。

2.六、負載因子 = 總鍵值對數/數組的個數

負載因子是哈希表的一個重要屬性,用來衡量哈希表的空/滿程度,必定程度也能夠提現查詢的效率。負載因子越大,意味着哈希表越滿,越容易致使衝突,性能也就越低。因此當負載因子大於某個常數(通常是0.75)時,哈希表將自動擴容。哈希表擴容時,通常會建立兩倍於原來的數組長度。所以即便key的哈希值沒有變化,對數組個數取餘的結果會隨着數組個數的擴容發生變化,所以鍵值對的位置都有可能發生變化,這個過程也成爲重哈希rehash)。

哈希表擴容 在數組比較多的時候,須要從新哈希並移動數據,性能影響較大。

哈希表擴容 雖然可以使負載因子下降,但並不老是能有效提升哈希表的查詢性能。好比哈希函數設計的不合理,致使全部的key計算出的哈希值都相同,那麼即便擴容他們的位置仍是在同一條鏈表上,變成了線性表,性能極低,查詢的時候時間複雜度就變成了O(n)

2.七、哈希衝突的解決方法:

方法一:拉鍊法

簡單來講就是 數組 + 鏈表 。將鍵經過hash函數映射爲大小爲M的數組的下標索引,數組的每個元素指向一個鏈表,鏈表中的每個結點存儲着hash出來的索引值爲結點下標的鍵值對。

Java 8解決哈希衝突採用的就是拉鍊法。在處理哈希函數設計不合理致使鏈表很長時(鏈表長度超過8切換爲紅黑樹,小於6從新退化爲鏈表)。將鏈表切換爲紅黑樹可以保證插入和查找的效率,缺點是當哈希表比較大時,哈希表擴容會致使瞬時效率下降。

Redis解決哈希衝突採用的也是拉鍊法。經過增量式擴容解決了Java 8中的瞬時擴容致使的瞬時效率下降的缺點,同時拉鍊法的實現方式(新插入的鍵值對放在鏈表頭部)帶來了兩個好處:

  • - 1、頭插法能夠節省插入耗時。若是插到尾部,則須要時間複雜度爲O(n)的操做找到鏈表尾部,或者須要額外的內存地址來保存尾部鏈表的位置。
  • - 2、頭插法能夠節省查找耗時。對於一個數據系統來講,最新插入的數據每每可能頻繁的被查詢。

方法二:開放定址線性探測發

使用兩個大小爲N的數組(一個存放keys,另外一個存放values)。使用數組中的空位解決碰撞,當碰撞發生時(即一個鍵的hash值對應數組的下標被兩外一個鍵佔用)直接將下標索引加一(index += 1),這樣會出現三種結果:

  • - 一、未命中(數組下標中的值爲空,沒有佔用)。keys[index] = keyvalues[index] = value
  • - 二、命中(數組下標中的值不爲空,佔用)。keys[index] == keyvalues[index] == value
  • - 三、命中(數組下標中的值不爲空,佔用)。keys[index] != key,繼續index += 1,直到遇到結果1或2中止。

拉鍊法的優勢

開放定址線性探測發相比,拉鍊法有以下幾個優勢:

  • - ①、拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
  • - ②、因爲拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
  • - ③、開放定址線性探測發爲減小衝突,要求裝填因子α較小,故當結點規模較大時會浪費不少空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增長的指針域可忽略不計,所以節省空間;
  • ④、在用拉鍊法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。而對開放定址線性探測發構造的散列表,刪除結點不能簡單地將被刪結 點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。這是由於各類開放定址線性探測發中,空地址單元(即開放地址)都是查找失敗的條件。所以在用開放定址線性探測發處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點。

拉鍊法的缺點

指針須要額外的空間,故當結點規模較小時,開放定址線性探測發較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址線性探測發中的衝突,從而提升平均查找速度。

開放定址線性探測法缺點

  • - 一、容易產生堆積問題;
  • - 二、不適於大規模的數據存儲;
  • - 三、散列函數的設計對衝突會有很大的影響;
  • - 四、插入時可能會出現屢次衝突的現象,刪除的元素是多個衝突元素中的一個,須要對後面的元素做處理,實現較複雜;
  • - 五、結點規模很大時會浪費不少空間;

2.八、Hash表的平均查找長度

Hash表的平均查找長度包括查找成功時的平均查找長度查找失敗時的平均查找長度

查找成功時的平均查找長度=表中每一個元素查找成功時的比較次數之和/表中元素個數;

查找不成功時的平均查找長度至關於在表中查找元素不成功時的平均比較次數,能夠理解爲向表中插入某個元素,該元素在每一個位置都有可能,而後計算出在每一個位置可以插入時須要比較的次數,再除以表長即爲查找不成功時的平均查找長度。

例子:

給定一組數據{32,14,23,01,42,20,45,27,55,24,10,53},假設散列表的長度爲13(最接近n的質數),散列函數爲H(k) = k%13。分別畫出線性探測法拉鍊法 解決衝突時構造的哈希表,並求出在等機率下狀況,這兩種方法查找成功和查找不成功的平均查找長度。

1、拉鍊法

查找成功時的平均查找長度:

ASL = (1*6+2*4+3*1+4*1)/12 = 7/4
複製代碼

查找不成功時的平均查找長度:

ASL = (4+2+2+1+2+1)/13
複製代碼

2、線性探測法

查找成功時查找次數=插入元素時的比較次數,查找成功的平均查找長度

ASL = (1+2+1+4+3+1+1+1+3+9+1+1+3)/12=2.5
複製代碼

查找不成功時的查找次數=第n個位置不成功時的比較次數爲,第n個位置到第1個沒有數據位置的距離:如第0個位置取值爲1,第1個位置取值爲2.查找不成功時的平均查找長度:

ASL = (1+2+3+4+5+6+7+8+9+10+11+12)/ 13 = 91/13
複製代碼

2.九、NSDictionary解釋版本一:是使用NSMapTable實現的,採用拉鍊法解決哈希衝突

typedef struct {
   NSMapTable        *table;
   NSInteger          i;
   struct _NSMapNode *j;
} NSMapEnumerator;
複製代碼

上述結構體描述了遍歷一個NSMapTable時的一個指針對象,其中包含table對象自身的指針,計數值,和節點指針。

typedef struct {
   NSUInteger (*hash)(NSMapTable *table,const void *);
   BOOL (*isEqual)(NSMapTable *table,const void *,const void *);
   void (*retain)(NSMapTable *table,const void *);
   void (*release)(NSMapTable *table,void *);
   NSString  *(*describe)(NSMapTable *table,const void *);
   const void *notAKeyMarker;
} NSMapTableKeyCallBacks;
複製代碼

上述結構體中存放的是幾個函數指針,用於計算keyhash值,判斷key是否相等,retainrelease操做。

typedef struct {
   void       (*retain)(NSMapTable *table,const void *);
   void       (*release)(NSMapTable *table,void *);
   NSString  *(*describe)(NSMapTable *table, const void *);
} NSMapTableValueCallBacks;
複製代碼

上述存放的三個函數指針,定義在對NSMapTable插入一對key-value時,對value對象的操做。

@interface NSMapTable : NSObject {
   NSMapTableKeyCallBacks   *keyCallBacks;
   NSMapTableValueCallBacks *valueCallBacks;
   NSUInteger             count;
   NSUInteger             nBuckets;
   struct _NSMapNode  **buckets;
}
複製代碼

從上面的結構真的能看出NSMapTable是一個 哈希表 + 鏈表 的數據結構嗎?在NSMapTbale中插入或者刪除一個對象的尋找時間 = O(1) + O(m) ,m爲最壞時可能爲n。

O(1) :對key進行hash獲得bucket的位置

O(m) :不一樣的key獲得相同的hash值的value存放到鏈表中,遍歷鏈表的時間

上面的結論和對應的解釋彷佛很合理?看看下面的分析再說也不遲!

2.十、NSDictionary解釋版本二:是對_CFDictionary的封裝,解決哈希衝突使用的是開放定址線性探測法

struct __CFDictionary {
    CFRuntimeBase _base;
    CFIndex _count;
    CFIndex _capacity;
    CFIndex _bucketsNum;
    uintptr_t _marker;
    void *_context;
    CFIndex _deletes;
    CFOptionFlags _xflags;
    const void **_keys;	
    const void **_values;
};
複製代碼

從上面的數據結構能夠看出NSDictionary內部使用了兩個指針數組分別保存keysvalues。採用的是連續方式存儲鍵值對。拉鍊法會將keyvalue包裝成一個結果存儲(鏈表結點),而Dictionary的結構擁有keysvalues兩個數組(開放尋址線性探測法解決哈希衝突的用的就是兩個數組),說明兩個數據是被分開存儲的,不像是拉鍊法

NSDictionary使用開放定址線性探測法解決哈希衝突的原理:

能夠看到,NSDictionary設置的keyvaluekey值會根據特定的hash函數算出創建的空桶數組,keysvalues一樣多,而後存儲數據的時候,根據hash函數算出來的值,找到對應的index下標,若是下標已有數據,開放定址法後移動插入,若是空桶數組到達數據閥值,這個時候就會把空桶數組擴容,而後從新哈希插入。

這樣把一些不連續的key-value值插入到了能創建起關係的hash表中,當咱們查找的時候,key根據哈希>值算出來,而後根據索引,直接index訪問hashkeyshashvalues,這樣查詢速度就能夠和連續線性存儲的數據同樣接近O(1)了,只是佔用空間有點大,性能就很強悍。

若是刪除的時候,也會根據_maker標記邏輯上的刪除,除非NSDictionaryNSDictionary本體的hash值就是count)內存被移除。

NSDictionary之因此採用這種設計, 其一出於查詢性能的考慮; 其二NSDictionary在使用過程當中老是會很快的被釋放,不會長期佔用內存;

2.十一、Apple方案選擇:

解決哈希衝突的拉鍊法和開放定址線性探測法,Apple都是用了。具體使用哪種是根據存儲數據的生命週期和特性決定的。

  • @synchronized使用的是拉鍊法。拉鍊法多用於存儲的數據是通用類型,可以被反覆利用,就像@synchronized存儲的是鎖是一種無關業務的實現結構,程序運行時多個對象使用同一個鎖的機率至關高,有效的節省了內存。
  • weak對象 associatedObject採用的是開放定址線性探測法。開放定址線性探測法用於存儲的數據是臨時的,用完儘快釋放,就像associatedObject,weak。

2.十二、NSDictionary的存儲過程:

具體參考我寫的另外一篇博客:iOS筆記:進一步認識 ==、isEqual、hash

  • 一、經過方法- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;能夠看出key必須遵循NSCopy協議,也就是說NSDictionary的key是copy一份新的,而value是淺拷貝的(若是想深拷貝可使用NSMapTable)。
  • 二、不過這還不夠,key還必需要繼承NSObject,而且重寫-(NSUInteger)hash;-(BOOL)isEqual:(id)object;兩個方法。第一個函數用於計算hash值,第二個函數用來判斷當哈希值相同的時候value是否相同(解決哈希衝突)。

參考博客

淺談算法和數據結構: 十一 哈希表

NSDictionary和NSMutableArray底層原理(哈希表和環形緩衝區)

Swift中字典的實現原理

深刻理解哈希表

解讀Objective-C的NSDictionary

iOS重作輪子,寫一個NSDictionary(一)

iOS重作輪子,寫一個NSDictionary(一)

哈希表——線性探測法、鏈地址法、查找成功、查找不成功的平均長度

哈希表、哈希算法、一致性哈希表

細說@synchronized和dispatch_once

相關文章
相關標籤/搜索