Need to partition a list into lists based on breaks in ascending order of elements (Haskell)

StackOverflow https://stackoverflow.com/questions/14403293

  •  16-01-2022
  •  | 
  •  

문제

Say I have any list like this:

[4,5,6,7,1,2,3,4,5,6,1,2]

I need a Haskell function that will transform this list into a list of lists which are composed of the segments of the original list which form a series in ascending order. So the result should look like this:

[[4,5,6,7],[1,2,3,4,5,6],[1,2]]

Any suggestions?

도움이 되었습니까?

해결책 2

ascend :: Ord a => [a] -> [[a]]
ascend xs = foldr f [] xs
  where
    f a []  = [[a]]
    f a xs'@(y:ys) | a < head y = (a:y):ys
                   | otherwise = [a]:xs'

In ghci

*Main> ascend [4,5,6,7,1,2,3,4,5,6,1,2]
[[4,5,6,7],[1,2,3,4,5,6],[1,2]]

다른 팁

You can do this by resorting to manual recursion, but I like to believe Haskell is a more evolved language. Let's see if we can develop a solution that uses existing recursion strategies. First some preliminaries.

{-# LANGUAGE NoMonomorphismRestriction #-}
-- because who wants to write type signatures, amirite?
import Data.List.Split -- from package split on Hackage

Step one is to observe that we want to split the list based on a criteria that looks at two elements of the list at once. So we'll need a new list with elements representing a "previous" and "next" value. There's a very standard trick for this:

previousAndNext xs = zip xs (drop 1 xs)

However, for our purposes, this won't quite work: this function always outputs a list that's shorter than the input, and we will always want a list of the same length as the input (and in particular we want some output even when the input is a list of length one). So we'll modify the standard trick just a bit with a "null terminator".

pan xs = zip xs (map Just (drop 1 xs) ++ [Nothing])

Now we're going to look through this list for places where the previous element is bigger than the next element (or the next element doesn't exist). Let's write a predicate that does that check.

bigger (x, y) = maybe False (x >) y

Now let's write the function that actually does the split. Our "delimiters" will be values that satisfy bigger; and we never want to throw them away, so let's keep them.

ascendingTuples = split . keepDelimsR $ whenElt bigger

The final step is just to throw together the bit that constructs the tuples, the bit that splits the tuples, and a last bit of munging to throw away the bits of the tuples we don't care about:

ascending = map (map fst) . ascendingTuples . pan

Let's try it out in ghci:

*Main> ascending [4,5,6,7,1,2,3,4,5,6,1,2]
[[4,5,6,7],[1,2,3,4,5,6],[1,2]]
*Main> ascending [7,6..1]
[[7],[6],[5],[4],[3],[2],[1]]
*Main> ascending []
[[]]
*Main> ascending [1]
[[1]]

P.S. In the current release of split, keepDelimsR is slightly stricter than it needs to be, and as a result ascending currently doesn't work with infinite lists. I've submitted a patch that makes it lazier, though.

This problem is a natural fit for a paramorphism-based solution. Having (as defined in that post)

para  :: (a -> [a] -> b -> b) -> b -> [a] -> b
foldr :: (a ->        b -> b) -> b -> [a] -> b

para  c n (x : xs)  =  c x xs (para  c n xs)
foldr c n (x : xs)  =  c x    (foldr c n xs)
para  c n []        =  n
foldr c n []        =  n

we can write

partition_asc xs  =  para g [] xs  where
  g x (y:_) ~(a:b) | x<y  =  (x:a):b 
  g x  _      r           =  [x]:r 

Trivial, since the abstraction fits.

BTW they have two kinds of map in Common Lisp - mapcar (processing elements of an input list one by one) and maplist (processing "tails" of a list). With this idea we get

import Data.List (tails)

partition_asc2 xs  =  foldr g [] . init . tails $ xs  where
  g (x:y:_) ~(a:b) | x<y  =  (x:a):b
  g (x:_)     r           =  [x]:r 

Lazy patterns in both versions make it work with infinite input lists in a productive manner (as first shown in Daniel Fischer's answer).

update 2020-05-08: not so trivial after all. Both head . head . partition_asc $ [4] ++ undefined and the same for partition_asc2 fail with *** Exception: Prelude.undefined. The combining function g forces the next element y prematurely. It needs to be more carefully written to be productive right away before ever looking at the next element, as e.g. for the second version,

partition_asc2' xs  =  foldr g [] . init . tails $ xs  where
  g (x: ~(y:_)) r@ ~(a:b)  =  (x:g):gs
                        where
                        (g,gs) | x < y    =  (a,b)
                               | otherwise  =  ([],r)

(again, as first shown in Daniel's answer).

You can use a right fold to break up the list at down-steps:

foldr foo [] xs
  where
    foo x yss = (x:zs) : ws
      where
        (zs, ws) = case yss of
                     (ys@(y:_)) : rest
                            | x < y     -> (ys,rest)
                            | otherwise -> ([],yss)
                     _ -> ([],[])

(It's a bit complicated in order to have the combining function lazy in the second argument, so that it works well for infinite lists too.)

One other way of approaching this task (which, in fact lays the fundamentals of a very efficient sorting algorithm) is using the Continuation Passing Style a.k.a CPS which, in this particular case applied to folding from right; foldr.

As is, this answer would only chunk up the ascending chunks however, it would be nice to chunk up the descending ones at the same time... preferably in reverse order all in O(n) which would leave us with only binary merging of the obtained chunks for a perfectly sorted output. Yet that's another answer for another question.

chunks :: Ord a => [a] -> [[a]]
chunks xs = foldr go return xs $ []
            where
            go :: Ord a => a -> ([a] -> [[a]]) -> ([a] -> [[a]])
            go c f = \ps -> let (r:rs) = f [c]
                            in case ps of
                               []  -> r:rs
                               [p] -> if c > p then (p:r):rs else [p]:(r:rs)

*Main> chunks [4,5,6,7,1,2,3,4,5,6,1,2]
[[4,5,6,7],[1,2,3,4,5,6],[1,2]]
*Main> chunks [4,5,6,7,1,2,3,4,5,4,3,2,6,1,2]
[[4,5,6,7],[1,2,3,4,5],[4],[3],[2,6],[1,2]]

In the above code c stands for current and p is for previous and again, remember we are folding from right so previous, is actually the next item to process.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top