1. 程式人生 > >iOS總結-NSDictionary的底層實現

iOS總結-NSDictionary的底層實現

參考:https://blog.csdn.net/zixiweimi/article/details/56677203
NSDictionary(字典)是使用hash表來實現key和value之間的對映和儲存的。hash函式設計的好壞影響著資料的查詢訪問效率。資料在hash表中分佈的越均勻,其均勻效率越高。在oc中,通常是利用NSString來作為鍵值,其內部使用的hash函式也是通過使用NSString物件作為鍵值來保證資料的各個節點在hash表中均勻分佈。
- (void)setObject:(id)anObject  forKey:(id <NSCodying>)aKey:
key值,必須遵循NSCoding協議。也就是說在NSDictionary內部,會對aKey物件copy一份新的,而anObject物件在其內部是作為強引用retain/strong,所以在MRC中,向該方法傳送訊息之後,我們會向anObect傳送release訊息進行釋放。
- (NSUInteger)hash;
hash方法是用來計算該物件的hash值,最終的hash值決定了該物件的hash表中儲存的位置,同樣,如果想重寫該方法,我們儘量設計一個能讓資料分佈均勻的hash函式。
- (BOOL)isEqual:(id)object;
是為了通過hash值來找到物件在hash表中的位置。

有關雜湊表:http://ios.jobbole.com/87716/
雜湊表概述:
oc中字典NSDictionary底層其實是一個雜湊表,實際上絕大多數語言中字典都是通過雜湊表實現.
雜湊表本質是一個數組,陣列中每一個元素稱為一個箱子(bin),箱子中存放的是鍵值對.

雜湊表儲存過程如下:
1.根據key計算出它的雜湊值h
2.假設箱子的個數為n , 那麼這個鍵值對應該放在第(h % n)個箱子中
3.如果該箱子中已經有了鍵值對,就使用開放定址法或者拉鍊法解決衝突.

在使用拉鍊法解決雜湊衝突時,每個箱子其實是一個連結串列,屬於同一個箱子的所有鍵值對都會排列在連結串列中.
雜湊表還有重要的屬性:負載因子(load factor),它用來衡量雜湊表的空/滿 程度,一定程度上可以提現查詢的效率
負載因子  = 總鍵值對數 /  箱子個數
負載因子越大,意味著雜湊表越滿,越容易導致衝突,效能也就越低.因此,一般來說,當負載因子大於某個常數(可能是1,或者0.75等)時,雜湊表將自動擴容.
雜湊表在自動擴容時,一般會建立兩倍於原來的箱子,因此即使key的雜湊值不變,對箱子個數取餘的結果也會發生變化,因此所有鍵值對的存放位置都有可能發生變化,這叫重雜湊(rehash).
雜湊表的擴容並不總是能夠有效解決負載因子過大的問題,假設所有key的雜湊值都一樣, 那麼即使擴容以後他們的位置也不會變化.雖然負載因子會降低,但實際儲存在每個箱子中的連結串列長度並不會發生變化,因此也就不能提高雜湊表的查詢效能.

雜湊表的兩個問題:
   1. 如果雜湊表中本來箱子就比較多,擴容時需要重新雜湊並移動資料,效能影響較大
   2.如果雜湊函式設計不合理,雜湊表在極端情況下會變成線性表,效能極低.

Java 8 中的雜湊表
 HashMap是基於HashTable的一種資料結構,在普通雜湊表的基礎上,它支援多執行緒操作以及空的key和value.
在HashMap中定義了幾個常量: static final inr DEFAULT_INITIAL_CAPACITY = 1
依次解釋以上常量:
  1.DEFAULT_INITIAL_CAPACITY:初始容量,也就是預設會建立16個箱子,箱子的個數不能太多或太少.如果太少,很容易觸發擴容,如果太多,遍歷雜湊表會比較慢.
 2.MAXIMUM_CAPACITY:雜湊表最大容量,一般情況下只要記憶體夠用,雜湊表不會出現問題
 3.DEFAULT_LOAD_FACTOR:預設的負載因子.因此初始情況下,當鍵值對的數量大於16 * 0.75 = 12 時,就會觸發擴容
