【问题标题】:Swap two elements in a list by its indices通过索引交换列表中的两个元素
【发布时间】:2015-05-30 20:27:05
【问题描述】:

如果我对元素的唯一了解是它们在列表中出现的位置,是否有任何方法可以交换列表中的两个元素。

更具体地说,我正在寻找这样的东西:

swapElementsAt :: Int -> Int -> [Int] -> [Int]

会这样:

> swapElementsAt 1 3 [5,4,3,2,1] -- swap the first and third elements
[3,4,5,2,1]

我认为 Haskell 中可能存在为此的内置函数,但我找不到它。

【问题讨论】:

    标签: list haskell


    【解决方案1】:

    这里有几个可行的答案,但我认为更惯用的 haskell 示例会很有用。

    本质上,我们用原始列表压缩一个无限的自然数序列,以将排序信息包含在结果对的第一个元素中,然后我们使用简单的右折叠(catamorphism)从右边消耗列表并创建一个新列表,但这次交换了正确的元素。我们最终提取所有第二个元素,丢弃包含排序的第一个元素。

    这种情况下的索引是从零开始的(与 Haskell 的典型索引一致),并且指针必须在范围内,否则你会得到一个异常(如果你将结果类型更改为 Maybe [a],这很容易避免)。

    swapTwo :: Int -> Int -> [a] -> [a]
    swapTwo f s xs = map snd . foldr (\x a -> 
            if fst x == f then ys !! s : a
            else if fst x == s then ys !! f : a
            else x : a) [] $ ys
        where ys = zip [0..] xs
    

    还有一个衬垫,只需一次完成交换(将 foldr 和 map 的功能组合到一个 zipWith 中):

    swapTwo' f s xs = zipWith (\x y -> 
        if x == f then xs !! s
        else if x == s then xs !! f
        else y) [0..] xs
    

    【讨论】:

    • 是的,终于有东西了,确实有效! swapTwo' 0 0 "a" == "a", swapTwo' 0 1 "ab" == "ba", swapTwo' 1 0 "ab" == "ba" 和更长的列表也可以。这应该是公认的答案!非常感谢您提供的优雅解决方案!
    【解决方案2】:

    Haskell 没有这样的功能,主要是因为它有点不起作用。你到底想达到什么目的?

    您可以实现自己的版本(也许有更惯用的方式来编写它)。请注意,我假设 i < j,但扩展该函数以正确处理其他情况将是微不足道的:

    swapElementsAt :: Int -> Int -> [a] -> [a]
    swapElementsAt i j xs = let elemI = xs !! i
                                elemJ = xs !! j
                                left = take i xs
                                middle = take (j - i - 1) (drop (i + 1) xs)
                                right = drop (j + 1) xs
                        in  left ++ [elemJ] ++ middle ++ [elemI] ++ right
    

    【讨论】:

    • 我正在尝试编写一个解决 15 个难题变体的 haskell 模块:en.wikipedia.org/wiki/15_puzzle。瓦片存储在一个列表中,我想使用此功能将空瓦片与其邻居之一交换。
    • 也许有更好的方法可以做到这一点,但如果我能设法找到一个可行的解决方案,我会很高兴
    • 我想知道如果您有一个 (number, (x, y)) 元组列表而不是整数列表,它将如何与您的程序的其余部分一起工作。在第一种情况下,您可以只在列表上 map 并更新给定位置中正方形的位置(保持其余元素不变)。
    • 像 aochagavia 我想知道这是否是正确的数据结构。也许,@jreuab,您正在寻找一个数组?
    • @Andrea Huh,我什至不知道 Haskell 中存在像数组这样的东西。我想这在这里真的更有意义。
    【解决方案3】:

    警告:微积分。我并不打算完全认真地回答这个问题,因为它更像是一个大锤胡桃夹子。但这是我随身携带的大锤,那为什么不做一些运动呢?除了这可能比提问者想知道的要多,对此我深表歉意。这是试图挖掘已经提出的明智答案背后的更深层次的结构。

    可微函子类至少提供以下部分。

    class (Functor f, Functor (D f)) => Diff (f :: * -> *) where
      type D f :: * -> *
      up   :: (I :*: D f) :-> f
      down :: f :-> (f :.: (I :*: D f))
    

    我想我最好解开其中的一些定义。它们是组合函子的基本工具包。这东西

    type (f :-> g) = forall a. f a -> g a
    

    容器操作的多态函数类型的缩写。

    这里是容器的常量、标识、组成、总和和乘积。

    newtype K a x = K a                       deriving (Functor, Foldable, Traversable, Show)
    newtype I x = I x                         deriving (Functor, Foldable, Traversable, Show)
    newtype (f :.: g) x = C {unC :: f (g x)}  deriving (Functor, Foldable, Traversable, Show)
    data (f :+: g) x = L (f x) | R (g x)      deriving (Functor, Foldable, Traversable, Show)
    data (f :*: g) x = f x :*: g x            deriving (Functor, Foldable, Traversable, Show)
    

    D 通过通常的微积分规则计算函子的导数。它告诉我们如何为一个元素表示一个单孔上下文。让我们再次阅读这些操作的类型。

    up   :: (I :*: D f) :-> f
    

    说我们可以从一对 one 元素和f 中该元素的上下文组成一个完整的f。它是“向上”,因为我们在一个层次结构中向上导航,关注整体而不是一个元素。

    down :: f :-> (f :.: (I :*: D f))
    

    同时,我们可以用 its 上下文将每个元素装饰在一个可微的函子结构中,计算“向下”到特定元素的所有方法。

    我将把基本组件的Diff 实例留到这个答案的末尾。对于我们得到的列表

    instance Diff [] where
      type D [] = [] :*: []
      up (I x :*: (xs :*: ys)) = xs ++ x : ys
      down [] = C []
      down (x : xs) = C ((I x :*: ([] :*: xs)) :
        fmap (id *:* ((x :) *:* id)) (unC (down xs)))
    

    在哪里

    (*:*) :: (f a -> f' a) -> (g a -> g' a) -> (f :*: g) a -> (f' :*: g') a
    (ff' *:* gg') (f :*: g) = ff' f :*: gg' g
    

    例如,

    > unC (down [0,1,2])
    [I 0 :*: ([] :*: [1,2]),I 1 :*: ([0] :*: [2]),I 2 :*: ([0,1] :*: [])]
    

    依次挑选出每个在上下文中的元素。

    如果f 也是Foldable,我们得到一个广义的!! 运算符...

    getN :: (Diff f, Foldable f) => f x -> Int -> (I :*: D f) x
    getN f n = foldMap (: []) (unC (down f)) !! n
    

    ...额外的好处是我们可以获取元素的上下文以及元素本身。

    > getN "abcd" 2
    I 'c' :*: ("ab" :*: "d")
    
    > getN ((I "a" :*: I "b") :*: (I "c" :*: I "d")) 2
    I "c" :*: R ((I "a" :*: I "b") :*: L (K () :*: I "d"))
    

    如果我们想要一个函子提供两个元素的交换,它最好是两次可微的,它的导数最好也是可折叠的。来了。

    swapN :: (Diff f, Diff (D f), Foldable f, Foldable (D f)) =>
      Int -> Int -> f x -> f x
    swapN i j f = case compare i j of
      { LT -> go i j ; EQ -> f ; GT -> go j i } where
      go i j = up (I y :*: up (I x :*: f'')) where
        I x :*: f'   = getN f i          -- grab the left thing
        I y :*: f''  = getN f' (j - 1)   -- grab the right thing
    

    现在可以很容易地取出两个元素并以相反的方式将它们重新插入。如果我们对位置进行编号,我们只需要注意移除元素重新编号位置的方式。

    > swapN 1 3 "abcde"
    "adcbe"
    
    > swapN 1 2 ((I "a" :*: I "b") :*: (I "c" :*: I "d"))
    (I "a" :*: I "c") :*: (I "b" :*: I "d")
    

    与以往一样,您无需深入挖掘有趣的编辑操作即可找到一些不同的工作结构。

    为了完整性。以下是上述涉及的其他实例。

    instance Diff (K a) where     -- constants have zero derivative
      type D (K a) = K Void
      up (_ :*: K z) = absurd z
      down (K a) = C (K a)
    
    instance Diff I where         -- identity has unit derivative
      type D I = K ()
      up (I x :*: K ()) = I x
      down (I x) = C (I (I x :*: K ()))
    
    instance (Diff f, Diff g) => Diff (f :+: g) where  -- commute with +
      type D (f :+: g) = D f :+: D g
      up (I x :*: L f') = L (up (I x :*: f'))
      up (I x :*: R g') = R (up (I x :*: g'))
      down (L f) = C (L (fmap (id *:* L) (unC (down f))))
      down (R g) = C (R (fmap (id *:* R) (unC (down g))))
    
    instance (Diff f, Diff g) => Diff (f :*: g) where  -- product rule
      type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
      up (I x :*: (L (f' :*: g))) = up (I x :*: f') :*: g
      up (I x :*: (R (f :*: g'))) = f :*: up (I x :*: g')
      down (f :*: g) = C     (fmap (id *:* (L . (:*: g))) (unC (down f))
                          :*: fmap (id *:* (R . (f :*:))) (unC (down g)))
    
    instance (Diff f, Diff g) => Diff (f :.: g) where  -- chain rule
      type D (f :.: g) = (D f :.: g) :*: D g
      up (I x :*: (C f'g :*: g')) = C (up (I (up (I x :*: g')) :*: f'g))
      down (C fg) = C (C (fmap inner (unC (down fg)))) where
        inner (I g :*: f'g) = fmap wrap (unC (down g)) where
          wrap (I x :*: g') = I x :*: (C f'g :*: g')
    

    【讨论】:

      【解决方案4】:

      我就是这样解决的:

      swapElementsAt :: Int -> Int -> [a] -> [a]
      swapElementsAt a b list = list1 ++ [list !! b] ++ list2 ++ [list !! a] ++ list3
          where   list1 = take a list;
                  list2 = drop (succ a) (take b list);
                  list3 = drop (succ b) list
      

      这里我使用了位置 0 是第一个的约定。我的函数需要一个

      在我的程序中我最喜欢的是take a list这一行。

      编辑:如果您想获得更多这样酷的线条,请查看以下代码:

      swapElementsAt :: Int -> Int -> [a] -> [a]
      swapElementsAt a another list = list1 ++ [list !! another] ++ list2 ++ [list !! a] ++ list3
          where   list1 = take a list;
                  list2 = drop (succ a) (take another list);
                  list3 = drop (succ another) list
      

      【讨论】:

      • take a list 真的是一条很棒的线路 ;)
      • 这个函数比公认的答案要好,因为swapElementsAt' 0 1 "ab" == "ba",但是在这里失败了:swapElementsAt' 1 0 "ab" == "aabb"跨度>
      • 我喜欢这个答案,因为我能够理解它。谢谢。
      【解决方案5】:

      一阶一次性交换

      swap 1 j    l  = let (jth,ith:l') = swapHelp j l ith in jth:l'
      swap j 1    l  = swap 1 j l
      swap i j (h:t) = h : swap (i-1) (j-1) t
      
      swapHelp 1 (h:t) x = (h,x:t)
      swapHelp n (h:t) x = (y,h:t') where
                           (y,  t') = swapHelp (n-1) t x
      
      • 现在 precondition 符合原始问题,即放宽到 1 for swap i j l
      • 大量借鉴@dfeuer 的想法,以减少将列表的第一个元素与给定位置的另一个元素交换的问题

      【讨论】:

      • 我怀疑任何花哨的解决方案都应该从类似于swap' 0 !j xs = swap'' j xs; swap' i !j (x : xs) = x : swap' (i - 1) (j - 1) xs 的东西开始。这减少了将列表头部与某个给定位置交换的问题。我仍然不清楚如何最好地从那里开始。
      • 如果你将第一个元素传递给你的助手并让它也返回被替换的元素,这很容易。好主意,请参阅我的编辑。
      【解决方案6】:

      这是一件奇怪的事情,但这应该可以,除了因为我在手机上写这篇文章时你必须修复的错误。这个版本避免了不必要地重复列表的相同部分。

      swap' :: Int -> Int -> [a] -> [a]
      swap' first second lst = beginning ++ [y] ++ middle ++ [x] ++ end
        where
          (beginning, (x : r)) = splitAt first lst
          (middle, (y : end)) = splitAt (second - first - 1) r
      
      swap x y | x == y = id
               | otherwise = swap' (min x y) (max x y)
      

      【讨论】:

      • 正如您所说,确实存在一个错误。第二个splitAt 应该是second - first - 1
      • 谢谢,@SohamChowdhury。
      • 这个函数至少比我评论过的其他两个函数要好,但它在边缘情况下失败了:swap 0 1 "ab" == "ba", swap 1 0 "ab" == "ba", 交换 0 0 "a"
      • @thetrutz,应该可以解决它。
      【解决方案7】:

      对于位置交换,使用更复杂的折叠函数,我将最小 (min) 索引的值更改为最大索引的值(xs!!(y-ii)),然后将最大索引的值保留在 temp 中,直到找到它,索引(max)。

      我使用了minmax 来确保以正确的顺序遇到索引,否则我将不得不在folds 函数中添加更多检查和条件。

      folds _ _ _ _ [] = []
      folds i z y tmp (x:xs)
          | i == z = (xs!!(y-ii)):folds ii z y x xs
          | i == y = tmp:folds ii z y 0 xs
          | otherwise = x:folds ii z y tmp xs
          where 
              ii = i+1
      
      swapElementsAt x y xs = folds 0 a b 0 xs
          where
              a = min x y
              b = max x y
      

      结果

      > swapElementsAt 0 1 [1,1,1,3,4,9]
      [1,1,1,3,4,9]
      > swapElementsAt 0 5 [1,1,1,3,4,9]
      [9,1,1,3,4,1]
      > swapElementsAt 3 1 [1,1,1,3,4,5]
      [1,3,1,1,4,5]
      > swapElementsAt 1 3 [1,1,1,3,4,5]
      [1,3,1,1,4,5]
      > swapElementsAt 5 4 [1,1,1,3,4,5]
      [1,1,1,3,5,4]
      
      

      【讨论】:

      • swapItems 2 3 [9,8,7,1,2,3,4] 怎么样,它会交换该列表中的第二个和第三个元素吗? swapItems 2 3 [1,2,3,3,3] 呢,它会交换列表中的 两个 元素吗?
      • 好点,对于具有重复项的列表,需要一种不同的方法。 @WillNess
      • 不,any 列表需要不同的方法,因为问题询问的是 positional 交换,而不是基于值的交换在你的回答中。
      【解决方案8】:

      还有一个递归解决方案:

      setElementAt :: a -> Int -> [a] -> [a]
      setElementAt a 0 (_:tail) = a:tail
      setElementAt a pos (b:tail) = b:(setElementAt a (pred pos) tail)
      
      swapElementsAt :: Int -> Int -> [a] -> [a]
      swapElementsAt 0 b list@(c:tail) = (list !! b):(setElementAt c (pred b) tail)
      swapElementsAt a b (c:tail) = c:(swapElementsAt (pred a) (pred b) tail)
      

      【讨论】:

        【解决方案9】:

        我真的很喜欢@dfeuer 的解决方案。然而,森林砍伐仍有优化空间:

        swap' :: Int -> Int -> [a] -> [a]
        swap' first second lst = beginning $ [y] ++ (middle $ [x] ++ end)
          where
            (beginning, (x : r)) = swapHelp first lst
            (middle, (y : end)) = swapHelp (second - first - 1) r
        
        swapHelp :: Int -> [a] -> ([a] -> [a],[a])
        swapHelp 0 l     = (    id , l)
        swapHelp n (h:t) = ((h:).f , r) where
                           (     f , r) = swapHelp (n-1) t
        

        【讨论】:

        • 我不认为您实际上砍伐了任何森林。除非 GHC 执行一些花哨的优化(我没有检查过),否则您的代码将构建函数组合链。这些通常并不比它们替换的列表好。您是否检查过 GHC 核心或进行了一些分析以查看?
        • 不,我没有。至少避免了重复案例分析。无论如何,感谢您的反馈!我已尝试将其合并到另一个解决方案中。
        猜你喜欢
        • 1970-01-01
        • 2013-12-03
        • 1970-01-01
        • 2020-11-23
        • 2011-09-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多