Jetzt ist mir endlich gelungen den Lazy Levenshtein Distance Algorithmus aus dem letzten Post in F# zu implementieren.
// levenshtein.fs
// According to the article, the worst-case complexity is O(|A|*|B|).
// In this case, the array version is much faster, which is probably due to the overhead
// of the use of Lazy and LazyList delayed methods in F#.
// However, if A=B the complexity is now O(|A|) because only the main diagonal is evaluated.
// In this case, lazy version is faster.
//from http://www.haskell.org/haskellwiki/Edit_distance.
// "An entry depends on three neighbours which lie on the diagonal below, the current diagonal and the diagonal above.
// Each diagonal therefore depends on the diagonal below and the diagonal above where a row depends only on the row above"
let inline editDist sa sb =
let min3 x y z = if x < y then x else min y (LazyList.head z)
let lab = List.length sa - List.length sb
let rec mainDiag:_ Lazy =
lazy(oneDiag sa sb (LazyList.delayed (fun () -> LazyList.head uppers.Value))
(LazyList.consDelayed -1 (fun ()-> LazyList.head lowers.Value)))
//upper diagonals
and uppers : _ Lazy = lazy(eachDiag sa sb (LazyList.consDelayed (mainDiag.Value) (fun ()-> uppers.Value)))
//lower diagonals. note swap sb sa !
and lowers : _ Lazy = lazy(eachDiag sb sa (LazyList.consDelayed (mainDiag.Value) (fun ()-> lowers.Value)))
// 'a list -> 'a list -> LazyList<LazyList<int>>
and eachDiag a b diag =
match a, b, diag with
| _, [], _ -> LazyList.empty
| a, (bch :: bs), ( LazyList.Cons (lastDiag, diags)) ->
let nextDiag = LazyList.delayed (fun () -> LazyList.head (LazyList.tail diags))
LazyList.consDelayed (oneDiag a bs nextDiag lastDiag) (fun ()-> (eachDiag a bs diags))
// 'a list -> 'a list -> LazyList<int> -> LazyList<int> -> LazyList<int>
and oneDiag a b diagAbove diagBelow =
// nw - north-west, n - north, w - west.
// 'a list -> 'a list -> int -> LazyList<int> -> LazyList<int> -> LazyList<int>
let rec doDiag a b nw n w =
match a, b with
| [], _ -> LazyList.empty
| _, [] -> LazyList.empty
| (ach :: achs), (bch :: bchs) ->
let me = if ach = bch then nw else 1 + min3 (LazyList.head w) nw n
LazyList.consDelayed me (fun () ->
doDiag achs bchs me // hope these
(LazyList.delayed (fun () -> LazyList.tail n)) // <---
(LazyList.delayed (fun () -> LazyList.tail w))) // <--- not evaluated.
let firstelt = 1 + (LazyList.head diagBelow)
LazyList.consDelayed firstelt (fun ()-> doDiag a b firstelt diagAbove (LazyList.tail diagBelow))
if lab = 0 then mainDiag.Value
else if lab > 0 then (LazyList.toArray lowers.Value).[lab - 1]
else (LazyList.toArray (uppers.Value)).[-1 - lab]
let inline lazyDist (s1:string) (s2:string) =
match s1.Length,s2.Length with
|0, l2 -> l2
|l1, 0 -> l1
| _ ->
let res = editDist (List.ofSeq s1) (List.ofSeq s2) |> LazyList.toArray
res.[res.Length - 1]let inline timeExec f a b s =
let timer = new System.Diagnostics.Stopwatch()
timer.Start()
let res = f a b
timer.Stop()
printfn "%A." s
printfn "distance = %A: Ellapsed Time: %A ticks, %A ms." res timer.ElapsedTicks timer.ElapsedMilliseconds
let str1 = String.replicate 500 "abcd"
let str2 = String.replicate 500 "defg"
let str3 = String.replicate 1000 "a"
let str4 = (String.replicate 20 "aba")+(String.replicate 500 "aa") + "aaa" + (String.replicate 500 "aa") + (String.replicate 20 "aba")
let str5 = (String.replicate 20 "aca")+(String.replicate 500 "aa") + "bbb" + (String.replicate 500 "aa") + (String.replicate 20 "aca")
[0..10] |> List.map (fun _ ->
printfn "-----------------------"
timeExec levenshteinDistanceArray str1 str2 "levenshtein Distance with Array. worst-case."
timeExec lazyDist str1 str2 "lazy levenshtein Distance. worst-case."
timeExec levenshteinDistanceArray str3 str3 "levenshtein Distance with Array. special case of similar strings."
timeExec lazyDist str3 str3 "lazy levenshtein Distance. special case of similar strings."
timeExec levenshteinDistanceArray str4 str5 "levenshtein Distance with Array."
timeExec lazyDist str4 str5 "lazy levenshtein Distance."
)|>ignore
-----------------------
"levenshtein Distance with Array. worst-case.".
distance = 1502: Ellapsed Time: 3858477L ticks, 269L ms,
"lazy levenshtein Distance. worst-case.".
distance = 1502: Ellapsed Time: 147370307L ticks, 10292L ms.
"levenshtein Distance with Array. special case of similar strings."
distance = 0: Ellapsed Time: 1072085L ticks, 74L ms.
"lazy levenshtein Distance. special case of similar strings.".
distance = 0: Ellapsed Time: 115395L ticks, 8L ms.
"levenshtein Distance with Array.".
distance = 43: Ellapsed Time: 4217920L ticks, 294L ms.
"lazy levenshtein Distance.".
distance = 43: Ellapsed Time: 3744706L ticks, 261L ms.
-----------------------
"levenshtein Distance with Array. worst-case.".
distance = 1502: Ellapsed Time: 3896606L ticks, 272L ms.
"lazy levenshtein Distance. worst-case.".
distance = 1502: Ellapsed Time: 146242131L ticks, 10213L ms.
"levenshtein Distance with Array. special case of similar strings."
distance = 0: Ellapsed Time: 1038478L ticks, 72L ms.
"lazy levenshtein Distance. special case of similar strings.".
distance = 0: Ellapsed Time: 10024L ticks, 0L ms.
"levenshtein Distance with Array.".
distance = 43: Ellapsed Time: 4204501L ticks, 293L ms.
"lazy levenshtein Distance.".
distance = 43: Ellapsed Time: 3610026L ticks, 252L ms.
-----------------------
"levenshtein Distance with Array. worst-case.".
distance = 1502: Ellapsed Time: 4039147L ticks, 282L ms.
"lazy levenshtein Distance. worst-case.".
distance = 1502: Ellapsed Time: 145049786L ticks, 10130L ms.
"levenshtein Distance with Array. special case of similar strings."
distance = 0: Ellapsed Time: 1180816L ticks, 82L ms.
"lazy levenshtein Distance. special case of similar strings.".
distance = 0: Ellapsed Time: 27104L ticks, 1L ms.
"levenshtein Distance with Array.".
distance = 43: Ellapsed Time: 4247992L ticks, 296L ms.
"lazy levenshtein Distance.".
distance = 43: Ellapsed Time: 3652880L ticks, 255L ms.Update
Write "min3" and "let me = if ach = bch then nw else 1 + min3 (LazyList.head w) nw n" explicitly
in order to avoid the unnecessary delay.
give an additional performance gain.
// levenshtein.fs
let inline editDist sa sb =
//let min3 x y z = if x < y then x else min y (LazyList.head z)
let lab = List.length sa - List.length sb
let rec mainDiag:_ Lazy =
lazy(oneDiag sa sb (LazyList.delayed (fun () -> LazyList.head uppers.Value))
(LazyList.consDelayed -1 (fun ()-> LazyList.head lowers.Value)))
//upper diagonals
and uppers : _ Lazy = lazy(eachDiag sa sb (LazyList.consDelayed (mainDiag.Value) (fun ()-> uppers.Value)))
//lower diagonals. note swap sb sa !
and lowers : _ Lazy = lazy(eachDiag sb sa (LazyList.consDelayed (mainDiag.Value) (fun ()-> lowers.Value)))
// 'a list -> 'a list -> LazyList<LazyList<int>>
and eachDiag a b diag =
match a, b, diag with
| _, [], _ -> LazyList.empty
| a, (bch :: bs), ( LazyList.Cons (lastDiag, diags)) ->
let nextDiag = LazyList.delayed (fun () -> LazyList.head (LazyList.tail diags))
LazyList.consDelayed (oneDiag a bs nextDiag lastDiag) (fun ()-> (eachDiag a bs diags))
// 'a list -> 'a list -> LazyList<int> -> LazyList<int> -> LazyList<int>
and oneDiag a b diagAbove diagBelow =
// nw - north-west, n - north, w - west.
// 'a list -> 'a list -> int -> LazyList<int> -> LazyList<int> -> LazyList<int>
let rec doDiag a b nw n w =
match a, b with
| [], _ -> LazyList.empty
| _, [] -> LazyList.empty
| (ach :: achs), (bch :: bchs) ->
if (ach = bch) then
//case if ach = bch then nw
LazyList.consDelayed nw (fun ()->
doDiag achs bchs nw
(LazyList.delayed (fun ()-> LazyList.tail n))
(LazyList.delayed (fun ()-> LazyList.tail w)))
// case else 1 + min3 (LazyList.head w) nw n
else if (LazyList.head w) < nw then
// case let min3 x y z = if x < y then x ...
let me = 1 + (LazyList.head w)
LazyList.consDelayed me (fun ()->
doDiag achs bchs me
(LazyList.delayed (fun ()-> LazyList.tail n))
(LazyList.tail w))
// case let min3 x y z = ... else min y (LazyList.head z)
else
let me = 1 + min nw (LazyList.head n)
LazyList.consDelayed me (fun ()->
doDiag achs bchs me
(LazyList.tail n)
(LazyList.tail w))
let firstelt = 1 + (LazyList.head diagBelow)
LazyList.consDelayed firstelt (fun ()-> doDiag a b firstelt diagAbove (LazyList.tail diagBelow))
if lab = 0 then mainDiag.Value
else if lab > 0 then (LazyList.toArray lowers.Value).[lab - 1]
else (LazyList.toArray (uppers.Value)).[-1 - lab]
let inline lazyDist (s1:string) (s2:string) =
match s1.Length,s2.Length with
|0, l2 -> l2
|l1, 0 -> l1
| _ ->
let res = editDist (List.ofSeq s1) (List.ofSeq s2) |> LazyList.toArray
res.[res.Length - 1]