在 Haskell 中添加存储为 0 和 1 列表的两个二进制数,而不使用反向

Posted

技术标签:

【中文标题】在 Haskell 中添加存储为 0 和 1 列表的两个二进制数,而不使用反向【英文标题】:Adding two binary numbers stored as lists of 0s and 1s in Haskell, without using reverse 【发布时间】:2017-05-10 02:25:07 【问题描述】:

我试图以一种优雅而富有表现力的方式来解决这个简单的问题。通常,我会从两个列表的末尾开始,添加相应的元素并存储一个进位来计算下一个数字。但是,我正在努力通过递归而不使用 reverse 函数来解决这个问题。

这是我的第一次尝试:

binarySum :: [Int] -> [Int] -> [Int]
binarySum ls ls'
  = let (res, c) = binarySum' ls ls' in c : res
  where
binarySum' [x] [y]
  = let (s, c) = add x y in ([s], c)
binarySum' (x : xs) (y : ys)
  = (s : res, c')
  where
    (res, c) = binarySum' xs ys
    (s, c')  = add' x y c

(add 和 add' 函数执行所需操作的地方)

结果列表似乎是正确的,但顺序相反。我不知道如何继续,因为我选择在与辅助函数中的进位一起返回的对中构建结果(通常我会做类似s : binarySum'... 的事情)。

另外我觉得代码太杂乱,没有应有的优雅。

非常感谢任何帮助!

【问题讨论】:

以书面顺序存储二进制数非常尴尬,因为您需要知道整个列表的长度才能知道第一位的位置值。使用 cons-lists 时,reverse 是表示它们的优雅方式。 Conal Elliott 对二进制加法 here 进行了相当抽象的处理,如果你喜欢这种东西的话。 你对add'的定义是什么?你确定你得到的是(result, carry),而不是相反吗? 如何存储数字不是我的选择(它是在练习中定义的)。不过,感谢您提供有关二进制加法的附加信息。问题确实出在add' 函数中。 【参考方案1】:

您快到了(至少您的解释似乎表明了这一点 - 您的代码依赖于您未包含的 add 函数)。诀窍确实是将进位作为一个单独的数字保留在辅助函数的元组中(我将其命名为binarySum')。您正在使用的不变量是返回的列表与提供的两个列表中较大的一个具有相同的长度(并且是它们总和的第一个数字) - 如果有的话,一个进位是单独保存的。

binarySum :: [Int] -> [Int] -> [Int]
binarySum xs ys
  | length xs < length ys = binarySum (replicate (length ys - length xs) 0 ++ xs) ys
  | length xs > length ys = binarySum xs (replicate (length xs - length ys) 0 ++ ys)
  | otherwise = case binarySum' xs ys of
                    (0, zs) -> zs
                    (1, zs) -> 1:zs
  where
    binarySum' :: [Int] -> [Int] -> (Int, [Int])
    binarySum' [] ys = (0, ys)
    binarySum' xs [] = (0, xs)
    binarySum' (x:xs) (y:ys) = let (c, zs) = binarySum' xs ys
                                   (c', z) = (x + y + c) `divMod` 2
                               in (c', z:zs)

【讨论】:

我认为divMod的输出应该是(z, c') 谢谢!似乎函数的定义方式相同(元组中元素的顺序除外)。但是,错误确实在 add' 函数中。我现在已经更正了代码。 但是,我还必须添加一个函数来为较小的列表添加适当数量的零前缀,以便binarySum 计算正确的东西。 @David 不错。在这里也修复了解决方案。【参考方案2】:

四年太晚了。不过我不在乎:

(+++) :: [Int] -> [Int] -> [Int]
(+++) n1 n2 = f2 $ foldr f1 ([],0) (copulate n1 n2)
  where
    -- immitates `zip`
    -- but it will make sure that both lists are of same length.
    -- if they are not of the same length, then the shorter list
    -- will be adjusted accordingly.
    copulate :: [Int] -> [Int] -> [(Int,Int)]
    copulate n1 n2
      | length n1 == length n2 = zip n1 n2
      | otherwise = 
          let diff = abs (length n1 - length n2)
          in if length n1 > length n2
               then zip n1 (replicate diff 0 ++ n2)
             else zip (replicate diff 0 ++ n1) n2
    
    f1 :: (Int,Int) -> ([Int],Int) -> ([Int],Int)    
    f1 (x,y) (res,z)
      | any (`notElem` [0,1])[x,y] = error "can only process binary bits"
      | otherwise = 
          let (l,rest) = f3 x y z
          in (l : res,rest)  

    --adding the rest if exists any
    f2 :: ([Int],Int) -> [Int]
    f2 (list,0) = list
    f2 (list,1) = [1] ++ list
    
    --doing the addition and calculating the rest
    f3 :: Int -> Int -> Int -> (Int,Int)
    f3 x y z = ((x+y+z) `mod` 2,if x+y+z>=2 then 1 else 0)

foldr 非常适合从右侧穿过而不倒车。此外,整个列表正在处理中,函数在f1 中严格严格,这使得foldr 非常适合此操作。

查看foldr 的累加器。开头是一个元组:([],0)。

元组中的第一个元素将存储结果。 元组中的第二个元素将存储添加每个位的“状态”。如果其值为 1,此“状态”将影响后面的元组对。

现在你可以测试它了:[1,0,1] +++ [1,0,0] 将给出 1001 感谢中缀运算符,如果您愿意,您可以链接更多加法,例如: [1,0,1] +++ [1,0,0] +++ [1,0,0, 0,0,0,0]

尽管答案看起来很长,但它很容易合理化和编写,这使得它在逻辑上并不冗长,而且我发现这个解决方案比递归更容易。

【讨论】:

以上是关于在 Haskell 中添加存储为 0 和 1 列表的两个二进制数,而不使用反向的主要内容,如果未能解决你的问题,请参考以下文章

用 90 行 Haskell 代码实现 2048 游戏

在 Haskell 中即时减少列表

Python 等效于 Haskell 的 [1..](索引列表)

Haskell:如何附加到元组列表列表?

如何在 haskell 中打印列表?

为啥haskell中的递归列表这么慢?