跳錶、散列表

跳錶
它是一種各方面性能都比較優秀的動態數據結構,可以支持快速的插入、刪除、查找操作。
對於一個存儲數據有序的單鏈表來說,我們可以通過建立索引提高查找效率,降低查找的時間複雜度。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
這種鏈表加多級索引的結構,就是跳錶。

在跳錶中查詢任意數據的時間複雜度是O(logn)。這個查找的時間複雜度與二分查找是一樣的,換句話說,我們基於單鏈表實現了二分查找。不過,這種查詢效率的提升,前提是建立了很多級索引,也就是用空間換時間的設計思想。
跳錶的空間複雜度是O(n)。

跳錶的插入和刪除操作的時間複雜度也是O(logn)。
在這裏插入圖片描述
在這裏插入圖片描述
在執行刪除操作時,如果這個結點在索引中也有出現,我們除了要刪除原始鏈表中的結點,還要刪除索引中的。

散列表/哈希表
散列表用的是數組支持按照下標隨機訪問數據的特性,所以散列表其實就是數組的一種擴展,由數組演化而來。
學校運動會,選手參賽號問題:
在這裏插入圖片描述
這就是典型的散列思想。
參賽選手的編號叫作鍵(key)或者關鍵字,用它來標識一個選手。
把參賽編號轉化爲數組下表的映射方法叫作散列函數或者哈希函數。
散列函數計算得到的值就叫做散列值或者哈希值。
規律:散列表用的就是數組支持按照下標隨機訪問的時候,時間複雜度是O(1)的特性。

  1. 我們通過散列函數把元素的鍵值映射爲下標,然後將數據存儲在數組中對應下標的位置。
  2. 當我們按照鍵值查詢元素時,我們用同樣的散列函數,將鍵值轉化爲數組下標,從對應的數組下標的位置取數據。

散列函數
我們把它定義成hash(key),其中key表示元素的鍵值,hash(key)的值表示經過散列函數計算得到的散列值。

散列函數設計的基本要求:

  1. 散列函數計算得到的散列值是一個非負整數;因爲數組下標是從0開始的,所以散列函數生成的散列值也要是非負整數。
  2. 如果key1 = key2,那hash(key1) == hash(key2);相同的key經過散列函數得到的散列值也應該是相同的。
  3. 如果key1 != key2,那hash(key1) != hash(key2)。這個要求看起來合情合理,但是在真實的情況下,要想找到一個不同的key對應的散列值都不一樣的散列函數,幾乎是不可能的。所以,實際情況中無法避免散列衝突的發生。

散列衝突解決方法—開放尋址法和鏈表法

開放尋址法:當我們往散列表中插入數據時,如果某個數據經過散列函數散列之後,存儲位置已經被佔用了,我們就從當前位置開始,依次往後查找,看是否有空閒位置,直到找到爲止。
圖中黃色色塊表示空閒位置,橙色色塊表示已經存儲了數據。
圖中黃色色塊表示空閒位置,橙色色塊表示已經存儲了數據。

鏈表法:(相比於開放尋址法,它要簡單很多)如下圖所示,在散列表中,每個「桶(bucket)」或者「槽(slot)」會對應一條鏈表,所有散列值相同的元素都會放到相同的槽位對應的鏈表中。
在這裏插入圖片描述
當插入的時候,我們只需要通過散列函數計算出對應的散列槽位,將其插入到對應鏈表中即可,所以插入的時間複雜度是O(1)。
當查找、刪除一個元素時,我們同樣通過散列函數計算出對應的槽,然後遍歷鏈表查找或者刪除。這兩個操作的時間複雜度與鏈表的長度k成正比,也就是O(k)。對於散列比較均勻的散列函數來說,k=n/m,其中n表示散列中數據的個數,m表示散列表中「槽」的個數。

如何設計散列函數
散列函數設計的好壞,決定了散列表衝突的概率大小,也決定了散列表的性能。
首先,散列表的設計不能太複雜。
其次,散列表函數生成的值要儘可能隨機並且均勻分配。

散列表的裝載因子=填入表中的元素 / 散列表的長度
裝載因子越大,說明空閒位置越小,衝突越大,散列表的性能會下降。
**插入一個數據,最好情況下,不需要擴容,最好的時間複雜度是O(1)。最壞情況下,散列表裝載因子過高,啓動擴容,就需要重新申請內存空間,重新計算哈希位置,並且搬移數據,所以時間複雜度是O(n)。

總結: 當數據量比較小、裝載因子小的時候,適合採用開放尋址法。 基於鏈表的散列衝突處理方法比較適合存儲大對象、大數據量的散列表,而且,比起開放尋址,它更加靈活,支持更多的優化策略。