[iOS开发]iOS中的Hash
文章目錄
- 前言
- 關聯對象的底層原理
- weak的實現原理
- KVO的實現原理
- iOS App簽名的原理
- 對象引用計數存儲的位置
- Runloop與線程的存儲關系
- NSDictionary的原理
- 哈希表
- 哈希表定義
- 哈希表優缺點
- 哈希查找步驟
- 哈希表的存儲過程
- 哈希表的實現
- 負載因子 = 總鍵值對數/數組的個數
- 哈希沖突的解決方法
- 開散列
- 閉散列
- 再哈希法
- 建立公共溢出區
- 開閉散列二者的比較
- 拉鏈法的優點:
- 拉鏈法的缺點:
- 線性探測法的缺點
- NSDictionary
前言
天天聽安卓的同學整Hash,猛的發現iOS也有很多底層原理也是Hash來實現的,好好學一下。
iOS中也用到了很多Hash表。
關聯對象的底層原理
[iOS開發]Category、Extension和關聯對象
關聯對象采用的是HashMap嵌套HashMap的結構存儲數據的,簡單來說就是根據對象從第一個HashMap中取出存儲所有關聯對象的第二個HashMap,然后根據屬性名從第二個HashMap中取出屬性對應的值和策略。
問題:為什么關聯對象沒有weak屬性?
weak的實現原理
weak底層原理
weak采用的是一個全局的HashMap嵌套數組的結構存儲數據的。銷毀對象(weak指針指向的對象)的時候,根據對象從HashMap中找到存放所有指向該對象的weak指針的數組,然后將數組中的所有元素(weak指針)都置為nil
weak最大的特點就是在對象銷毀的時候,自動置nil,減少訪問野指針的風險,這也是設計weak的初衷。weak指針置nil的基本步驟:
- 對象dealloc的時候,從全局的HashMap中,根據一個唯一代碼對象的值作為key,找到存儲所有指向該對象的weak指針的數組
- 將數組中的所有元素都置nil
蘋果對于weak的實現其實類似于通知的實現,指明誰(weak指針)要監聽誰(賦值對象)什么事件(dealloc操作)執行什么操作(置nil)
KVO的實現原理
GNUstep KVC/KVO探索(二):KVO的內部實現
iOS App簽名的原理
一致性Hash算法 + 非對稱加密算法
iOS App 簽名的原理
對象引用計數存儲的位置
if 對象支持TaggedPointer {return 直接將對象的指針值作為引用計數返回 } else if 設備是64位環境 && Objective-C2.0 {return 對象isa指針的一部分空間(bits_extra_rc) } else {return hash表 }Runloop與線程的存儲關系
線程和Runloop之間是一一對應的關系,其關系是保存在一個全局的Dictionary里。
線程在剛創建是沒有RunLoop,如果我們不主動獲取,那他就一直都不會有。RunLoop的創建是發生在第一次獲取時,RunLoop的銷毀時發生在線程結束時。我們只能在一個線程內部獲取其RunLoop。
NSDictionary的原理
學完Hash之后我們來學習具體的實現
哈希表
前面說了那么多的底層原理都與Hash表有關,我們來詳細學一下哈希表
哈希表定義
哈希表(hash tabl,也叫散列表),是根據鍵(key)直接訪問在內存存儲位置的數據結構。哈希表本質是一個數組,數組中的每一個元素成為一個箱子,箱子中存放的是鍵值對。根據下標index從數組中取value。關鍵是如何獲取index,這就需要一個固定的函數(哈希函數),將key轉換成index。不論哈希函數設計的如何完美,都可能出現不同的key經過hash處理后得到相同的hash值,這時候我們就需要處理哈希沖突。
哈希表優缺點
優點:把數據的存儲和查找消耗的時間大大降低,幾乎可以看成是常數時間;而代價僅僅是消耗比較多的內存。然而在當前可利用內存越來越多的情況下,用空間換時間的做法是值得的。另外,編碼比較容易也是它的特點之一。
缺點:哈希表通常是基于數組,數組創建后難于擴展。也沒有一種簡便的方法可以以任何一種順序來遍歷哈希表。
所以,如果不需要有序遍歷數據,并且可以提前預測數據量的大小,那么哈希表在速度和易用性方面是無與倫比的。
哈希查找步驟
哈希表的存儲過程
哈希表的實現
哈希表的底層實際上是基于數組來存儲的,當插入鍵值對時,并不是直接插入該數組中,而是通過對鍵進行Hash運算得到Hash值,然后和數組容量取模,得到在數組中的位置后再插入。取值時,先對指定的鍵求Hash值,再和容量取模得到底層數組中對應的位置,如果指定的鍵值與存貯的鍵相匹配,則返回該鍵值對,如果不匹配,則表示哈希表中沒有對應的鍵值對。這樣做的好處是在查找、插入、刪除等操作可以做到 O ( 1 ) O(1) O(1),最壞的情況是 O ( n ) O(n) O(n),當然這種是最極端的情況,極少遇到。
負載因子 = 總鍵值對數/數組的個數
負載因子是哈希表的一個重要屬性,用來衡量哈希表的空/滿成都,一定程度也可以體現查詢的效率。負載因子越大,意味著哈希表越滿,越容易導致沖突,性能也就越低。所以當負載因子大于某個常數(一般是0.75)時,哈希表將自動擴容。哈希表擴容是,一般會創建兩倍于原來的數組長度。這個過程被稱為重哈希(rehash)。
哈希表擴容在數組比較多的時候需要重新哈希并移動數據,性能影響較大。
哈希表擴容雖然能夠使負載因子降低,但并不總能有效提高哈希表的查詢性能。比如哈希函數設計的不合理,導致所有的key計算出的哈希值都相同,那么即使擴容他們的位置還是在同一條鏈表上,變成了線性表,性能也極低,查詢時候的時間復雜度就變成了O(n)。
哈希沖突的解決方法
大類分為四種 開散列、閉散列、再哈希法、建立公共溢出區
開散列
開散列也叫鏈地址法,也叫拉鏈法、哈希桶。
簡單來說就是 數組 + 鏈表。將鍵通過hash函數映射為大小為M的數組的下標索引,數組的每一個元素指向一個鏈表,鏈表中的每一個節點都存儲著hash出來的索引值為結點下標的鍵值對。
JAVA 8解決哈希沖突采用的就是拉鏈法。在處理哈希函數設計不合理導致鏈表很長時(鏈表長度超過8切換為紅黑樹,小于6重新退化為鏈表)。將鏈表切換為紅黑樹能夠保證插入和查找的效率,缺點是當哈希表比較大時,哈希表擴容會導致瞬時效率降低。
Redis解決哈希沖突采用的也是拉鏈法。通過增量式擴容解決了java 8中的瞬時擴容導致的瞬時效率降低的缺點,同時拉鏈法的實現方式(新插入的鍵值對放在鏈表頭部)帶來了兩個好處:
一、 頭插法可以節省插入耗時。如果插到尾部,則需要時間復雜度為O(n)的操作找到鏈表的尾部,或者需要額外的內存地址來保存尾部鏈表的位置。
二、頭插法可以節省查找耗時。最新插入的數據往往可能頻繁的被查詢。
閉散列
閉散列也叫開放地址法、線性探測法。
當發生哈希沖突時,且哈希表未滿,可以通過探測的方法吧key存放在沖突位置的下一個位置中。
方法一:線性探測
從發生沖突的位置開始,一次向后探測,直到尋找到下一個空位置為止。當然這種方法比較簡單,也有很大的缺陷,就是容易造成數據的堆積,使得關鍵字需要多次比較,這樣以來就導致搜索效率低
方法二:二次探測
方法三:偽隨機探測
建立一個偽隨機數發生器(如 i = (i + p) % m),生成一個偽隨機序列,并給定一個隨機數做起點,每次加上這個偽隨機數,++就可以了。
再哈希法
再哈希法其實很簡單,就是再使用哈希函數去散列一個輸入的時候,輸出是同一個位置就再次哈希,直至不發生沖突位置
缺點:每次沖突都要重新哈希,計算時間增加。
建立公共溢出區
這種方法的基本思想是:將哈希表分為基本表和溢出表兩部分,凡是和基本表發生沖突的元素,一律填入溢出表。
開閉散列二者的比較
拉鏈法的優點:
與線性探測法相比,拉鏈法有如下幾個優點:
- 處理沖突簡單,且無堆積現象,即非同義詞絕不會發生沖突,因此平均查找長度較短;
- 由于拉鏈法中各鏈表上的結點空間是動態申請的,所以更適合于造表錢無法確定表長的情況
- 線性探測法為減少沖突,要求裝填因子較小,故當結點規模較大時會浪費很多空間。二拉鏈法中可取a>=1,且結點較大時,拉鏈法中增加的指針域可忽略不計,因此節省空間
- 在用拉鏈法構造的散列表中,刪除結點的操作易于實現。只要簡單地刪去鏈表上相應的結點即可。而對開放定址線性探測發構造的散列表,刪除結點不能簡單地將被刪結 點的空間置為空,否則將截斷在它之后填人散列表的同義詞結點的查找路徑。這是因為各種開放定址線性探測發中,空地址單元(即開放地址)都是查找失敗的條件。因此在用開放定址線性探測發處理沖突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。
拉鏈法的缺點:
指針需要額外的空間,故當結點規模較小時,開放定址線性探測法較為節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址線性探測法中的沖突,從而提高平均查找速度。
線性探測法的缺點
NSDictionary
總結
以上是生活随笔為你收集整理的[iOS开发]iOS中的Hash的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 10岁男童高考566分8岁开发操作系统
- 下一篇: 当“互联网+”遇上“新零售”,卖1000