【问题标题】:interleaving two strings, preserving order: functional style交错两个字符串,保持顺序:函数式
【发布时间】:2016-03-31 20:19:34
【问题描述】:

this question,作者提出了一个有趣的编程问题:给定两个字符串,找出保留原始字符串顺序的那些可能的“交错”排列。

我将问题概括为n 字符串,而不是 OP 中的 2,并提出:

-- charCandidate is a function that finds possible character from given strings.
-- input : list of strings
-- output : a list of tuple, whose first value holds a character 
-- and second value holds the rest of strings with that character removed
-- i.e ["ab", "cd"] -> [('a', ["b", "cd"])] ..

charCandidate xs = charCandidate' xs []
charCandidate' :: [String] -> [String] -> [(Char, [String])]
charCandidate' [] _ = []     
charCandidate' ([]:xs) prev = 
    charCandidate' xs prev
charCandidate' (x@(c:rest):xs) prev =
    (c, prev ++ [rest] ++ xs) : charCandidate' xs (x:prev)

interleavings :: [String] -> [String]
interleavings xs = interleavings' xs []    

-- interleavings is a function that repeatedly applies 'charCandidate' function, to consume
-- the tuple and build permutations.
-- stops looping if there is no more tuple from charCandidate.

interleavings' :: [String] -> String -> [String]
interleavings' xs prev = 
    let candidates = charCandidate xs
        in case candidates of
            [] -> [prev]
            _  -> concat . map (\(char, ys) -> interleavings' ys (prev ++ [char])) $ candidates

-- test case
input :: [String]
input = ["ab", "cd"]    
-- interleavings input == ["abcd","acbd","acdb","cabd","cadb","cdab"]

它可以工作,但是我很关心代码:

  1. 太丑了。没有积分!
  2. 显式递归和附加函数参数prev 以保留状态
  3. 使用元组作为中间形式

如何将上述程序重写为更“haskellic”、简洁、易读和更符合“函数式编程”?

【问题讨论】:

  • 我不建议过分追求无点风格。它可能具有教育意义,甚至很有趣,但如果超出某个点,无点编程会使事情的可读性降低,而不是更多......
  • 另外,请避免使用(:) (...) $ ... 之类的东西,转而使用(...):(...)。在上述情况下,您甚至不需要第二组括号...
  • 我没有测试过,但我怀疑你的意思是在你目前有getCandidate的任何地方写charCandidate',并打算在你目前有charCandidate的任何地方写charCandidate'在五行在charCandidate' 的类型声明之后。
  • @DanielWagner 抱歉,我在从编辑器复制粘贴时更改了一些名称,似乎我忘记让我的编辑与名称更改匹配。

标签: haskell functional-programming


【解决方案1】:

我想我会这样写。主要思想是将创建交错视为一个不确定的过程,它选择一个输入字符串来开始交错和递归。

在我们开始之前,拥有一个我用过无数次的实用函数会很有帮助。它提供了一种从列表中选择元素并知道它是哪个元素的便捷方法。这有点像你的charCandidate',只是它一次只对一个列表进行操作(因此适用范围更广)。

zippers :: [a] -> [([a], a, [a])]
zippers = go [] where
    go xs [] = []
    go xs (y:ys) = (xs, y, ys) : go (y:xs) ys

有了这些,使用 list monad 很容易做出一些不确定的选择。从概念上讲,我们的interleavings 函数可能应该有一个类似[NonEmpty a] -> [[a]] 的类型,它承诺每个传入的字符串中至少有一个字符,但是NonEmpty 的语法开销对于像这样的简单练习来说太烦人了,所以我们' 会在违反此前提条件时给出错误的答案。您还可以考虑将其设为辅助函数,并在运行此函数之前从您的顶级函数中过滤掉空列表。

interleavings :: [[a]] -> [[a]]
interleavings [] = [[]]
interleavings xss = do
    (xssL, h:xs, xssR) <- zippers xss
    t <- interleavings ([xs | not (null xs)] ++ xssL ++ xssR)
    return (h:t)

你可以在 ghci 中看到它:

> interleavings ["abc", "123"]
["abc123","ab123c","ab12c3","ab1c23","a123bc","a12bc3","a12b3c","a1bc23","a1b23c","a1b2c3","123abc","12abc3","12ab3c","12a3bc","1abc23","1ab23c","1ab2c3","1a23bc","1a2bc3","1a2b3c"]
> interleavings ["a", "b", "c"]
["abc","acb","bac","bca","cba","cab"]
> permutations "abc" -- just for fun, to compare
["abc","bac","cba","bca","cab","acb"]

【讨论】:

  • [xs | hay xs] 是什么?
  • @thkang 啊,对,所以:[e1 | e2] 是一个列表,如果 e2 计算结果为 True,则包含元素 e1,如果 e2 计算结果为 False,则为空列表.所以它是if hay xs then [xs] else [] 的简写。可爱,不是吗?无论如何,目的是保持我们只将非空列表传递给interleavings的不变量。
  • 谢谢,这是我唯一不能理解的部分——我读过关于拉链的文章,但很高兴在这里看到一个真正的应用程序(我对 haskell 很陌生)。
  • 为了后代:thkang 的早期问题提到了[xs | hay xs]。这是因为最初我定义了hay = not . null;但我认为不值得拆分成自己的函数并内联,所以新的拼写是[xs | not (null xs)]
  • 我想出了一个在处理长列表时似乎要快得多的实现。你能看看有什么建议吗(尤其是让它更漂亮)?
【解决方案2】:

这是迄今为止我提出的最快的实现。它成对地交错列表列表。

interleavings :: [[a]] -> [[a]]
interleavings = foldr (concatMap . interleave2) [[]]

这种可怕的丑陋混乱是我能找到的交错两个列表的最佳方法。它旨在渐近最优(我相信确实如此);它不是很漂亮。可以通过使用专用队列(例如在Data.List 中用于实现inits 的队列)而不是序列来改进常量因素,但我不想包含那么多样板。

{-# LANGUAGE BangPatterns #-}
import Data.Monoid
import Data.Foldable (toList)
import Data.Sequence (Seq, (|>))

interleave2 :: [a] -> [a] -> [[a]]
interleave2 xs ys = interleave2' mempty xs ys []

interleave2' :: Seq a -> [a] -> [a] -> [[a]] -> [[a]]
interleave2' !prefix xs ys rest =
  (toList prefix ++ xs ++ ys)
     : interleave2'' prefix xs ys rest

interleave2'' :: Seq a -> [a] -> [a] -> [[a]] -> [[a]]
interleave2'' !prefix [] _ = id
interleave2'' !prefix _ [] = id
interleave2'' !prefix xs@(x : xs') ys@(y : ys') =
  interleave2' (prefix |> y) xs ys' .
      interleave2'' (prefix |> x) xs' ys

【讨论】:

    【解决方案3】:

    在 interleave2 上使用 foldr

    interleave :: [[a]] -> [[a]]
    interleave = foldr ((concat .) . map . iL2) [[]]  where 
       iL2 [] ys = [ys]
       iL2 xs [] = [xs]
       iL2 (x:xs) (y:ys) = map (x:) (iL2 xs (y:ys)) ++ map (y:) (iL2 (x:xs) ys)
    

    【讨论】:

      【解决方案4】:

      另一种方法是使用 list monad:

      interleavings xs ys = interl xs ys ++ interl ys xs where
        interl [] ys = [ys]
        interl xs [] = [xs]
        interl xs ys = do
          i <- [1..(length xs)]
          let (h, t)  = splitAt i xs
          map (h ++) (interl ys t)
      

      因此,递归部分将在两个列表之间交替,依次从每个列表中取出 1 到 N 个元素,然后生成所有可能的组合。 list monad 的有趣使用。

      编辑:修复了导致重复的错误

      编辑:回答 dfeuer。在注释字段中编写代码变得很棘手。不使用length 的解决方案示例可能类似于:

      interleavings xs ys = interl xs ys ++ interl ys xs where 
        interl [] ys = [ys]
        interl xs [] = [xs]
        interl xs ys = splits xs >>= \(h, t) -> map (h ++) (interl ys t)
      
      splits [] = []
      splits (x:xs) = ([x], xs) : map ((h, t) -> (x:h, t)) (splits xs)
      

      拆分功能感觉有点尴尬。可以将takeWhilebreaksplitAt 结合使用来替换它,但该解决方案最终也有点尴尬。你有什么建议吗?

      (我去掉了 do 符号只是为了让它稍微短一些)

      【讨论】:

      • 你能找到避免length的方法吗?这里真的不需要。
      • 您可以懒惰地产生所有可能的方法来拆分列表,然后从中绘制元素。我尝试了一个版本,但它并没有使它更具可读性,我也不确定它是否会更快。
      • 刚刚意识到这个解决方案与 Daniel Wagner 的解决方案非常相似
      • 另一种选择(我认为更好)是认识到交错可以成对完成。
      • 嗯。请详细说明
      【解决方案5】:

      结合现有答案中的最佳想法并添加一些我自己的想法:

      import Control.Monad
      
      interleave [] ys = return ys
      interleave xs [] = return xs
      interleave (x : xs) (y : ys) =
        fmap (x :) (interleave xs (y : ys)) `mplus` fmap (y :) (interleave (x : xs) ys)
      
      interleavings :: MonadPlus m => [[a]] -> m [a]
      interleavings = foldM interleave []
      

      这不是你能得到的最快的,但在通用和简单方面应该不错。

      【讨论】:

      • 很漂亮,这是我最初的答案,但它有一些性能问题。左嵌套++ 和大量重复的map 应用程序会使这个速度变慢。
      • 确实如此。这种低效率与输出规模相比相形见绌,因此它们不太重要。
      • 稍微编辑了答案,以便interleavings 现在适用于任何MonadPlus。这意味着您只需使用不同的 monad 就可以使其更快!
      • 更好,但嵌套地图仍然是一个问题,您必须在生成任何内容之前到达第一个列表的末尾。我喜欢你的mplus 想法,但我认为这还不够。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-11
      • 2016-11-01
      • 2014-11-09
      • 1970-01-01
      相关资源
      最近更新 更多