• Tidak ada hasil yang ditemukan

●Section 8 哈希表(Hash Table)

N/A
N/A
Protected

Academic year: 2023

Membagikan "●Section 8 哈希表(Hash Table)"

Copied!
7
0
0

Teks penuh

(1)

●Section 8 哈希表(Hash Table)

又稱為散列表、雜湊表。

在許多牽扯到需要記錄資訊的問題中,我們往往會需要有一張表格來記錄我們要儲存的 資訊,例如說:搜索時的”狀態”是否重複出現過、某個”字串”是否出現過……等等。

假設所有可能狀態所形成的集合為 U,我們所希望的就是能有個一對一的映射 U→T={0, 2, 3….,m-1},以便每次要使用查找一個 U 中狀態時,只需要檢查 T 中相對應的 key 值就可 以了。

舉個例子來說,像在 DP 表格存的狀態 s,其實就是將 U 映射到 T 中,因為 DP 較不重 視到某個狀態時的實際過程,所以往往用幾個數字就能代表某個狀態 s,而這代表狀態的幾 個數字就會被映射到 T 中,每次要處理這個狀態的訊息時,只需要用 key 做代表就好了。

然而像在 BFS 搜索時的時候,狀態是否出現過就顯得很重要,這時所要記錄的 s,並不 是一個「數字」,而是一個「集合」或「字串」等等的東西,所以整體集合元素可能就會大非 常非常多(也就是 U 非常大),因此無法將所有可能狀態都一對一映射到 T 中。

不過如果實際會用到的集合 SU,且 |S| << |U| 時,那麼我們就可以將想辦法用一個 函數 h,使得 h: U -> T。而這樣的將狀態映射的過程我們把它叫作哈希(Hash),而 T 就是 哈希表(Hash Table),h 函數則稱為哈希函數(Hash Function)。而我們為何可以這樣做?

是因為 h 其實並不是一個一對一的函數,而是多對一的,因此我們可以將所有可能的狀態表 示壓縮成 m 種,當然這樣會造成碰撞(collision)的情形,不過因為|S| < m,所以 S 還是 能容納到 T 中,且碰撞次數不會過多,至於碰撞處理我們將到§8-2 再來說。

我們可以先分析我們所需要達到的操作與目的,首先就是要能夠插入(Insert)查找

(Find),而且這兩樣操作不能花太多的時間,事實上平衡的 BST 也能作到同樣的事,但是 Insert 與 Find 都是 O( lgn),而理想的 hash 便是能支持這兩樣操作,且期望複雜度能在 O(1) 達到的一種資料結構。因為 hash 的分析並不容易,需要考慮到機率與期望分析的問題,所 以下討論就比較不去證明其複雜度,有興趣的話可以自己參考相關書籍。

※ 注意:Hash 的目的並不是因為狀態很難表示所以才要使用 Hash,而使因為總狀態元素個 數可能很大,但實際用到的狀態不多才會用到 hash。

§8-1 哈希函數的選取(Hash Function)

A. 對於狀態 s 表示為數值:

通常我們都是直接將其 Mod 一個質數,將 s 直接映射到 0~M-1 裡。而選質數的目的則 是因為質數較能減少發生碰撞的情形。

其實這樣的作法就很理想了,不過你會感覺顯得不夠”亂”些,假如可能集合 S 剛好數 值都很接進,那豈不是有可能會映射到某一塊區域內嗎?因此說我們可以將 s 平方,再取中 間 r 位 bits,這樣作的目的是希望每一個 bit 都能對打亂 hash 有貢獻,”亂”的程度也許會 高一些。不過通常來說是沒什麼必要這樣做就是了。

B. 對於狀態 s 表示不為數值:

而對於處理其他狀態的 Hash Function 作法(字串、集合、矩陣……),通常是先將這個 s 表示成一個數字,再用上面的處理值的方法去處理。而要怎麼樣將這個 s 轉換成一個數值

(2)

14

會比較好呢?

假設是一個字串,我們可以通常有下面幾種處理方式:

a. 滾動 hash(Rolling hash)

其實因為 ASCII Code 會界於 0~127 之間,因此我們可以把它想成一個 128 進位的數(當 然也可以更大),例如字串”abc”=97×1282+98×1281+99×1280 = 1601891。當然 Mod 應該要於過程中去處理,以免發生溢位情形(對其他 hash function 或許沒差,可是 rolling hash 溢位的話就沒辦法使用 rolling 的特性)。

當然了 rolling hash 的功用絕對不只於此,被叫做 rolling,是因為他是一個可以”滾動”

