函数式编程中的滑动窗口匹配

时间:2014-10-17 11:35:47

标签: functional-programming erlang

我试图实现一个滑动窗口算法来匹配文本文件中的单词。我来自程序背景,我在Erlang这样的函数式语言中首次尝试这样做似乎需要时间O(n ^ 2)(甚至更多)。如何用函数式语言做到这一点?

-module(test).
-export([readText/1,patternCount/2,main/0]).

readText(FileName) ->
    {ok,File} = file:read_file(FileName),
    unicode:characters_to_list(File).

patternCount(Text,Pattern) ->
    patternCount_(Text,Pattern,string:len(Pattern),0).

patternCount_(Text,Pattern,PatternLength,Count) ->
    case string:len(Text) < PatternLength of 
        true -> Count;
        false ->
            case string:equal(string:substr(Text,1,PatternLength),Pattern) of
                true ->
                    patternCount_(string:substr(Text,2),Pattern,PatternLength,Count+1);
                false ->
                    patternCount_(string:substr(Text,2),Pattern,PatternLength,Count)
            end
    end.

main() ->
    test:patternCount(test:readText("file.txt"),"hello").

1 个答案:

答案 0 :(得分:5)

您的问题有点过于宽泛,因为它要求在函数式语言中实现此算法,但最好的方法是依赖于语言。因此,我的答案主要针对Erlang,给出了您的示例代码。

首先,请注意,不需要单独的patternCountpatternCount_函数。相反,您可以只使用具有不同arities的多个patternCount函数以及同一arity的多个子句。首先,让我们重写您的函数以将其考虑在内,并使用string:len/1内置函数替换对length/1的调用:

patternCount(Text,Pattern) ->
    patternCount(Text,Pattern,length(Pattern),0).

patternCount(Text,Pattern,PatternLength,Count) ->
    case length(Text) < PatternLength of
        true -> Count;
        false ->
            case string:equal(string:substr(Text,1,PatternLength),Pattern) of
                true ->
                    patternCount(string:substr(Text,2),Pattern,PatternLength,Count+1);
                false ->
                    patternCount(string:substr(Text,2),Pattern,PatternLength,Count)
            end
    end.

接下来,patternCount/4函数中的多级缩进是“代码气味”,表示可以做得更好。让我们将该函数分成多个子句:

patternCount(Text,Pattern,PatternLength,Count) when length(Text) < PatternLength ->
    Count;
patternCount(Text,Pattern,PatternLength,Count) ->
    case string:equal(string:substr(Text,1,PatternLength),Pattern) of
        true ->
            patternCount(string:substr(Text,2),Pattern,PatternLength,Count+1);
        false ->
            patternCount(string:substr(Text,2),Pattern,PatternLength,Count)
    end.

第一个子句使用guard来检测不可能有更多匹配,而第二个子句查找匹配。现在让我们重构第二个子句以使用Erlang的内置匹配。我们希望一次一个元素前进输入文本,就像原始代码一样,但我们也希望在我们这样做时检测匹配。让我们在我们的函数头中执行匹配,如下所示:

patternCount(_Text,[]) -> 0;
patternCount(Text,Pattern) ->
    patternCount(Text,Pattern,Pattern,length(Pattern),0).

patternCount(Text,_Pattern,_Pattern,PatternLength,Count) when length(Text) < PatternLength ->
    Count;
patternCount(Text,[],Pattern,PatternLength,Count) ->
    patternCount(Text,Pattern,Pattern,PatternLength,Count+1);
patternCount([C|TextTail],[C|PatternTail],Pattern,PatternLength,Count) ->
    patternCount(TextTail,PatternTail,Pattern,PatternLength,Count);
patternCount([_|TextTail],_,Pattern,PatternLength,Count) ->
    patternCount(TextTail,Pattern,Pattern,PatternLength,Count).

首先,请注意我们在底部四个子句中添加了一个新参数:我们现在将Pattern作为第二个和第三个参数传递,以允许我们使用其中一个进行匹配,其中一个用于维护原始模式,如下面更全面的解释。另请注意,我们在顶部添加了一个新子句来检查空Pattern,并在这种情况下返回0。

