【问题标题】:How to improve this algorithm by 1)use arrays, 2)avoid list concatenation (lazy lists?)?如何通过 1)使用数组,2)避免列表连接(惰性列表?)来改进此算法?
【发布时间】:2025-12-19 00:30:02
【问题描述】:

我试图了解 STArray 的工作原理,但我学不会。 (Doc很差,或者至少是我找到的那个)。

无论如何,我有下一个算法,但它使用了很多!!,这很慢。如何将其转换为使用 STArray monad?

-- The Algorithm prints the primes present in [1 .. n]

main :: IO ()
main = print $ primesUpTo 100

type Nat = Int

primesUpTo :: Nat -> [Nat]
primesUpTo n = primesUpToAux n 2 [1]

primesUpToAux :: Nat -> Nat -> [Nat] -> [Nat]
primesUpToAux n current primes = 
  if current > n
  then primes
  else primesUpToAux n (current + 1) newAcum
  where newAcum = case isPrime current primes of
                  True  -> primes++[current]
                  False -> primes

isPrime :: Nat -> [Nat] -> Bool
isPrime 1 _ = True
isPrime 2 _ = True
isPrime x neededPrimes = isPrimeAux x neededPrimes 1

isPrimeAux x neededPrimes currentPrimeIndex = 
  if sqrtOfX < currentPrime
  then True
  else if mod x currentPrime == 0
       then False
       else isPrimeAux x neededPrimes (currentPrimeIndex + 1)
  where
        sqrtOfX = sqrtNat x
        currentPrime = neededPrimes !! currentPrimeIndex

sqrtNat :: Nat -> Nat
sqrtNat = floor . sqrt . fromIntegral

编辑

哎呀,!!不是问题;在算法的下一个版本(如下)中,我删除了 !! 的使用;另外,我将 1 固定为素数,正如@pedrorodrigues 所指出的那样,这不是素数

main :: IO ()
main = print $ primesUpTo 20000

type Nat = Int

primesUpTo :: Nat -> [Nat]
primesUpTo n = primesUpToAux n 1 []

primesUpToAux :: Nat -> Nat -> [Nat] -> [Nat]
primesUpToAux n current primesAcum = 
    if current > n
    then primesAcum
    else primesUpToAux n (current + 1) newPrimesAcum
    where newPrimesAcum = case isPrime current primesAcum of
                          True  -> primesAcum++[current]
                          False -> primesAcum

isPrime :: Nat -> [Nat] -> Bool
isPrime 1 _ = False
isPrime 2 _ = True
isPrime x neededPrimes =
    if sqrtOfX < currentPrime
    then True
    else if mod x currentPrime == 0
         then False
         else isPrime x restOfPrimes
    where
          sqrtOfX = sqrtNat x
          currentPrime:restOfPrimes = neededPrimes

sqrtNat :: Nat -> Nat
sqrtNat = floor . sqrt . fromIntegral

现在这个问题实际上是关于 2 个问题:

1.- 如何将此算法转换为使用数组而不是列表?(是为了学习如何在 Haskell 中处理状态和数组) 有人已经在 cmets 中回答了,但指向一个不太好的解释示例。

2.- 每次发现新素数时如何消除列表的串联?

真 -> primesAcum++[当前]

【问题讨论】:

  • 这里不需要STArray,算法只是试用分区。我认为,您应该先查看标准列表操作函数(filtertakeWhileallany 等),因为可以使用它们以最佳方式和惯用方式解决问题,在这里你'正在绕弯路。
  • 您想要一个功能风格的快速实现?然后查看之前的评论(并确保避免盲目地用列表表示集合的谬误)。或者:您想学习使用 STArray - 然后准确描述您使用 STArray 的问题。你意识到你必须编写“命令式”代码(一切都在 ST monad 中)。
  • 1 不是质数,顺便说一下。
  • Haskell wiki 有一个使用 ST 数组计算素数的示例:haskell.org/haskellwiki/Prime_numbers#Using_ST_Array
  • 解决您的第二个问题:不要执行 primesAcum++[current],即 O(n^2),而是执行 current:primesAccum 并在最后反转列表,即 O(n)。

标签: arrays haskell monads st-monad starray


【解决方案1】:

以下是将您的代码或多或少直接转换为使用未装箱的整数数组:

import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.Array.Unboxed
import Control.Arrow

main :: IO ()
main = print . (length &&& last) . primesUpTo $ 1299709

primesUpTo :: Int -> [Int]
primesUpTo = takeWhile (> 0) . elems . primesUpToUA 

primesUpToUA :: Int -> UArray Int Int
primesUpToUA n = runSTUArray $ do
  let k = floor( 1.25506 * fromIntegral n / log (fromIntegral n)) -- WP formula
  ar <- newArray (0,k+1) 0            -- all zeroes initially, two extra spaces 
  let 
    loop c i | c > n = return ar           -- upper limit is reached 
             | otherwise = do              -- `i` primes currently in the array
         b <- isPrime 0 i c                -- is `c` a prime?
         if  b  then do { writeArray ar i c ; loop (c+1) (i+1) }
         else   loop (c+1) i
    isPrime j i c | j >= i = return True   -- `i` primes 
                  | otherwise = do         --   currently in the array 
            p <- readArray ar j
            if   p*p > c           then return True 
            else  if rem c p == 0  then return False 
            else                   isPrime (j+1) i c
  loop 2 0

这或多或少是不言自明的,当您慢慢阅读时,一次一个陈述。

使用数组,列表连接没有问题,因为没有列表。我们在向其中添加新项目时使用素数数组。

当然,您可以重新编写基于列表的代码以使其表现更好; the simplest re-write

ps :: [Int]
ps = 2 : [i | i <- [3..],  
              and [rem i p > 0 | p <- takeWhile ((<=i).(^2)) ps]]
primesTo n = takeWhile (<= n) ps

关键是从递归思维切换到核心递归思维——不是如何在末尾显式地添加,而是定义如何生成一个列表——让惰性语义处理剩下的事情。

【讨论】:

  • 非常感谢。一个问题,我知道 newArray (0,k+1) 0 产生一个 ST s (STUArray s Int Int),对吧? s(状态)的类型是什么?我无法弄清楚 isPrime 的类型。
  • 查看任何实体的类型,例如isPrime,添加let x = isPrime == True 并阅读有关Bool 与您想知道的类型之间类型不匹配的错误消息。 -- 我们知道runSTUArray :: Ix i =&gt; (forall s. ST s (STUArray s i e)) -&gt; UArray i e,所以其中的do 表达式是:: Ix i =&gt; (forall s. ST s (STUArray s i e))"A computation of type ST s a transforms an internal state indexed by s, and returns a value of type a"。 (...续)
  • (...contd) 所以 do 表达式中的计算是 in ST s monad。 sforall s. 封装,所以我们不知道它是什么,也不能知道。 newArray :: Ix i =&gt; (i, i) -&gt; e -&gt; m (a i e) 所以m ~ ST s 与不可见的sarr :: a i e ~ STUArray s i e(因为我们来自一元计算); eIntiIx - the ranged index 中,就像 (Int, Int)。
  • ...arr :: a i e ~ STUArray s i esame 不可见 s。 -- newArray (0,k+1) 0 :: ST s (STUArray s Int Int) 我们从中“拉/得到”arr
  • 所以是的,你做对了。和isPrime 应该是Int -&gt; Int -&gt; Int -&gt; ST s Bool(同样是不可见的s)。