來計算 hash 值的 Function,現在假如有一個字串 s =“abcABC123”。則我們可以利用 rolling hash,從其中一段 H[ a … b ]的推到 H[ a±1 … b+1 ]。

由上圖應該很明顯得看出要如何轉換了,實際做法就不贅述。

因此我們可以知道,如果我們要把總長度為 L 的所有長度為 m 的子字串 hash 起來,只 需要 O( L )的時間,相對於其他 Hash Function 複雜度為 O( Lm )來說快了許多倍。

雖然說因為相近的字串 hash 值有一定的關係,這可能會造成說不是那麼的”亂”,也 就會造成碰撞機率較大,但是我們卻能利用這種”相關性”來減少計算 hash 的時間,在字 串處理問題中能發揮很大的功用。

舉例來說:給你一個長度為 L 的字串 S,與 n 個長度不等的對應字串,問你說 S 中有多 少個子字串與這 n 個對應字串 match,這時候就可以利用 hash 將 n 個對應字串 hash 起來,

然後再對於所有可能長度 S 去做 rolling hash,如此便能迅速的找到 match 的對應字串。當 然以上也可以用其他字串處理方法處裡啦,不過 hash 卻也實為一種好用而方便的做法。

b. 其他實用的 Hash Function

以下列舉一些常用的字串處理 Hash Function,本人較常用的是 ELFhash,看個人取捨 了。(註:為了節省板面所以就把 code 縮在一起了 XD)

// ELF Hash Function 

unsigned int ELFHash(char *str){ 

  unsigned int hash = 0, x = 0; 

   while (*str)  { 

    hash = (hash << 4) + (*str++); 

    if ( x = hash & 0xF0000000 ){ 

      hash ^= (x >> 24); 

      hash &= ~x; 

    } 

  } 

  return (hash & 0x7FFFFFFF); 

// JS Hash Function 

unsigned int JSHash(char *str){ 

  unsigned int hash = 1315423911;  // 1315423911 = 3 * 438474637    while (*str){ 

(3)

    hash ^= ((hash << 5) + (*str++) + (hash >> 2)); 

  } 

  return (hash & 0x7FFFFFFF); 

// BKDR Hash Function 

unsigned int BKDRHash(char *str){ 

  unsigned int seed = 131, hash = 0; // 31 131 1313 13131 131313 etc. 

  while (*str){ 

    hash = hash * seed + (*str++); 

  } 

  return (hash & 0x7FFFFFFF); 

// AP Hash Function 

unsigned int APHash(char *str){ 

  unsigned int hash = 0; 

  for (int i=0 ; *str; i++){ 

    if ( i & 1 ){ 

      hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5))); 

    } 

    else{ 

      hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3)); 

    } 

  } 

  return (hash & 0x7FFFFFFF); 

不過假如 s 不為字串怎麼辦呢?這就自己隨便想了,總之就是先把狀態”數字化”,再 Mod 一個數即可。

§8-2 哈希表的實踐(Implementation)

有了 Hash Function,看似好像已經能夠解決問題了,不過實際上還有一個問題。就是 說即使 hash function 弄得再好,也還是會有機率發生碰撞,也就是 a≠b,但 h(a)=h(b)的 情況發生。這種時候要怎麼辦呢?以下有幾種解決碰撞問題的方法:

A. 無視:

就直接忽略,這樣的好處是連額外比對的時間都不用,絕對只有 O(1)。當然這樣做有極 大風險就是了,所以強烈不建議這樣做。

B. 多次哈希:

因為基於碰撞機率不大,而且 Hash Function 有一點不同就會造成巨大改變,所以我們 可以使用多個 Hash,交互比對鍵值所對應的狀態是否相同。這樣的方法比上面好些,而且也 能夠在 O(1)時間內達成(假設總哈希表數量不多),不過仍然有些風險,但已經能將完全碰 撞(某個狀態在所有哈希中都有其他東西跟他撞在一起)的機率降到非常低。

C. 開散列法:

這個方法也叫拉鏈法(chaining),是目前最常見與實用的做法。也就是說,碰撞在一起 沒關係,重點是碰撞的元素都會在同一格內。一但在某次插入的時候發現某格已佔有元素,

就將其在此格後面用 Linked-list 接一個 node(當然實際只需要將 hash table 陣列開大一 點,將要拉的鏈接在陣列後面並用 index 當 pointer 的指向位址就好了)。這樣做法與前面不 同的是,這樣做有必與狀態卻實的一個一個去比較,看是否在某個 node 地方能 match,所 以實際複雜度會是 O(k),其中k是檢查兩個狀態是否相同的複雜度(例如字串就是長度之類 的…)。

(4)

16

D. 閉散列法:

這個方法也叫開放地址法(open addressing)。簡單來說就是既然你的位子被別人霸佔 了,那你只好也去霸佔別人的位子。也就是說一但某次插入的時候發現某格已佔有元素,就”

照某種規則”去看其他格子,直到某個格子沒有元素。而”某種規則”通常有下面幾種做法:

a. 線性探查(linear probing):

假設有另一個輔助哈希函數(auxiliary hash function)h’:U→{0 , 1 , … , M‐1}。

則哈希函數hi(s)=( h’(s) + i )mod M,其中hi(s)為狀態s插入時第i次序可以插入的 位子。雖然寫很方便,但是線型探查很容易出現群集(clustering),也就是很有可能會有一 塊聚在那裡,隨著時間變長則某塊區域可能會被用很兇,而另一塊卻空空如也的情形,所以 平均查找插入時間也會不斷增加。

b. 二次探查(quadratic probing):

因為線性可能造成的群集問題,所以改採用另一個 hash function: 

hi(s)=( h’(s) + c1i + c2i)mod M,其中c1,c2為輔助常數(≠0)。 

這個的效果就比線性探查好許多,但如果發生碰撞時又會有群集產生,因此同樣在初次 的探查就決定了整個序列的發展。 

c. 雙重散列(double hashing):

為了解決初始位子相同就決定後續發展的弊端,所以使用了兩個輔助哈希函數 h1, h2。 則hi(s)=( h1(s) + ih2(s) )mod M,因為對於兩個狀態碰撞的情況不會發生一碰撞 就永遠碰撞的情形,所以這種方法算是很不錯的了。

除了插入跟查找之外,有時候可能會用到刪除操作,通常都是增加一個標記符號,直到 標記符號到達某個數量時再去刪除(這麼做是為了把每次整個刪除重整時的操作”攤”到之 前的所做的操作中),但其實碰到機會很少,所以就暫且不深論了。

§8-3 小結(Conclusion)

Hash 的應用非常的廣,其實就連線性時間排序,都能夠用上 hash。雖然有 map 或 set 這種可以輔助快速查找的東西,但是其複雜度與常數總是使人不盡滿意,所以能夠靈活自在 的使用 hash 也是很重要的。

(5)

Chapter 5 貪婪演算法(Greedy Method)

●Section 1 貪心法簡介(Introduction to Greedy Mothod)

最佳化問題的演算法通常會有一序列的選擇,要決定怎麼樣的選擇是最佳的。一個最原 始的想法就是暴蒐,對所有可能的選擇序列嘗試過一遍,從中找出最佳的一個,但通常這樣 的程式的執行效率均不佳。而當這個問題具有最佳子結構的特性,我們便可以使用前一節所 提的動態規劃方法,紀錄每一個子問題的答案以避免做到重複的子問題,獲得較好的效率。

但在某些特定的問題裡,我們可以藉由分析問題的一些性質來跳過大部分的子問題不去處 理,即使用貪心法。簡而言之,貪心法在每次作出選擇時做出當下局部看起來最佳的選擇,

而不管其他選擇是否可能在之後有更好的結果,並期望這樣的過程可以獲得整個問題最佳的 答案。

當然,這個過程不一定會獲得最佳的結果,也就是貪心法不一定都能用,所以當使用貪 心法到一個問題上時,必須要去證明(至少要說服你自己吧…)這樣的選擇會是最佳的。而 當我們確定一個貪心的演算法是正確時,以此為基礎去寫出來的程式通常不會太難寫,也通 常有優異的時空效率,這正是貪心法的優點。貪心法的另一種用途是在這個方法不保證答案 的最佳化時,也可以好的效率得到不錯的估計(approximation algorithm)。

●Section 2 經典問題討論(Classic Problem)

問題一《工作排程》

給定 n 個工作,以及每個工作開始時間 Si與結束時間 Ei(此 n 個工作已預先按結束時間大 小順序排序),每次只能執行一個工作,問最多可以執行幾個工作?(執行一個工作就是從 他的開始時間做到他的結束時間)

問題二《工作排程 II》

給定

n

個工作,每個工作有截止時限和超時處罰,每個工作需要一單位時間,最少會得到 多少處罰?

問題三《骨牌遊戲》(NPSC2004)(TIOJ 1432,1465)

把有 n 個數字的序列分成 w 段,使每段的最大值最小。(全部數字<1001)

問題四《分數背包》

n

樣物品,均有其價值與重量,有一個最大載重量 w 的背包去裝這些物品,物品可以切 割(切割後仍有相應成比例的價值),最多可以裝多少價值的東西?

問題五《線段覆蓋》(TIOJ1396, 線段覆蓋)

