如何转换某些数组元素的功能方法?

时间:2010-02-11 14:06:14

标签: scala functional-programming

我有一个带有复选框的项目列表的Scala应用程序,以便用户选择一些,然后单击按钮将它们向上移动一个位置(左)。我决定编写一个函数来移动某些符合给定谓词的任意类型的元素。所以,如果你有这些元素:

a b c D E f g h I

并且谓词是“大写字符”,该函数将返回:

a b D E c f g I h

简而言之,任何符合谓词的连续元素序列都会与它左边的单个元素交换。

我想出了以下丑陋的命令式实现。我希望看到一个很好的,希望可读的功能性解决方案。

def shiftUp[T](a:Array[T], shiftable: T => Boolean) = {
    val s = new Array[T](a.length)
    var i = 0
    var j = 0
    while(i < a.length)
    {
        if(!shiftable(a(i)) && i < a.length - 1 && shiftable(a(i+1)))
        {
            var ii = i + 1
            while(ii < a.length && shiftable(a(ii)))
            {
                s(j) = a(ii)
                ii = ii+1
                j = j+1
            }
            s(j) = a(i)
            i = ii
        }
        else
        {
            s(j) = a(i)
            i = i+1
        }
        j = j+1
    }
    s
}
编辑:谢谢大家,我希望你喜欢这个练习!

9 个答案:

答案 0 :(得分:12)

这是一个纯粹的功能实现

def shiftElements[A](l: List[A], pred: A => Boolean): List[A] = {
  def aux(lx: List[A], accum: List[A]): List[A] = {
    lx match {
      case Nil => accum
      case a::b::xs if pred(b) && !pred(a) => aux(a::xs, b::accum)
      case x::xs => aux(xs, x::accum)
    }
  }
  aux(l, Nil).reverse
}

这里使用内部的可变性更快

import scala.collection.mutable.ListBuffer
def shiftElements2[A](l: List[A], pred: A => Boolean): List[A] = {
  val buf = new ListBuffer[A]
  def aux(lx: List[A]) {
    lx match {
      case Nil => ()
      case a::b::xs if pred(b) && !pred(a) => {
        buf.append(b)
        aux(a::xs)
      }
      case x::xs => {
        buf.append(x)
        aux(xs)
      }
    }
  }
  aux(l)
  buf.toList
}

答案 1 :(得分:6)

您可以通过foldLeft(也称为/:)执行此操作:

(str(0).toString /: str.substring(1)) { (buf, ch) =>
    if (ch.isUpper) buf.dropRight(1) + ch + buf.last  else buf + ch
}

它需要处理空字符串,但是:

def foo(Str: String)(p: Char => Boolean) : String = (str(0).toString /: str.substring(1)) { 
   (buf, ch) => if (p(ch) ) buf.dropRight(1) + ch + buf.last else buf + ch
}

val pred = (ch: Char) => ch.isUpper
foo("abcDEfghI")(pred) //prints abDEcfgIh

我将把它留作如何将其修改为基于阵列的解决方案的练习

答案 2 :(得分:2)

这基本上是具有功能风格的命令式算法。

def shifWithSwap[T](a: Array[T], p: T => Boolean) = {
  def swap(i:Int, j:Int) = {
    val tmp = a(i); a(i) = a(j); a(j) = tmp
  }
  def checkAndSwap(i:Int) = i match {
    case n if n < a.length - 1 && !p(a(i)) && p(a(i+1)) => swap(i, i+1)
    case _ =>
  }
  (0 until a.length - 1) map checkAndSwap
  a
}

它修改了Array的位置,带有副作用。我认为它确实像问题中的版本一样,除了它更容易阅读。势在必行并不一定难看......

编辑: darn,在我写下来之前无法入睡(与上面相同,只是更紧凑):

def shift[T](a: Array[T], p: T => Boolean) = {
  for (i <- 0 until a.length - 1; if !p(a(i)) && p(a(i+1))) {
    val tmp = a(i); a(i) = a(i+1); a(i+1) = tmp // swap
  }
  a
}

答案 3 :(得分:1)

不是最快的,但不限于String并使用与@oxbow_lakes相同的逻辑

