學習軌道
/
Factor
Factor
/
練習
/
燈塔航海日誌
燈塔航海日誌

燈塔航海日誌

學習練習

簡介

雜湊集合是可變的無序集合,每個值最多只儲存一次。查找、插入和刪除的平均時間複雜度都是 O(1)。它們位於hash-sets中,並實作了sets的協定。

雜湊集合字面值

HS{ "NS-1024" "WB-203" } .
! => HS{ "NS-1024" "WB-203" }

HS{ }是空字面值。就像其他 Factor 字面值一樣,它是個共享物件,原始碼中每個HS{ }的參照都指向同一個集合。HS{ } clone每次呼叫都會給你一份全新、獨立的副本。

從序列建立

>hash-set ( seq -- set )    ! build a fresh set from a sequence

當你已經把值放在序列裡時,>hash-set會一步把它們轉換過來,並捨棄重複的值。

USING: hash-sets prettyprint ;

{ "NS-1024" "WB-203" "NS-1024" } >hash-set .
! => HS{ "NS-1024" "WB-203" }

加入與移除

adjoin     ( elt set -- )    ! insert in place; no-op if already present
adjoin-all ( seq set -- )    ! insert every element of seq in place
delete     ( elt set -- )    ! remove in place; no-op if absent

這三個都會就地修改集合,都不會在堆疊上留下任何東西。

USING: hash-sets kernel sets ;

HS{ } clone
"NS-1024" over adjoin
"WB-203"  over adjoin
"NS-1024" over adjoin    ! duplicate — no effect
.                        ! => HS{ "NS-1024" "WB-203" }

adjoin-all是批次形式:它會加入序列中的每個元素,就像adjoin一次加入一個時那樣略過重複項。

USING: hash-sets kernel sets ;

HS{ "NS-1024" } clone
{ "WB-203" "NS-1024" "QR-7" } over adjoin-all
.                        ! => HS{ "QR-7" "NS-1024" "WB-203" }  (order not guaranteed)

查詢集合

in?         ( elt set -- ? )   ! is elt in the set?
null?       ( set -- ? )       ! is the set empty?
cardinality ( set -- n )       ! number of elements
members     ( set -- seq )     ! enumerate as a sequence

in?是集合協定的成員測試。它和member?(來自sequences)不同,後者會對序列做線性掃描。對雜湊集合來說,in?是雜湊查找,平均 O(1),而這正是使用雜湊集合的重點。

null?會回報集合是否不含任何元素,這是直接詢問「這個集合是空的嗎?」的方式,而不必把cardinality拿來和零比較。

: my-log ( -- set ) HS{ "NS-1024" "WB-203" } ;

"NS-1024" my-log in? .   ! => t
"X-99"    my-log in? .   ! => f
my-log null? .           ! => f
HS{ } clone null? .      ! => t
my-log cardinality .     ! => 2

合併集合

union     ( set1 set2 -- set )
intersect ( set1 set2 -- set )
diff      ( set1 set2 -- set )
set-like  ( set exemplar -- set' )

union是「來自任一邊的所有元素」;intersect是「兩邊都有的元素」;diff是「在set1裡但不在set2裡的元素」。每一個都會回傳新的集合,不會修改它的輸入。

set-like會把set強制轉型成exemplar的型別。union/intersect/diff的預設實作會用它讓結果符合接收者的型別,所以<my-set> <hash-set> union會回傳my-set,而不是雜湊集合。

為什麼這很重要

雜湊集合和雜湊表在圖形走訪上是天生的搭檔:雜湊表把每個節點對應到它的相鄰節點,雜湊集合則記錄哪些節點已經走訪過,讓搜尋不會打轉或重複做白工。走訪本身則是一個佇列加上這兩個結構,在你向外掃描的過程中就地修改。

說明

你是 Cape Crozier 燈塔的看守人。你在燈室裡記錄每一艘經過的船隻,每艘船都以專屬的呼號辨識,同時也和沿岸其他燈塔協調訊號中繼。

1. 全新的日誌本

定義empty-log,讓它回傳一個全新的空雜湊集合,用來收集呼號。

empty-log .
! => HS{ }

2. 記錄一筆目擊

定義sight,讓它接受一本日誌本和一個呼號,並就地把這筆目擊記錄下來。不回傳任何值。

empty-log dup "NS-1024" sight .
! => HS{ "NS-1024" }

3. 我們看過這一艘嗎?

定義seen?,讓它接受一本日誌本和一個呼號;如果該呼號已經記錄過就回傳t,否則回傳f。

HS{ "NS-1024" "WB-203" } "NS-1024" seen? .   ! => t
HS{ "NS-1024" "WB-203" } "X-99"    seen? .   ! => f

4. 忘記一筆目擊

定義forget-sighting,讓它接受一本日誌本和一個呼號,並就地從日誌中移除該呼號。不回傳任何值。如果呼號不在日誌裡,就什麼都不做。

HS{ "NS-1024" "WB-203" } clone dup "WB-203" forget-sighting .
! => HS{ "NS-1024" }

5. 有多少艘不同的船隻?

定義unique-count,讓它回傳日誌中不同呼號的數量。

HS{ "NS-1024" "WB-203" "AC-77" } unique-count .   ! => 3
empty-log unique-count .                          ! => 0

6. 可達的燈塔

海岸巡防隊維護一份relay-map:這是一張以燈塔名稱為鍵的雜湊表,每個值是一個陣列,列出該燈塔可以直接中繼到的燈塔。

定義reachable,讓它接受一個start燈塔和一份relay-map,並回傳一個雜湊集合,包含所有能從start透過反覆中繼抵達的燈塔(包括start本身)。

H{
    { "Crozier"  { "Beacon"  "Hadley"  } }
    { "Beacon"   { "Crozier" "Spiral"  } }
    { "Hadley"   { "Crozier"           } }
    { "Spiral"   { "Beacon"  "Outpost" } }
    { "Outpost"  { "Spiral"            } }
    { "Far-Isle" { "Lonely"            } }
    { "Lonely"   { "Far-Isle"          } }
}
"Crozier" swap reachable .
! => HS{ "Crozier" "Beacon" "Hadley" "Spiral" "Outpost" }

Far-Isle與Lonely這對燈塔自成一個連通分量,所以兩者都不會出現在結果中。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Factor Exercism

準備好開始 燈塔航海日誌 了嗎?

註冊 Exercism,透過 47 個概念163 個練習 和真人引導來學習並精通 Factor,全部免費。