給定

n

條線段及每條的位置,從其中至少須選幾條才能完整覆蓋一個給定的區間?

問題六《Barn Repair》(USACO section 1.3)

n

個牛欄,某些牛欄有牛,現在要用若干塊連續的木板把有牛的牛欄封起來。而現在只 有

m

塊木板可以用,但每塊長度可以是任意長,試問需要使用的木板總長最少是多少?

(6)

2

問題七《霍夫曼碼》(TIOJ1155, 經濟編碼)

n

個字元及此

n

個字元在文件中出現的頻率,現在要壓縮這個文件,方式是把每個字元 用一串 0 與 1 組成的二元碼代替,但任一代表字母的二元碼,不能是另一個二元碼的前綴

(若 A 是 011,B 是 01,C 是 1,此時 B 是 A 的前綴,但如此便無法分辨 011 究竟是 A 還是 BC),問怎樣的對應方法能使壓縮後檔案長度最短。

(若 A 出現 100 次,B 出現 10 次,C 出現 5 次,D 出現 1 次。則可把 A 對應到 0,B 對 應 10,C 對應 110,D 對應 111。共花 138 bits)

問題八《刪位數》(TIOJ1397, 刪位數的問題)

給定長度為 n 的正整數 A,欲從中刪去 k 位得另一數 B(B 首位數不能是 0),求 B 的最小 值。

問題九《晶片設計》(TIOJ1316, 晶片設計)

一排晶片兩兩希望連成一對(不一定相鄰),但由於空間限制,晶片間的連線最多只能上方 一條、下方一條,即同一鉛直位置上至多兩條線,問最多能連結幾對晶片?

問題十《功夫城堡》(TIOJ1401, 功夫城堡)

在一個 N*N 的棋盤上放置 N 個給定範圍的城堡,是否能使得他們不能互相攻擊?

問題十一《最小覆蓋》(TIOJ1567, 黑色騎士團的飛彈野望)

最少要用多少個半徑為 d 且圓心在 x 軸上的圓覆蓋給定的 n 個點(1≦n≦1000000)?

問題十二《過河拆橋》(TIOJ1057, 過河拆橋)

你有一塊長 M 公尺的輕便可攜式木橋,正準備渡過一條寬 P 公尺的河流。這條河流上總 共有 N 個木樁,這些木樁的排列方式恰好形成一條垂直於河流流向的直線。你是否能夠利 用這塊可攜式木橋達到過河的任務呢?如果可以過河,至少要拆幾次橋呢?

問題十三《馬的遍訪》

在8×8方格的棋盤上,從任意指定方格出發,為馬尋找一條走遍棋盤每一格並且只經過 一次的一條路徑。

問題十三《誰先晚餐》(NPSC2005)(TIOJ1072)

給你烹煮每個人點的餐點所需要的時間,和每個人把他點的餐點吃完所需要的時間。問你 怎麼排才能讓最後一人吃完的時間最小。

問題十四《萬里長城》(TIOJ1441)

給定一條數列,請求出一條子序列滿足:

1. 一個大一個小的順序。

2. 兩端的數都要比相鄰的數還大。

進階一《照亮的山景》(TIOJ1404, 照亮的山景)

給定一條山稜線各轉折點的

x, y

座標,在山稜線上方高度為

t

的地方架設了一條線,線上 有許多燈泡,問至少須點亮幾個燈泡才能照亮整條山稜線?

(7)

●Section 3 延伸算法(Advanced Algorithm)

§3-1 隨機貪心法(randomized greedy)

有時不能保證貪心法可以得到最佳解,則可採用多次不同的貪心策略或是程度來求得最 佳解,此方法即為隨機貪心法。

§3-2 遺傳演算法(Genetic Algorithms , GA)

遺傳演算法通常實現為一種計算機模擬。對於一個最優化問題,一定數量的候選解(稱 為個體)的抽象表示(稱為染色體)的種群向更好的解進化。進化從完全隨機個體的種群開 始,之後一代一代發生。

以上兩種方法都是由貪婪法延伸出來的,不過因為在比賽中不太實用,所以在此不詳加 說明。

Referensi

Dokumen terkait

難,然而要像往年一樣,一眼就可看出答案的試題幾乎沒有。其實,我們永遠無法 預知當年度的試題,所以筆者依然建議同學們的基本觀念應多加強 多項式函數 圖形及多項方程式根與圖形交點的關係、直線參數式即是動點的觀念、正餘弦函數 值比較大小及求值 、數據解讀能力還要再提升 高低溫與溫差問題 ,尤 其作圖能力更不可忽視 雙曲線及其漸近線與拋物線相交情形 。而對於 99