def shift[T](iter: Iterable[T])(p: T=>Boolean): Iterable[T] = 
  iter.foldLeft(Iterable[T]())((buf, elm) => 
    if (p(elm) && buf.nonEmpty) 
      buf.dropRight(1) ++ Iterable[T](elm) ++ Iterable[T](buf.last) 
    else 
      buf++Iterable[T](elm)
  )

def upperCase(c:Char)=c.isUpper

shift("abcDEfghI")(upperCase).mkString
    //scala> res86: String = abDEcfgIh

val array="abcDEfghI".toArray
shift(array)(upperCase).toArray
    //res89: Array[Char] = Array(a, b, D, E, c, f, g, I, h)

def pair(i:Int)=i%2==0
val l=List(1,2,3,5,4,6,7,9,8)
shift(l)(pair)
    //scala> res88: Iterable[Int] = List(2, 1, 3, 4, 6, 5, 7, 8, 9)

答案 4 :(得分:1)

我不认为下面这些内容有效或可读。不幸的是,所有好的答案似乎都被采纳了,所以我要追求创意。 : - )

def shift[T](a: Seq[T], p: T => Boolean) = {
  val (areP, notP) = a.zipWithIndex partition { case (t, index) => p(t) }
  val shifted = areP map { case (t, index) => (t, index - 1) }
  val others = notP map (shifted.foldLeft(_){
    case ((t, indexNotP), (_, indexIsP)) => 
      if (indexNotP == indexIsP) (t, indexNotP + 1) else (t, indexNotP)
  })
  (shifted ++ others).sortBy(_._2).map(_._1)
}

所以,这就是发生的事情。首先,我将每个字符与其索引(a.zipWithIndex)相关联,然后将其分为arePnotP,具体取决于字符是否满足p

所以,在这一点上,我有两个序列,每个序列由一个字符及其原始序列中的索引组成。

接下来,我只需移动第一个序列中元素的索引,减去1,然后计算shifted

计算未移位元素的新索引要困难得多。对于每个元素(notP map),我将执行foldLeft。左侧的累加器将是元素本身(始终带有索引)。正在折叠的序列是移位元素的序列 - 因此我可以看到,对于每个未移位的元素,我遍历整个移位元素序列(非常低效!)。

因此,我们将未移位元素的索引与每个移位元素的索引进行比较。如果它们相等,则增加未移位元素的索引。因为移位元素的序列是有序的(partition不会改变顺序),我们知道我们将首先测试较低的索引,然后测试更高的索引,保证元素的索引会增加为必要时。

有了它,我们加入两个序列,按它们的新索引排序,然后映射回元素。

答案 5 :(得分:1)

以下是杰夫回答的另一个变种:

def shift[T](l: List[T], p: T => Boolean): List[T] = {
  l match {
    case a::b::t if ! p(a) && p(b) => b::shift(a::t, p)
    case a::t => a::shift(t, p)
    case Nil => l
  }
}

使用

快速测试
scala> def pred(c: Char) = c.isUpper
pred: (c: Char)Boolean

scala> shift("abcDEfghI".toList, pred)
res3: List[Char] = List(a, b, D, E, c, f, g, I, h)

scala> shift("AbCd".toList, pred)
res4: List[Char] = List(A, C, b, d)

scala> shift(Nil, pred)
res5: List[Nothing] = List()

这是第二版

def shift[T](l: List[T], p: T => Boolean, r: List[T] = Nil): List[T] = {
  l match {
    case a::b::t if ! p(a) && p(b) => shift(a::t, p, b::r)
    case a::t => shift(t, p, a::r)
    case Nil => r.reverse
  }
}

答案 6 :(得分:1)

我不太清楚在Scala中编写它,但是这个问题是为列表函数takeWhiledropWhile量身定制的。我们的想法是将项目列表分为三个部分:

  • 左侧部分,使用takeWhile计算,包含不满足谓词的最左侧元素。

  • 中间部分是您要向左移动的元素组,通过删除左侧元素然后takeWhile余数来计算。

  • 正确的部分是剩下的一切; dropWhile中间元素。

这是在Haskell:

-- take first group of elements satisfying p and shift left one
shift :: (a -> Bool) -> [a] -> [a]
shift p l = case reverse left of 
              []     -> l
              (a:as) -> reverse as ++ middle ++ a : shift p right
  where left    = takeWhile (not . p) l  -- could be done with List.break
        notLeft = dropWhile (not . p) l
        middle  = takeWhile p notLeft    -- could be done with List.span
        right   = dropWhile p notLeft

