【问题标题】:Sliding window matching in functional programming函数式编程中的滑动窗口匹配
【发布时间】:2014-10-17 23:26:24
【问题描述】:

我正在尝试实现一个滑动窗口算法来匹配文本文件中的单词。我来自程序背景,我第一次尝试使用像 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").

【问题讨论】:

    标签: functional-programming erlang


    【解决方案1】:

    您的问题有点过于宽泛,因为它询问的是用函数式语言实现此算法,但如何最好地做到这一点取决于语言。因此,鉴于您的示例代码,我的答案集中在 Erlang。

    首先,请注意,不需要有单独的 patternCountpatternCount_ 函数。相反,您可以只拥有多个具有不同元数的 patternCount 函数以及具有相同元数的多个子句。首先,让我们重写您的函数以考虑到这一点,并将对 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.
    

    第一个子句使用一个守卫来检测不可能有更多的匹配,而第二个子句寻找匹配。现在让我们重构第二个子句以使用 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 子句。这些子句在运行时按顺序尝试,但让我们先看这三个子句中的第二个,然后是第三个子句,然后是三个子句中的第一个:

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

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

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

    -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").
    

    一些最终建议:

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

    • 您可能还想研究使用 Erlang 二进制文件而不是列表,因为它们在内存方面更紧凑并且they allow for matching more than just their first elements。例如,如果Text 是二进制输入文本,Pattern 是二进制模式,假设Text 的大小等于或大于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.
      

      请注意,此代码 sn-p 恢复为使用 patternCount/4,因为我们不再需要额外的 Pattern 参数来逐个元素地工作。

    • 如完整修改后的模块所示,在同一个模块中调用函数时,不需要模块前缀。请参阅简化的main/0 函数。

    • 如完整修改后的模块所示,传统的 Erlang 风格不使用混合大小写的函数名称,如 patternCount。大多数 Erlang 程序员会改用pattern_count

    【讨论】:

    • 感谢您的详细解答!我希望我能给你更多的选票。我会一遍又一遍地仔细阅读你所说的。再次感谢您的帮助!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-05-17
    • 2015-04-13
    • 2015-02-28
    • 1970-01-01
    • 2019-01-23
    • 1970-01-01
    相关资源
    最近更新 更多