【问题标题】:All substrings that are sequences of characters using functional programming使用函数式编程作为字符序列的所有子字符串
【发布时间】:2012-12-01 13:03:24
【问题描述】:

作为my earlier question 在字符串中查找相同字符的运行的后续行动,我还想找到一种函数算法来查找长度大于 2 的所有子字符串,这些子字符串是字母或数字的升序或降序序列(例如:“defgh”、“34567”、“XYZ”、“fedcba”、“NMLK”、9876”等)在字符串([Char])中。我正在考虑的唯一序列是子字符串A..Z、a..z、0..9 及其降序对应项。返回值应该是(从零开始的偏移量、长度)对的列表。我正在从 JavaScript 翻译“zxcvbn”密码强度算法(包含命令式代码)转换为 Scala。出于以函数式编程风格编写的所有常见原因,我希望使我的代码尽可能纯函数式。

我的代码是用 Scala 编写的,但我可能可以用 Clojure、F#、Haskell 或伪代码中的任何一种来翻译算法。

示例:对于字符串 qweABCD13987 将返回 [(3,4),(9,3)]。

我写了一个相当可怕的函数,当我再次可以访问我的工作计算机时,我将发布它,但我确信存在一个更优雅的解决方案。

再次感谢。

【问题讨论】:

    标签: functional-programming sequences


    【解决方案1】:

    我想这个问题的一个很好的解决方案确实比最初看起来更复杂。 我不是 Scala Pro,所以我的解决方案肯定不是最佳和好的,但也许它会给你一些想法。

    基本思想是计算两个连续字符之间的差异,然后不幸的是它变得有点混乱。部分代码不清楚的问我!

    object Sequences {
    
      val s = "qweABCD13987"                         
    
      val pairs = (s zip s.tail) toList   // if s might be empty, add a check here            
      // = List((q,w), (w,e), (e,A), (A,B), (B,C), (C,D), (D,1), (1,3), (3,9), (9,8), (8,7))
    
      // assuming all characters are either letters or digits
      val diff = pairs map {case (t1, t2) =>
        if (t1.isLetter ^ t2.isLetter) 0 else t1 - t2}   // xor could also be replaced by !=
      // = List(-6, 18, 36, -1, -1, -1, 19, -2, -6, 1, 1)
    
      /**
       *
       * @param xs A list indicating the differences between consecutive characters
       * @param current triple: (start index of the current sequence;
       *                         number of current elements in the sequence;
       *                         number indicating the direction i.e. -1 = downwards, 1 = upwards, 0 = doesn't matter)
       * @return A list of triples similar to the argument
       */
      def sequences(xs: Seq[Int], current: (Int, Int, Int) = (0, 1, 0)): List[(Int, Int, Int)] = xs match {
        case Nil => current :: Nil
        case (1 :: ys) =>
          if (current._3 != -1)
            sequences(ys, (current._1, current._2 + 1, 1))
          else
            current :: sequences(ys, (current._1 + current._2 - 1, 2, 1))  // "recompute" the current index
        case (-1 :: ys) =>
          if (current._3 != 1)
            sequences(ys, (current._1, current._2 + 1, -1))
          else
            current :: sequences(ys, (current._1 + current._2 - 1, 2, -1))
        case (_ :: ys) =>
          current :: sequences(ys, (current._1 + current._2, 1, 0))
      }                                               
    
      sequences(diff) filter (_._2 > 1) map (t => (t._1, t._2))
    }
    

    【讨论】:

    • 算法需要稍作改动以只允许字母和数字序列:在diff 方法中,如果 t._1 或 t_.2 不是字母或数字,则返回 0。
    • 确实如此,感谢您的评论。我相应地调整了方法!
    【解决方案2】:

    最好将一个问题拆分为几个较小的子问题。我用 Haskell 写了一个解决方案,这对我来说更容易。它使用惰性列表,但我想您可以使用流或通过使主函数尾递归并将中间结果作为参数传递来将其转换为 Scala。

    -- Mark all subsequences whose adjacent elements satisfy
    -- the given predicate. Includes subsequences of length 1.
    sequences :: (Eq a) => (a -> a -> Bool) -> [a] -> [(Int,Int)]
    sequences p [] = []
    sequences p (x:xs) = seq x xs 0 0
      where
        -- arguments: previous char, current tail sequence, 
        -- last asc. start offset of a valid subsequence, current offset
        seq _ [] lastOffs curOffs = [(lastOffs, curOffs - lastOffs)]
        seq x (x':xs) lastOffs curOffs
            | p x x'    -- predicate matches - we're extending current subsequence
                = seq x' xs lastOffs curOffs'
            | otherwise -- output the currently marked subsequence and start a new one
                = (lastOffs, curOffs - lastOffs) : seq x' xs curOffs curOffs'
          where
            curOffs' = curOffs + 1
    
    -- Marks ascending subsequences.
    asc :: (Enum a, Eq a) => [a] -> [(Int,Int)]
    asc = sequences (\x y -> succ x == y)
    
    -- Marks descending subsequences.
    desc :: (Enum a, Eq a) => [a] -> [(Int,Int)]
    desc = sequences (\x y -> pred x == y)
    
    -- Returns True for subsequences of length at least 2.
    validRange :: (Int, Int) -> Bool
    validRange (offs, len) = len >= 2
    
    -- Find all both ascending and descending subsequences of the
    -- proper length.
    combined :: (Enum a, Eq a) => [a] -> [(Int,Int)]
    combined xs = filter validRange (asc xs) ++ filter validRange (desc xs)
    
    -- test:
    main = print $ combined "qweABCD13987"
    

    【讨论】:

    • 算法需要稍作改动以仅允许字母和数字序列:在 asc 和 desc 函数中,如果任一参数不是字母或数字,则返回 False。
    【解决方案3】:

    这是我在 Clojure 中的近似值:

    我们可以转换输入字符串,以便应用您的previous algorithm 来找到解决方案。算法不会是最高性能的,但我认为您将拥有更抽象和可读的代码。

    示例字符串可以通过以下方式进行转换:

    user => (find-serials "qweABCD13987")
    (0 1 2 # # # # 7 8 # # #)
    

    重用previous function "find-runs":

    user => (find-runs (find-serials "qweABCD13987"))
    ([3 4] [9 3])
    

    最终代码如下所示:

    (defn find-runs [s]
      (let [ls (map count (partition-by identity s))]
        (filter #(>= (% 1) 3) 
                (map vector (reductions + 0 ls) ls))))
    
    (def pad "#")
    
    (defn inc-or-dec? [a b] 
      (= (Math/abs (- (int a) (int b))) 1 ))
    
    (defn serial? [a b c] 
      (or (inc-or-dec? a b) (inc-or-dec? b c)))
    
    (defn find-serials [s] 
      (map-indexed (fn [x [a b c]] (if (serial? a b c) pad x)) 
           (partition 3 1 (concat pad s pad))))
    

    find-serials 创建一个 3 单元格滑动窗口并应用 serial? 来检测作为序列开始/中间/结束的单元格。该字符串被方便地填充,因此窗口始终以原始字符为中心。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      • 2015-05-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多