这是一个单元测试:

*Shiftup> shift (>9) [1, 2, 3, 44, 55, 6, 7, 8]
[1,2,44,55,3,6,7,8]

Haskell程序员可能会使用List.breakList.span来组合对takeWhiledropWhile的调用,但我不确定Scala是否有这些内容。此外,takeWhiledropWhile是很有意义的名字,而我至少发现breakspan不那么明显。

编辑:修复递归调用以执行shift p right而不是right来提升所有群组。

答案 7 :(得分:0)

编辑:这实际上并没有解决提出的问题 - 它解决了一个相关但不同的问题(将标记项目的优先级提高一个)。不过,我将它留在这里作为参考。


对于Scala 2.8,这是一个“单行”,根据请求使用数组。

def shiftUp[T](a: Array[T], p: T => Boolean) = {
  a.zipWithIndex.map(ci => {
    (ci._1 , if (p(ci._1)) ci._2 - 1.5 else ci._2.toDouble)
  }).sortWith((l,r) => l._2 < r._2).map(_._1)
}

scala> shiftUp(Array('h','E','l','l','O'),(c:Char)=>c.isUpper).toArray
res0: Array[Char] = Array(E, h, l, O, l)

scala> shiftUp("HeLlO".toArray,(c:Char)=>c.isUpper).toArray
res1: Array[Char] = Array(H, L, e, O, l)

我将它作为练习留给读者来弄清楚它是如何工作的。 (如果你真的想要使用T的泛型,那么在Scala 2.8中它会给你一个GenericArray;如果你想要一个Java潜在的原始数组,你就可以使用它。)

答案 8 :(得分:0)

J:

中的解决方案
   ('abcdefghijklmnopqrstuvwxyz';'ABCDEFGHIJKLMNOPQRSTUVWXYZ') (4 : '(y#~y e. >1{x)([: I. '' ''= ])} }._1&|.&.((1,~y e. >0{x)&#)y,'' ''') 'abcDEfghI'
abDEcfgIh

让我们把它分成命名的部分,以便更容易理解。最后一个字符串“abDEcfgIh”是将函数应用于字符串“abcDEfghI”的结果,该字符串是函数的正确参数。这对字母组成了函数的左参数(它是开始的部分“(4 : ...”)。因此,我们可以单独命名每个字符串,而不是盒装字符串的2元素矢量:

   'lc uc'=. 'abcdefghijklmnopqrstuvwxyz';'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

现在我们有两个变量“lc”和“uc”用于小写和大写字母,让我们详细检查函数体。从右端获取逻辑上连贯的块,因为这将首先进行评估,我们可以这样命名:

   rmUCshift=: 4 : 0
   }._1&|.&.((1,~y e. >0{x)&#)y,' '
)

这将“rmUCshift”定义为需要左右参数(“4 :”指定此内容)的内容,正文从下一行开始并继续到裸露的结束paren。 “4 : 0”形式,后跟身体,是最初显示的“4 :'body'”形式的变体。这个动词rmUCshift可以像这样独立调用:

   (lc;'') rmUCshift 'abcDEfghI'  NB. Remove upper-case, shift, then insert
ab  cfg h                         NB. spaces where the upper-case would now be.

调用缩进三个空格,输出紧跟在它之后。左参数(lc;'')是一个双元素向量,其中空数组被指定为第二个元素,因为它没有在这段代码中使用 - 我们可以在分号后使用任何值但是两个单引号很容易输入。

接下来要命名的部分是这些(定义后跟示例):

  ixSpaces=: [:I.' '=]
  ixSpaces 'ab  cfg h'
2 3 7
   onlyUC=: 4 : 'y#~y e.>1{x'
   ('';uc) onlyUC 'abcDEfghI'
DEI

将这些命名的片段组合在一起就可以得到:

   (lc;uc) (4 : '(x onlyUC y)(ixSpaces x rmUCshift y)}x rmUCshift y') 'abcDEfghI'
abDEcfgIh

然而,重复“x rmUCshift y”是不必要的,可以简化为我们提供:

   (lc;uc) (4 : '(x onlyUC y) ((ixSpaces ]) } ]) x rmUCshift y') 'abcDEfghI'
abDEcfgIh