我是Haskell的新手,我正在尝试将输入的字符串转换为小写字母。转换后,我想对来自['a'..'z']的所有小写字母进行计数。
例如:“这是TES3T” 结果:[(t,3),(h,1),(i,2),(s,2),(a,1)(e,1)]
这是我到目前为止所做的:
countL :: [Char] -> Char -> Int
countL s c = length ( [x | x <- s, x == c])
letter_count :: [Char] -> [(Char, Int)]
letter_count s = nub [(c, countL s c) | c <- s]
我发现以下内容将给定的字符串转换为小写:
toLowerString :: [Char] -> [Char]
toLowerString str = [ toLower x | x <- str]
但是我不知道如何在我的“letter_count”函数中使用“toLowerString”并且只读取['a'..'z']。
我完全错了吗?是否有一种简单的方法可以做到这一点?
谢谢!
答案 0 :(得分:3)
计算字母的一个很好的简短解决方案是使用模块group
(link)中的函数Data.List
。它需要一个列表并返回其分组元素的列表。要获得唯一组,只需先对输入字符串进行排序。 E.g:
group (sort "Hello World")
会给你:
[" ","H","W","d","e","lll","oo","r"]
您需要做的就是将每个子字符串转换为其长度和首字母的元组:
map (\s -> (head s, length s))
所以你的letter_count
将是:
letter_count :: [Char] -> [(Char, Int)]
letter_count = map (\s -> (head s, length s)) . group . sort
答案 1 :(得分:1)
您的解决方案中有一些优化空间。
首先,函数letter_count
在O(n ^ 2)时间运行:当计算[(c, countL s c) | c <- s]
的结果时,对于n个字母的每个字符字符串O {n)查找由countL
执行。这不是最佳时间复杂度 - 计算字母可以更快地完成。
如果您只想考虑字母[a,...,z],那么可以使用Data.Array
中的数组在O(n)时间内完成计数(或者,如评论中提到的@dfeuer,在O(n log k)时间内使用Data.IntMap.Strict
。如果你想坚持列表 - 这将导致更简单的代码(特别是初学者需要),你可以实现O(n log(n))通过先排序列表然后计算字母来确定复杂性。
排序后,您确定相同的字母会出现在连续的块中,因此您可以轻松地对它们进行分组并以线性时间计算它们。
第二次优化远没那么重要,但它可以提高代码的清晰度。看看你对toLowerString
:
toLowerString :: [Char] -> [Char]
toLowerString str = [ toLower x | x <- str]
你在片段[ toLower x | x <- str]
中所做的正是map
函数的作用 - 你用一些函数的结果替换某些列表的每个元素(这里:toLower
)。您可以像这样重写此部分:
toLowerString :: [Char] -> [Char]
toLowerString str = map toLower str
此外,现在您可以观察到,可以省略定义中的str
参数 - 代码变得更短:
toLowerString :: [Char] -> [Char]
toLowerString = map toLower
我对您的问题的解决方案如下:
import Data.Char (isLetter, toLower)
import Data.List (sort, group)
countLetters str =
let
filteredStr = filter isLetter str
lowerCaseStr = map toLower filteredStr
sortedStr = sort lowerCaseStr
groupedStr = group sortedStr
in
[(h, length fragment) | fragment@(h:_) <- groupedStr]
main = do
putStrLn "Type the string to count letters in:"
string <- getLine
let result = countLetters string
print result
你可以在这里试试: https://repl.it/repls/GrubbyUnnaturalKitty
如果您想让这个解决方案更短,您可以这样写:
countLetters str =
let
groupedStr = group . sort . map toLower . filter isLetter $ str
in
[(head fragment, length fragment) | fragment <- groupedStr]
使用函数组合运算符.
。棘手的一点可能是$
运算符的使用,它实际上什么也没做 - 它将左边的函数应用于右边给出的参数。它唯一的作用是帮助避免写太多括号 - 没有它,这条线看起来像这样:
groupedStr = (group . sort . map toLower . filter isLetter) str
请注意两种不同的方法来提取字母组的头部,同时还将名称fragment
绑定到整个组:
A。将整个群组命名为fragment
,并在结果元组的定义中使用函数head
:
[(head fragment, length fragment) | fragment <- groupedStr]
B。将整个群组命名为片段,但也使用h
符号将其标题为@
:
[(h, length fragment) | fragment@(h:_) <- groupedStr]
基本上@
允许你执行模式匹配,同时在匹配发生之前引入整个事物的名称。
答案 2 :(得分:1)
Radek提到的阵列解决方案如下所示:
import qualified Data.Array.Unboxed as A
import Data.Char ( isAsciiLower )
countLettersArr :: [Char] -> [(Char, Int)]
countLettersArr cs = filter ((/= 0) . snd) (A.assocs arr)
where
arr :: A.UArray Char Int
arr = A.accumArray (+) 0 ('a', 'z')
[(c, 1 :: Int) | c <- cs, isAsciiLower c]
countLettersArr
将删除任何非小写字母的字符。这可以稍微概括一下。启用{-# LANGUAGE ScopedTypeVariables #-}
,
countIxesArr :: forall c. A.Ix c => (c,c) -> [c] -> [(c, Int)]
countIxesArr (lo,hi) cs = filter ((/= 0) . snd) (A.assocs arr)
where
arr :: A.UArray c Int
arr = A.accumArray (+) 0 (lo, hi)
[(c, 1::Int) | c <- cs, A.inRange (lo,hi) c]
效率稍低的方法是使用IntMap
而不是数组。这有点痛苦,因为IntMap
只接受Int
索引,但它的工作方式几乎相同:fromListWith
与accumArray
非常相似。此版本将记录您抛出的任何字符,小写字母与否。你可以通过在im
的定义中为列表理解添加一个保护来轻松地改变它。
import qualified Data.IntMap.Strict as M
import Data.Char (ord, chr)
import Data.Bifunctor (first)
-- Count how many of each letter appear in a string.
countLettersIM :: [Char] -> [(Char, Int)]
countLettersIM cs = map (first chr) . M.toList $ im
where
im :: M.IntMap Int
im = M.fromListWith (+) [(ord c, 1) | c <- cs]
如果您愿意,也可以将其概括为一个方向:
countEnumsIM :: Enum a => [a] -> [(a, Int)]
countEnumsIM cs = map (first toEnum) . M.toList $ im
where
im = M.fromListWith (+) [(fromEnum c,1) | c <- cs]