---
# System prepended metadata

title: Hash Tables

---

# Hash Tables 雜湊表

### Dictionary

> 一種ADT，每一個元素都有Key，可以依照Key找到對應資料。

* Operations：
    * `INSERT(T,x)` 插入元素
    * `DELETE(T,x)` 刪除元素
    * `SEARCH(T,x)` 搜尋元素
* Example：
    * 編譯器中的symbol table

#
### Hash Tables
> Hash Tables是實作Dictionary的資料結構

* Serach Time
    * `worst case` Θ(n)
    * `expected time` O(1)
    
>使用一個Array來實作，Array index由Key計算而來


#
### Direct-Address Tables

> Key來自於U = {0,1,2,...,m-1}
>m不會太大，不會有兩個相同key的元素
>表示成Array T[0,1,...,m-1]
>solt k直接對應key = k

* 沒有hash function
* 沒有collision
* 所有操作都是O(1)
* Operations：
    * `Direct-Address-Search(T,k)`
    return T[K]
    
    * `Direct-Address-Insert(T,k)`
    T[ key[x] ] <- x
    
    * `Direct-Address-Delete(T,k)`
    T[ key[x] ] <- NIL

![image](https://hackmd.io/_uploads/HyF0nN6fbe.png)

#### 問題
* key universe太大 -> 記憶體不實際
* 浪費空間，實際使用的Key可能很少

#### Hash Table解決方法
* 空間需求降為Θ(|K|)
* 平均搜尋時間 O(1)

#
### Hash Tables

![image](https://hackmd.io/_uploads/S1YIxrpMbl.png)


* `Hash function` *h*(k)
> 把key映射成Array index
* `Load factor(負載因子)` *a = n/m*
> 平均每個slot裡有多少個元素
* `Collision(碰撞)`
> 無法避免，兩種處理方式是重點：
 * Chaining (鏈表法)
 * Open Addressing (開放地址法)

#
### Chaining 鏈表法

* 同一個slot的元素使用`linked list`存
* slot存的是list的`head pointer`
* load factor 可以 >= 1
* 如果slot沒有元素則放`NULL`
* Operations：
    * `Chaining-Hash-Insert(T,x)`
        插入在鏈表的頭部 T[ *h*(key[x]) ]
    
    * `Chaining-Hash-Search(T,x)`
        在list中線性查找x
    
    * `Chaining-Hash-Delete(T,x)`
        從list中移除x
        
![image](https://hackmd.io/_uploads/SkB5HHpfbx.png)

#
### Open Addressing 開放地址法

* 所有的元素都放在table中
* 每個slot只有一個key或NULL
* load factor <= 1
* 插入時：
    * 不斷probe(探測)直到找到空的slot
* 刪除困難：
    * 不能接設為`NIL`
    * 使用特殊標記`Deleted`
    #### Example：
    *h*(3)  = 3 -> T[3] = 3 `放入`
    *h*(13) = 3 -> T[3] 已經有了東西了，probe -> T[4] (空的) = 13 `放入`
    
    要搜尋 **13** `bool Open-Addressing-Hash-Search(k,13)` -> T[3]=3 
    不是**13** 就probe(探測) -> T[4] = 13 `找到` return `True`
    
    如果刪除**3**直接設為`NIL`，要搜尋**13**就會有問題
    `bool Open-Addressing-Hash-Search(k,13)` -> T[3] = `NIL` 
    看到NIL代表不用繼續probe -> return `False`
    所以刪除不能直接把值改成`NIL`，要放入特殊標記`Delete`讓尋找時能繼續probe
    
    插入同理`Open-Addressing-Hash-Insert(k,23)` -> T[3] = `Delete`
    看到是`Delete`就可以直接放入。

### Linear Probing 線性探測

> 給一個雜湊函數 *h'* 探測序列從 *h*(k) 開使 也就是Array T[ *h*(k) ]的位置 
> 接著依序往後一格一格檢查整個表，當走到slot m-1之後，繞回slot 0的位置繼續
> 缺點是只要一段連續被填滿新元素容易碰撞，效能會急遽下降。

### Quadratic Probing 二次探測

> 跟Linear Probing一樣，給定一個雜湊函數後從*h*(k)開始，
> 但是不是一格格往後探測，是依照二次函數跳躍，避免群聚

#### Example：

輔助的hash function：*h'*(k) = k%11
二次探測的 function：*h*(k,i) = (*h'*(k)+i^2)%11
依序插入11, 33, 44的流程如下：
* *h'*(11) = 11%11 = 0 -> i=0 `(0+0^2)%11` = 0 -> T[0]是空的 -> `放入`
* *h'*(33) = 33%11 = 0 -> i=0 `(0+0^2)%11` = 0 -> T[0] = 11 -> `Quadratic Probing`
i=1 `(0+1^2)%11` = 1 -> T[1]是空的 -> `放入`
* *h'*(44) = 44%11 = 0 -> i=0 `(0+0^2)%11` = 0 -> T[0] = 11 -> `Quadratic Probing`
i=1 `(0+1^2)%11` = 1 -> T[1] = 33 -> `Quadratic Probing`
i=2 `(0+2^2)%11` = 4 -> T[4]是空的 -> `放入`

### Double Hashing 雙重雜湊

> 使用兩個輔助雜湊函數來決定探測的路線
> 每個不同的Key幾乎不會走相同的路線
> 公式：`h(k,i) = [h1(k)+i．h2(k)]%m`
> **i** = 0, 1, 2, ..., m-1 ; **m** = table size ; *h2*(k) 與 **m** 互質
 
 #### Eaxmple：
![image](https://hackmd.io/_uploads/BJaB_iAGbl.png)

> 以圖片中舉例，將 `79` `69` `72` `50` 依序插入後，我們要將 `98` 插入時該怎麼做？
> *h1*(98) = `98 % 13` = `7` , *h2*(98) = `1+(98 % 11)` = `11`
> *h*(98,i) = `(7+i．11) % 13`
> **i** = `0` , *h*(98,0) = `7` -> T[7] = `72` -> `Collision`
> **i** = `1` , *h*(98,1) = `5` -> T[5] = `NIL` -> `存入`

> `14` 插入的過程：
> *h*(14,0) = `1` -> T[1] = `79` -> `Collision`
> *h*(14,1) = `5` -> T[5] = `98` -> `Collision`
> *h*(14,2) = `9` -> T[9] = `NIL` -> `存入`
#  
### Good Hash Functions

> 一個好的雜湊函數應該要讓每一個Key有相同的機率被雜湊到任意一個 slot 
> 並且不受到其他key雜湊到哪裡的影響，彼此獨立達到均勻隨機分佈
> 理想上如此但實際上不可能完全做到，我們通常不知道Key是依照什麼機率分布產生的
> Key之間可能不是彼此獨立的

# 
### Keys as Natural Numbers

> 將Key視為自然數，當Key不是數字的時候，必須要將它轉換成自然數
#### Example：
假設Key是字串，就要把字串解釋成某種底數表示法的整數
以字串 `pt` 為例子，ASCII `p` , `t` 分別是 `112` , `116`
那就將 `pt` 視為 (112,116) 將它轉換成128進位(ASCII有128個basic values)
轉換後 `pt` -> `112 * 128^1 + 116` = `14452`
就可以將 `14452` 視為代表 `pt` 自然數的Key
# 
### Hash Functions 雜湊函數

* Division method **除法法**
    $$ h(k) = k\ \% \ m $$ 
    * 一個不太接近2的整數次方的質數，是m的良好選擇
    像是 m = `23` , k = `91` -> *h*(k) = `22`

* Multiplication method **乘法法**
    $$h(k) = \lfloor \ m (\ kA \ \%\  1) \rfloor$$
    * m的選擇不是那麼關鍵，比 `除法法` **慢**
    * 容易實作
#### Example：

* 不碰位元
`A` 是一個介於 0~1之間的小數，`kA % 1` 就是求 `key．A` 後的小數部分
`m` 再乘上求出的小數部分後取 `地板函數` 
以 `m = 16` , `A = 0.618` , `k = 123` 為例
`kA = 123．0.168 = 76.014` , `kA % 1 = 0.014` , `m．kA取地板函數 = 0`
$h(123) = \lfloor 16 \times (76.014\ \%\ 1)  \rfloor = \lfloor 0.224 \rfloor = 0$
Array index = `0`

* 位元處理
![image](https://hackmd.io/_uploads/B1i_oTAMZl.png)



