【问题标题】:Selection sort in functional Scala函数式Scala中的选择排序
【发布时间】:2010-12-12 22:13:05
【问题描述】:

我正在学习“Scala 编程”并编写了选择排序算法的快速实现。然而,由于我在函数式编程方面还有些不成熟,所以我在转换成更 Scala 风格的风格时遇到了麻烦。对于那里的 Scala 程序员,我怎样才能使用 Lists 和 val 来做到这一点,而不是退回到我的命令方式?

http://gist.github.com/225870

【问题讨论】:

  • 您可能希望将代码添加到您的问题中,因为它会让其他人更容易。
  • 注意示例代码问题中引用的 url (Gist)。
  • 如果您给要点起一个以 .scala 结尾的名称,它将突出显示代码;这将有助于阅读。
  • 我的浏览器 (IE 8) 甚至不会通过该链接显示任何内容。
  • gist.github.com/226158 分叉添加语法高亮

标签: scala functional-programming recursion


【解决方案1】:

作为starblue already said,您需要一个函数来计算列表的最小值并返回删除该元素的列表。这是我对类似东西的尾递归实现(我相信foldl 在标准库中是尾递归的),我试图让它尽可能地实用:)。它返回一个列表,其中包含原始列表的所有元素(但有点颠倒 - 请参阅下面的解释),其中最小值作为头部。

def minimum(xs: List[Int]): List[Int] =
  (List(xs.head) /: xs.tail) {
    (ys, x) =>
      if(x < ys.head) (x :: ys)
      else            (ys.head :: x :: ys.tail)
  }

这基本上是折叠,从包含xs 第一个元素的列表开始如果xs 的第一个元素小于该列表的头部,我们将其预先附加到列表ys .否则,我们将其添加到列表 ys 作为第二个元素。以此类推,我们将列表折叠成一个新列表,其中包含最小元素作为头部,另一个列表包含xs 的所有元素(不一定以相同的顺序),并移除最小值作为尾部。请注意,此函数不会删除重复项。

创建此辅助函数后,现在很容易实现选择排序。

def selectionSort(xs: List[Int]): List[Int] =  
  if(xs.isEmpty) List()
  else {
    val ys = minimum(xs)
    if(ys.tail.isEmpty) 
      ys
    else
      ys.head :: selectionSort(ys.tail)
  }

不幸的是,这个实现不是尾递归的,所以它会炸毁大列表的堆栈。无论如何,您不应该对大型列表使用 O(n^2) 排序,但是......如果实现是尾递归的,那就太好了。我会想办法……我认为它看起来像是折叠的实现。

尾递归!

为了让它成为尾递归,我在函数式编程中使用了一种非常常见的模式——累加器。它的工作有点落后,因为现在我需要一个名为maximum 的函数,它基本上与minimum 相同,但具有最大元素 - 它的实现与最小值完全相同,但使用&gt; 而不是&lt; .

def selectionSort(xs: List[Int]) = {
  def selectionSortHelper(xs: List[Int], accumulator: List[Int]): List[Int] =
    if(xs.isEmpty) accumulator
    else {
          val ys = maximum(xs)
          selectionSortHelper(ys.tail, ys.head :: accumulator)
    }

  selectionSortHelper(xs, Nil) 
  }

编辑:将答案更改为具有辅助功能作为选择排序功能的子功能。

它基本上将最大值累积到一个列表中,最终将其作为基本情况返回。您还可以通过将 accumulator 替换为 throw new NullPointerException 来查看它是尾递归的 - 然后检查堆栈跟踪。

这里是使用累加器的分步排序。左侧显示列表xs,而右侧显示accumulator。最大值在每一步用星号表示。

64* 25 12 22 11  ------- Nil
11 22 12 25*     ------- 64
22* 12 11        ------- 25 64
11 12*           ------- 22 25 64
11*              ------- 12 22 25 64
Nil              ------- 11 12 22 25 64

下面展示了一步一步的折叠计算最大值:

maximum(25 12 64 22 11)

25 :: Nil         /: 12 64 22 11  -- 25 > 12, so it stays as head
25 :: 12          /: 64 22 11     -- same as above
64 :: 25 12       /: 22 11        -- 25 < 64, so the new head is 64
64 :: 22 25 12    /: 11           -- and stays so
64 :: 11 22 25 12 /: Nil          -- until the end

64 11 22 25 12

【讨论】:

  • 我建议将尾递归函数作为主函数的子函数。否则,您必须关注类或方法是最终的,以确保可以应用优化。
  • 是的,这是个好主意,谢谢。我将编辑我的答案以将其作为子功能。
  • 这不仅对我的具体问题非常有帮助,而且也有助于加深我对 FP 的理解。谢谢,弗拉维乌!
【解决方案2】:

您应该在函数式样式中进行选择排序时遇到问题,因为它是一种就地排序算法。根据定义,就地就地不起作用。

