【问题标题】:Sorting algorithm lisp-scheme排序算法lisp-scheme
【发布时间】:2012-04-26 18:39:20
【问题描述】:

我决定学习一些函数式语言,毕竟我迷上了 lisp-scheme。

我正在尝试创建一个函数来检查列表是否已排序,无论是最低的第一个变得更高还是反之亦然,如果它可以排序,它应该返回 true,否则返回 false。

这是我的第一个代码,仅当列表增加(或相等)时才有效。

    (define sorted?
     (lambda (lst)
      (cond ((empty? lst) #t)
            (else (and (<= (car lst) (cadr lst))
                  (sorted? (cdr lst)))))))

澄清:像 (sorted? '(1 2 3 4 5)) 和 (sorted? '(5 4 3 2 1)) 这样的东西应该返回 true,否则当然返回 false。

在使用函数式编程时我应该如何思考?语法看起来很简单,但我不习惯逻辑。

【问题讨论】:

    标签: sorting lisp scheme


    【解决方案1】:

    具体实现

    我会选择Óscar López's answer 并更进一步:

    (define sorted? (lambda (lst)
      (letrec ((sorted-cmp 
                (lambda (lst cmp)
                  (cond ((or (empty? lst) (empty? (cdr lst)))
                         #t)
                        (else (and (cmp (car lst) (cadr lst))
                                   (sorted-cmp (cdr lst) cmp)))))))
        (or (sorted-cmp lst <=) (sorted-cmp lst >=)))))
    

    这个版本和他的最大区别在于sorted? 现在将 Óscar 的版本定义为使用letrec 的内部辅助函数,并且双向调用。

    功能性思维

    您实际上选择了一个很好的示例来说明 Scheme 如何看待世界的某些方面,并且您的实现有了一个非常好的开始。

    解决这个问题的一个重要功能原则是,任何你可以输入(**here** more stuff '(1 2 3 4))的东西,你都可以作为参数传递给另一个函数。也就是说,函数是函数式编程语言中的第一类。因此,您在比较中使用&lt;= 的事实意味着您可以将&lt;= 作为参数传递给另一个相应地进行比较的函数。 Óscar 的回答很好地说明了这一点。

    体现另一种常见功能模式的此问题的另一个方面是主要由(cond) 块组成的功能。在许多函数式编程语言(Haskell、ML、OCaml、F#、Mathematica)中,您获得的模式匹配能力比在 Scheme 默认情况下获得的要强。因此,对于 Scheme 中的 (cond),您必须描述如何测试您所寻求的模式,但这通常相当简单(例如此实现中的 (or (empty? lst) (empty? (cdr lst)))

    我认为在这个问题中得到很好体现的最后一个函数式编程模式是许多函数式编程解决方案都是递归的。递归是我必须使用 letrec 而不是普通的 let 的原因。

    通过对第一个元素(或本例中的 2 个元素)进行操作,然后在列表的尾部 (cdr) 上重复操作,您几乎可以做任何事情。命令式for- 或while 式循环在Scheme 中并非不可能(尽管在Haskell 等纯函数式语言中几乎不可能),但在许多情况下它们在Scheme 中有点不合适。但是,Scheme 的灵活性允许您作为开发人员做出决定,从而在某些情况下实现重要的性能或可维护性优化。

    继续探索

    我第一次实现sorted? 是为了根据它在列表中看到的内容来决定将哪个比较运算符传递给sorted-cmp。当我发现一个列表可以以两个相等的数字'(1 1 2 3 4 5) 开头时,我放弃了这一点。但是当我想得更多时,肯定有一种方法可以跟踪你是否已经决定了一个方向,因此,只需要一个电话到sorted-cmp。接下来你可以考虑探索一下。

    【讨论】:

    • 感谢您的回复,对您有很大帮助。我想我本可以找出你的大部分答案代码,但最后一部分,我真的不知道你能回忆起一个带有这样的或子句的论点,并且它会“记住”。因为当我看到那个代码时,我认为它总是会给出真实的,因为它会检查前两个是否更高、更低或相等。
    • 另外,我读过一个定义这样的匿名函数的回合,但是我认为使用 let、let*、letrec 等可能会很混乱,并且更喜欢使用 (define sorted (lst cmp)) local 代替(当我尝试时有效),有很大的不同吗?
    • “你可能会考虑探索下一个” - 好的,先生,我接受你的挑战,用三种语言(见我的回答)
    • @YamanBaron,使用let 的一个原因是您一次只能define 给定符号,而let 可以临时覆盖现有的defines。在一个小程序中,您总是可以知道哪些符号已经有定义,并且您可以证明使用 define 是安全的。在稍微大一点的程序中,使用let(及其朋友let*letrec)创建适当范围的临时定义通常是最安全的。
    【解决方案2】:

    你几乎猜对了,看:

    (define sorted?
      (lambda (lst)
        (cond ((or (empty? lst) (empty? (cdr lst)))
               #t)
              (else (and (<= (car lst) (cadr lst))
                         (sorted? (cdr lst)))))))
    

    在基本情况下稍作修改,一切就绪。当列表中只剩下一个元素时必须停止,否则cadr表达式会抛出错误。

    对于您问题的第二部分:如果您想检查它是否使用不同的标准进行排序,只需将比较函数作为参数传递,如下所示:

    (define sorted?
      (lambda (lst cmp)
        (cond ((or (empty? lst) (empty? (cdr lst)))
               #t)
              (else (and (cmp (car lst) (cadr lst))
                         (sorted? (cdr lst) cmp))))))
    
    (sorted? '(1 2 3 4 5) <=)
    > #t
    (sorted? '(5 4 3 2 1) >=)
    > #t
    

    现在如果你想知道一个列表是按或者升序还是降序排序的:

    (define lst '(1 2 3 4 5))
    (or (sorted? lst >=) (sorted? lst <=))
    > #t
    

    如您所见,函数式编程是关于定义尽可能通用的过程并将它们组合起来以解决问题。您可以将函数作为参数传递这一事实有助于实现泛型函数。

    【讨论】:

    • 感谢您的评论。我正在寻找一种本地定义 cmp 参数的方式,但你也提供了帮助。谢谢。
    【解决方案3】:

    我将把你的问题理解为,更具体地说,“如果我已经使用 C 或 Java 等命令式语言进行编程,我该如何调整我的函数式编程思维?”以你的问题为例,我将在周六早上用长篇回答这个问题。我将通过三个阶段来追溯函数式程序员的演变,每个阶段都是禅宗的更高层面 - 1) 迭代地思考; 2) 递归思考; 3) 懒惰地思考。

    第一部分 - 反复思考

    假设我正在用 C 语言编程,但我不能或不会使用递归 - 也许编译器没有优化尾递归,递归解决方案会溢出堆栈。所以我开始思考我需要保持什么状态。我想象有一台小机器爬过输入。它会记住它是在搜索递增序列还是递减序列。如果它还没有决定,它会根据当前的输入来决定,如果可以的话。如果它发现输入指向错误的方向,它会以 zigzag=true 终止。如果到达输入的末尾,则以 zigzag=false 终止。

    int
    zigzag(int *data, int n)
    {
      enum {unknown, increasing, decreasing} direction = unknown;
    
      int i;
      for (i = 1; i < n; ++i)
        {
          if (data[i] > data[i - 1]) {
        if (direction == decreasing) return 1;
        direction = increasing;
          }
          if (data[i] < data[i - 1]) {
        if (direction == increasing) return 1;
        direction = decreasing;
          }
        }
    
      /* We've made it through the gauntlet, no zigzagging */
      return 0;
    }
    

    这个程序是典型的 C 程序:它很高效,但很难证明它会做正确的事情。即使对于这个简单的示例,也不能立即看出它不会陷入无限循环,或者在某个地方的逻辑出现错误。当然,更复杂的程序会变得更糟。

    第二部分 - 递归思考

    我发现,以函数式语言的精神(而不是仅仅试图将命令式解决方案转变为该语言)编写可读程序的关键是关注程序应该计算什么而不是而不是如何它应该做到这一点。如果你能以足够的精度做到这一点——如果你能清楚地写出问题——那么在函数式编程中的大部分时间里,你几乎已经找到了解决方案!

    所以让我们从写出要更详细计算的内容开始。我们想知道一个列表是否曲折(即在某个点减少,在另一个点增加)。哪些列表符合此标准?好吧,如果:

    • 它的长度超过两个元素并且
    • 它最初会增加,但随后会在某个点减少或
    • 它最初会减少,但随后会在某个点增加或
    • 它的尾巴曲折。

    可以或多或少地直接将上述语句翻译成一个 Scheme 函数:

    (define (zigzag xs)
      (and (> (length xs) 2)
           (or (and (initially-increasing xs) (decreases xs))
               (and (initially-decreasing xs) (increases xs))
               (zigzag (cdr xs)))))
    

    现在我们需要定义initially-increasinginitially-decreasingdecreasesincreasesinitially- 函数很简单:

    (define (initially-increasing xs)
      (> (cadr xs) (car xs)))
    
    (define (initially-decreasing xs)
      (< (cadr xs) (car xs)))
    

    decreasesincreases 呢?好吧,如果序列的长度大于一个,并且第一个元素大于第二个元素,或者它的尾部减少,则序列会减少:

    (define (decreases xs)
      (letrec ((passes
            (lambda (prev rest)
              (cond ((null? rest) #f)
                ((< (car rest) prev)
                 #t)
                (else (passes (car rest) (cdr rest)))))))
        (passes (car xs) (cdr xs))))
    

    我们可以编写一个类似的increases 函数,但很明显只需要进行一项更改:&lt; 必须变为&gt;。复制这么多代码会让你感到不安。难道我不能让语言让我成为像decreases 这样的函数,而是在那个地方使用&gt; 吗?在函数式语言中,您完全可以这样做,因为函数可以返回其他函数!因此我们可以编写一个函数来实现:“给定一个比较运算符,如果该比较对其参数的任何两个连续元素都为真,则返回一个返回真的函数。”

    (define (ever op)
     (lambda (xs)
       (letrec ((passes
             (lambda (prev rest)
               (cond ((null? rest) #f)
                 ((op (car rest) prev)
                  #t)
                 (else (passes (car rest) (cdr rest)))))))
         (passes (car xs) (cdr xs)))))
    

    increasesdecreases 现在都可以非常简单地定义:

    (define decreases (ever <))
    (define increases (ever >))
    

    没有更多要实现的功能 - 我们完成了。这个版本相对于 C 版本的优势是显而易见的——更容易推断这个程序会做正确的事情。该程序的大部分内容都非常简单,所有复杂性都被推到了ever 函数中,这是一个非常通用的操作,在许多其他上下文中都很有用。我确信通过搜索可以找到一个标准的(因此更值得信赖的)实现,而不是这个自定义的实现。

    虽然有所改进,但该程序仍不完美。有很多自定义递归,起初并不明显所有这些都是tail recursive(尽管它是)。此外,该程序以多个条件分支和退出点的形式保留了 C 的微弱回声。在惰性评估的帮助下,我们可以得到更清晰的实现,为此我们将切换语言。

    第三部分 - 懒惰地思考

    让我们回到问题定义。它实际上可以比第二部分更简单地说明 - “如果序列包含在两个方向上的相邻元素之间的比较,则它是曲折的(即未排序的)”。我可以或多或少直接将那句话翻译成一行 Haskell:

    zigzag xs = LT `elem` comparisons && GT `elem` comparisons
    

    现在我需要一种方法来导出comparisons,这是xs 的每个成员与其后继者的比较列表。这并不难做到,也许最好通过例子来解释。

    > xs
    [1,1,1,2,3,4,5,3,9,9]
    
    > zip xs (tail xs)
    [(1,1),(1,1),(1,2),(2,3),(3,4),(4,5),(5,3),(3,9),(9,9)]
    
    > map (\(x,y) -> compare x y) $ zip xs (tail xs)
    [EQ,EQ,LT,LT,LT,LT,GT,LT,EQ]
    

    这就是我们所需要的;这两行是完整的实现 -

    zigzag xs = LT `elem` comparisons && GT `elem` comparisons
      where comparisons = map (\(x,y) -> compare x y) $ zip xs (tail xs)
    

    我会注意到,这个程序只通过列表一次来测试增加和减少的情况。

    到此为止,您可能已经想到了一个反对意见:这种方法是不是很浪费?这不是要搜索整个输入列表,而它只需要到达第一个方向变化吗?实际上,不,它不会,因为懒惰的评估。在上面的示例中,它计算了整个比较列表,因为它必须打印出来。但是,如果它将结果传递给zigzag,它只会评估比较列表,以找到GT 的一个实例和LT 的一个实例,而不会进一步。为了让自己相信这一点,请考虑以下案例:

    > zigzag $ 2:[1..]
    True
    > zigzag 1:[9,8..]
    True
    

    两种情况下的输入都是一个无限列表([2,1,2,3,4,5..] 和 [1,9,8,7,6,5...])。尝试将它们打印出来,它们将填满屏幕。但是将它们传递给zigzag,它会很快返回,只要它发现第一个方向变化。

    阅读代码的许多困难来自于遵循控制流的多个分支。许多这些分支确实是在努力避免计算超出我们的需要。但是通过惰性求值可以实现很多相同的目标,从而使程序更短,更真实地反映原始问题。

    【讨论】:

    • 一篇非常好的帖子,我从没想过会有这么多函数式程序员,我认为大多数人都使用 c/java,也许还有其他快速脚本语言,如 python。您的帖子非常受欢迎,我多次阅读以真正掌握内容。我一直在阅读有关方案的一些更独立的内容,我目前正在通过著名的 SICP。关于查看某些东西是否是尾递归的,我得到的印象是,如果在递归调用中没有进行任何操作/计算,那么某些东西就是尾递归的。 1 使用trace时可以看到。
    【解决方案4】:

    试试这个

    (define sorted?
      (lambda (l)
         (cond ((null? l) #t)
               (else (check-asc? (car l) (sorted? (cdr l))
                     (check-desc? (car l) (sorted? (cdr l))))))
    
    
    (define check-asc?
      (lambda (elt lst)
         (cond ((null? lst) #t)
               (else (or (< elt (car lst)) (= elt (car lst))) (check-asc? (car lst) (cdr lst))))))
    
    (define check-desc?
      (lambda (elt lst)
         (cond ((null? lst) #t)
               (else (or (< elt (car lst)) (= elt (car lst))) (check-desc? (car lst) (cdr lst))))))
    

    我自己也是新手。我没有测试过这段代码。仍在为递归而苦苦挣扎。请告诉我它是否有效或它给出了什么错误。

    【讨论】:

      【解决方案5】:

      我之前给出的答案真的很糟糕。

      我在 DrScheme 中运行了代码,但它给出了错误。

      但是我已经修改了它。这是一个有效的代码:

      (define sorted?
       (lambda (l)
         (cond ((null? l) #t)
             (else (if (check-asc? (car l) (cdr l)) #t
                   (check-desc? (car l) (cdr l)))))))
      
      
      (define check-asc?
      (lambda (elt lst)
        (cond ((null? lst) #t)
             (else (if (or (< elt (car lst)) (= elt (car lst))) (check-asc? (car lst) (cdr lst))
                       #f)))))
      
      (define check-desc?
       (lambda (elt lst)
        (cond ((null? lst) #t)
             (else (if (or (> elt (car lst)) (= elt (car lst))) (check-desc? (car lst) (cdr lst))
                       #f)))))
      

      检查案例:

      (排序?'(5 4 3 2 1)) 返回#t

      (排序?'(1 2 3 4 5)) 返回#t

      (排序?'(1 2 3 5 4)) 返回#f

      (排序?'()) 返回#t

      (排序?'(1)) 返回#t

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-06-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多