让我们只关注底部的三个patternCount/5子句。这些子句在运行时按顺序尝试,但让我们首先看看这三个子句中的第二个,然后是第三个子句,然后是三个子句中的第一个:

  1. 在这三个子句的第二个中,我们用[Head|Tail]列表符号写出第一个和第二个参数,这意味着Head是列表的第一个元素,{{1}是列表的其余部分。我们对两个列表的头部使用相同的变量,这意味着如果两个列表的第一个元素相等,我们正在进行潜在匹配,因此我们递归调用Tail将列表的尾部传递为前两个论点。传递尾部允许我们一次通过输入文本和模式元素,检查匹配元素。

  2. 在最后一个子句中,前两个参数的头部不匹配;如果他们这样做,运行时将执行第二个子句,而不是这个子句。这意味着我们的模式匹配失败了,因此我们不再关心第一个参数的第一个元素,也不关心第二个参数,我们必须通过输入文本来寻找新的匹配。请注意,我们将输入文本的头部和第二个参数都写为patternCount/5“不关心”变量,因为它们对我们来说不再重要。我们递归调用_,将输入文本的尾部作为第一个参数传递,将完整的patternCount/5作为第二个参数传递,允许我们开始寻找新的匹配。

  3. 在这三个子句的第一个中,第二个参数是空列表,这意味着我们已经通过逐个元素成功匹配完整的Pattern来实现。所以我们递归调用Pattern传递完整的patternCount/5作为第二个参数开始寻找新的匹配,我们也增加了匹配次数。

  4. 试试吧!这是完整的修订模块:

    Pattern

    一些最终建议:

    • 逐个元素地搜索文本比必要的慢。您应该查看Boyer-Moore algorithm和其他相关算法,以了解在更大的块中推进文本的方法。例如,Boyer-Moore首先尝试匹配模式的 end ,因为如果不匹配,它可以通过文本前进到模式的整个长度。

    • 您可能还希望使用Erlang二进制文件而不是列表,因为它们在内存方面更加紧凑且they allow for matching more than just their first elements。例如,如果-module(test). -export([read_text/1,pattern_count/2,main/0]). read_text(FileName) -> {ok,File} = file:read_file(FileName), unicode:characters_to_list(File). pattern_count(_Text,[]) -> 0; pattern_count(Text,Pattern) -> pattern_count(Text,Pattern,Pattern,length(Pattern),0). pattern_count(Text,_Pattern,_Pattern,PatternLength,Count) when length(Text) < PatternLength -> Count; pattern_count(Text,[],Pattern,PatternLength,Count) -> pattern_count(Text,Pattern,Pattern,PatternLength,Count+1); pattern_count([C|TextTail],[C|PatternTail],Pattern,PatternLength,Count) -> pattern_count(TextTail,PatternTail,Pattern,PatternLength,Count); pattern_count([_|TextTail],_,Pattern,PatternLength,Count) -> pattern_count(TextTail,Pattern,Pattern,PatternLength,Count). main() -> pattern_count(read_text("file.txt"),"hello"). 是输入文本作为二进制文件而Text是模式作为二进制文件,并假设Pattern的大小等于或大于{{Text的大小1}},此代码尝试匹配整个模式:

      Pattern

      请注意,此代码段恢复为使用case Text of <<Pattern:PatternLength/binary, TextTail/binary>> = Text -> patternCount(TextTail,Pattern,PatternLength,Count+1); <<_/binary,TextTail/binary>> -> patternCount(TextTail,Pattern,PatLen,Count) end. ,因为我们不再需要额外的patternCount/4参数来逐个元素地工作。

    • 如完整修订模块所示,在同一模块中调用函数时,不需要模块前缀。请参阅简化的Pattern函数。

    • 如完整修订模块所示,传统的Erlang样式不使用main/0等混合大小写函数名称。大多数Erlang程序员会使用patternCount代替。