您将面临的主要问题是您无法交换 元素。这就是为什么这很重要。假设我有一个列表 (a0 ... ax ... an),其中 ax 是最小值。你需要把 ax 弄走,然后组成一个列表 (a0 ... ax-1 ax+ 1 一个n)。问题是,如果您希望保持纯粹的功能,则必须将元素 a0复制到 ax-1。其他函数式数据结构,尤其是树,可以有比这更好的性能,但基本问题仍然存在。

【讨论】:

  • 仍然是它的功能版本,正如您所指出的那样,它不能就位,对于教育/学习目的可能很有价值。
【解决方案3】:

这是选择排序的另一种实现(通用版本)。

def less[T <: Comparable[T]](i: T, j: T) = i.compareTo(j) < 0

def swap[T](xs: Array[T], i: Int, j: Int) { val tmp = xs(i); xs(i) = xs(j); xs(j) = tmp }

def selectiveSort[T <: Comparable[T]](xs: Array[T]) {
    val n = xs.size
    for (i <- 0 until n) {
        val min = List.range(i + 1, n).foldLeft(i)((a, b) => if (less(xs(a), xs(b))) a else b)
        swap(xs, i, min)
    }
  }     

【讨论】:

    【解决方案4】:

    您需要一个辅助函数来进行选择。它应该返回最小元素和删除元素的列表的其余部分。

    【讨论】:

    • 这正是我正在努力解决的部分......我可以想到非常低效的方法,但似乎都不理想。我也用一些伪代码更新了 Gist。
    【解决方案5】:

    我认为以函数式风格进行选择排序是合理可行的,但正如 Daniel 指出的那样,它很有可能表现得很糟糕。

    我只是尝试编写一个函数式冒泡排序,作为选择排序的一个稍微简单和退化的例子。这是我所做的,这暗示了您可以做什么:

    define bubble(data)
      if data is empty or just one element: return data;
      otherwise, if the first element < the second,
        return first element :: bubble(rest of data);
        otherwise, return second element :: bubble(
          first element :: (rest of data starting at 3rd element)).
    

    一旦完成递归,最大的元素就在列表的末尾。现在,

    define bubblesort [data]
      apply bubble to data as often as there are elements in data.
    

    完成后,您的数据确实已排序。是的,这很糟糕,但是我的 Clojure 实现的这个伪代码可以工作。

    只关注第一个或两个元素,然后将其余工作留给递归活动,这是一种 lisp-y、功能性的方式来做这种事情。但是,一旦您的大脑习惯了这种思维方式,就会有更明智的方法来解决问题。

    我建议实现一个归并排序:

    Break list into two sub-lists, 
    either by counting off half the elements into one sublist 
      and the rest in the other,
    or by copying every other element from the original list 
      into either of the new lists.
    
    Sort each of the two smaller lists (recursion here, obviously).
    
    Assemble a new list by selecting the smaller from the front of either sub-list
    until you've exhausted both sub-lists.
    

    递归在这中间,我没有看到使算法尾递归的聪明方法。不过,我认为它的时间是 O(log-2),而且不会给堆栈带来过大的负载。

    玩得开心,祝你好运!

    【讨论】:

      【解决方案6】:

      感谢以上提示,它们非常鼓舞人心。这是选择排序算法的另一种功能方法。我试图基于以下想法:min(A)=if A=Nil -&gt;Int.MaxValue else min(A.head, min(A.tail)) 可以很容易地找到最大值/最小值。第一个最小值是列表的最小值,第二个是两个数字的最小值。这很容易理解,但不幸的是不是尾递归。使用 accumulator 方法,可以像这样转换 min 定义,现在在正确的 Scala 中:

      def min(x: Int,y: Int) = if (x<y) x else y
      
      def min(xs: List[Int], accu: Int): Int = xs match {
          case Nil => accu
          case x :: ys => min(ys, min(accu, x))
      }
      

      (这是尾递归)

      现在需要一个最小版本,它返回一个省略最小值的列表。以下函数返回一个列表,其头部为最小值,尾部包含原始列表的其余部分:

      def minl(xs: List[Int]): List[Int] = minl(xs, List(Int.MaxValue))
      
      def minl(xs: List[Int],accu:List[Int]): List[Int] = xs match {
      // accu always contains min as head
          case Nil => accu take accu.length-1
          case x :: ys => minl(ys, 
              if (x<accu.head) x::accu else accu.head :: x :: accu.tail )
      }
      

      使用这种选择排序可以递归地写成:

      def ssort(xs: List[Int], accu: List[Int]): List[Int] = minl(xs) match {
          case Nil => accu
          case min :: rest => ssort(rest, min::accu)
      }
      

      (颠倒顺序)。在包含 10000 个列表元素的测试中,该算法仅比通常的命令式算法慢 4 倍左右。

      【讨论】:

        【解决方案7】:

        尽管在编写 Scala 时,我习惯于更喜欢函数式编程风格(通过组合器或递归)而不是命令式编程风格(通过变量和迭代),这一次,对于这个特定问题,老派的命令式嵌套循环会导致更简单、更高效的代码。

        对于某些类型的问题,我不认为退回到命令式风格是错误的,例如排序算法通常将输入缓冲区转换到位而不是产生新的集合。

        我的解决办法是:

        package bitspoke.algo
        
        import scala.math.Ordered
        import scala.collection.mutable.Buffer
        
        abstract class Sorter[T <% Ordered[T]] {
        
          // algorithm provided by subclasses
          def sort(buffer : Buffer[T]) : Unit
        
          // check if the buffer is sorted
          def sorted(buffer : Buffer[T]) = buffer.isEmpty || buffer.view.zip(buffer.tail).forall { t => t._2 > t._1 }
        
          // swap elements in buffer
          def swap(buffer : Buffer[T], i:Int, j:Int) {
            val temp = buffer(i)
            buffer(i) = buffer(j)
            buffer(j) = temp
          }
        }
        
        
        class SelectionSorter[T <% Ordered[T]] extends Sorter[T] {
          def sort(buffer : Buffer[T]) : Unit = {
            for (i <- 0 until buffer.length) {
              var min = i
              for (j <- i until buffer.length) {
                if (buffer(j) < buffer(min))
                  min = j
               }
               swap(buffer, i, min)
             }
          }
        }
        

        如您所见,为了实现参数多态,我更喜欢java.lang.Comparable,而不是使用scala.math.Ordered 和Scala View Bounds 而不是Upper Bounds。得益于 Scala 将原始类型隐式转换为 Rich Wrappers,这确实有效。

        您可以编写如下客户端程序:

        import bitspoke.algo._
        import scala.collection.mutable._
        
        val sorter = new SelectionSorter[Int]
        val buffer = ArrayBuffer(3, 0, 4, 2, 1)
        sorter.sort(buffer)
        
        assert(sorter.sorted(buffer))
        

        【讨论】:

          【解决方案8】:

          一个简单的 Scala 选择排序函数程序

          def selectionSort(list:List[Int]):List[Int] = {
            @tailrec
            def selectSortHelper(list:List[Int], accumList:List[Int] = List[Int]()): List[Int] = {
          
              list match {
                case Nil => accumList
                case _ => {
                  val min  = list.min
                  val requiredList = list.filter(_ != min)
                  selectSortHelper(requiredList, accumList ::: List.fill(list.length - requiredList.length)(min))
                }
              }
            }
            selectSortHelper(list)
          }
          

          【讨论】:

            【解决方案9】:

            您可能想尝试用递归替换您的 while 循环,因此,您有两个地方可以创建新的递归函数。

            这将开始摆脱一些变量。

            这对我来说可能是最艰难的一课,我试图更多地转向 FP。

            我不愿在这里展示解决方案,因为我认为您最好先尝试。

            但是,如果可能,您应该使用尾递归,以避免堆栈溢出问题(如果您正在对非常非常大的列表进行排序)。

            【讨论】:

            • 是的,与其说是递归,不如说是我所欠缺的递归函数的字面实现。我也用伪代码更新了 Gist。不过谢谢你的建议……我会继续努力的。
            【解决方案10】:

            这是我对这个问题的看法:SelectionSort.scala

            def selectionsort[A <% Ordered[A]](list: List[A]): List[A] = {
              def sort(as: List[A], bs: List[A]): List[A] = as match {
                case h :: t => select(h, t, Nil, bs)
                case Nil => bs
              }
            
              def select(m: A, as: List[A], zs: List[A], bs: List[A]): List[A] = 
                as match {
                  case h :: t => 
                    if (m > h) select(m, t, h :: zs, bs)
                    else select(h, t, m :: zs, bs)
                  case Nil => sort(zs, m :: bs)
                }
            
              sort(list, Nil)
            }
            

            有两个内部函数:sortselect,代表原始算法中的两个循环。第一个函数sort 遍历元素并为每个元素调用select。当源列表为空时,它返回bs 列表作为结果,最初是Nilsort 函数尝试在源列表中搜索最大值(不是最小值,因为我们以相反的顺序构建结果列表)元素。它假设最大值默认为head,然后将其替换为适当的值。

            这是 Scala 中选择排序的 100% 功能实现。

            【讨论】:

              【解决方案11】:

              这是我的解决方案

              def sort(list: List[Int]): List[Int] = {
                  @tailrec
                  def pivotCompare(p: Int, l: List[Int], accList: List[Int] = List.empty): List[Int] = {
                    l match {
                      case Nil              => p +: accList
                      case x :: xs if p < x => pivotCompare(p, xs, accList :+ x)
                      case x :: xs          => pivotCompare(x, xs, accList :+ p)
                    }
                  }
                  @tailrec
                  def loop(list: List[Int], accList: List[Int] = List.empty): List[Int] = {
                    list match {
                      case x :: xs =>
                        pivotCompare(x, xs) match {
                          case Nil       => accList
                          case h :: tail => loop(tail, accList :+ h)
                        }
                      case Nil => accList
                    }
                  }
              
                  loop(list)
                }
              
              

              【讨论】:

                猜你喜欢
                • 2016-08-28
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2014-11-02
                • 1970-01-01
                • 2021-11-03
                • 2013-01-11
                • 2016-08-15
                相关资源
                最近更新 更多