4. TREEIFY_THRESHOLD:如果雜湊表函式不合理,即使擴容也無法減少箱子中連結串列的長度,因此java的處理方案是當連結串列太長時,轉換為紅黑樹.這個值表示當某個箱子中,連結串列長度大於8時,有可能會轉化為樹.
5. UNTREEIFY_THRESHOLD:在雜湊表擴容時,如果發現連結串列長度小於6,則會由樹重新退化為連結串列.
6.MIN_TREEIFY_CAPACITY:在轉變樹之前,還會有一次判斷,只要鍵值對數量大於64才會發生轉換.這是為了避免在雜湊表建立初期,多個鍵值對恰好放入同一個連結串列中而導致不必要的轉化.
java 對雜湊表的設計一定程度上避免了不恰當的雜湊函式導致的效能問題,每一個箱子中的連結串列可以與紅黑樹切換

.
Redis
Redis是一個高效的key-value快取系統,也可以理解為基於鍵值對的資料庫.它對雜湊表的設計有非常值得學習的地方.
資料結構
在Redis中,字典是一個dict型別的結構體.
typdef struct dict{
  dictht ht[2];
  long rehashidex;
} dict;
這個dict是用於儲存資料的結構體.定義一個長度為2的陣列,為了解決擴容時速度較慢而引入的.
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long used;
} dictht;
結構體有一個二維陣列table,元素型別是dictEntry,對應著儲存的一個鍵值對:
typedef struct dictEntry{
  void *key;
  union{
      void *val;
uint64_t u64;
   int64_t s64;
  double d;
}v;
struct dictEntry  *next;
} dictEntry;
從next 指標以及二維陣列可以看出,Redis的雜湊表採用拉鍊法解決衝突.
Redis新插入的鍵值對會放在箱子中連結串列的頭部,而不是尾部繼續插入.
好處: 1.找到連結串列尾部的時間複雜度o(n),或者需要使用額外的記憶體地址來儲存連結串列尾部的位置.頭插法可以節省插入耗時.
2.對於一個數據庫系統來說,最新插入的資料往往更有可能頻繁的被獲取.頭插法可以節省查詢耗時.
增量式擴容
所謂增量式擴容是指,當需要重雜湊時,每次只遷移一個箱子裡的連結串列,這樣擴容時不會出現效能的大幅度下降.
為了標記雜湊表正處於擴容階段,我們在dict結構日中使用rehashidx來表示當前正在遷移哪個箱子裡的資料.由於在結構體中實際上有兩個雜湊表,如果新增新的鍵值對時雜湊表正在擴容,我們首先從第一個雜湊表中遷移一個箱子的資料到第二個雜湊表中,然後鍵值對會被插入到第二個雜湊表中.

對比java 和 Redis
Java 長處在於當雜湊函式不合理導致連結串列過長時,會使用紅黑樹來保證插入和查詢的效率.缺點是當雜湊表比較大時,如果擴容會導致瞬時效率降低.
Redis通過增量式擴容解決了這個缺點,同時拉鍊法的實現(放在連結串列頭部)值得我們學習.Redis 還提供了一個經過嚴格測試,表現良好的預設雜湊函式,避免了連結串列過長的問題.
OC的實現和Java比較類似,當我們需要重寫isEqual()方法時,還需要重寫hash方法. 這兩種語言沒有提供一個通用的,預設的雜湊函式,主要考慮到isEqual()方法可能會被重寫,兩個記憶體資料不同的物件可能在語義上被認為是相同的.如果使用預設的雜湊函式就會得到不同的雜湊值,這兩個物件就會同時被新增到NSSet集合中,這可能違揹我們的期望結果.
Redis不支援重寫雜湊方法,它是一個高效的,kei-value儲存系統,它的key並不會是一個物件,而是一個用來唯一確定物件的標記.
有兩個字典,分別存有100條資料和10000條資料,如果用一個不存在的key去查詢資料,在哪個字典中速度更快?

在Redis中,得益於自動擴容和預設雜湊函式,兩者查詢速度一樣快.在Java和OC中,如果雜湊函式不合理,返回值過於集中,會導致大字典更慢.Java由於存在連結串列和紅黑樹互換機制,搜尋時間呈對數級增長,非線性增長.在理想的雜湊函式下,無論字典多大,搜尋速度都是一樣快.