排序算法 Lisp-Scheme

4

我决定学习一些函数式语言,最终选择了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。

在函数式编程中,我应该如何思考?语法似乎很简单,但我不习惯这种逻辑。

5个回答

6

具体实现

我会采用Óscar López的答案并进一步拓展:

(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 >=)))))

这个版本和他的最大区别在于,现在使用letrecsorted?定义为Óscar的内部帮助函数,并以两种方式调用它。 函数式思维 你选择了一个很好的例子来说明Scheme如何看待这个世界,而且你的实现已经有了很好的开端。
解决这个问题所涉及的一个重要的函数式原则是,任何你可以把(**here** more stuff '(1 2 3 4))放进去的东西,你都可以将其作为参数传递给另一个函数。也就是说,在函数式编程语言中,函数是一等公民。因此,你在比较中使用<=的事实意味着你可以将<=作为参数传递给另一个函数,该函数相应地进行比较。Óscar的答案很好地说明了这一点。
这个问题的另一个方面体现了另一个常见的函数式模式,即由一个(cond)块主要组成的函数。在许多函数式编程语言(如Haskell、ML、OCaml、F#、Mathematica)中,你会获得比在Scheme中默认更强的模式匹配能力。因此,在Scheme中使用(cond),你必须描述如何测试所寻找的模式,但这通常是相当简单的(例如,在这个实现中的(or (empty? lst) (empty? (cdr lst))))。
我认为在这个问题中体现得很好的另一个函数式编程模式是,许多函数式编程解决方案都是递归的。递归是我不得不使用letrec而不是普通的let的原因。
几乎任何你可以通过对第一个元素(或者像这种情况下的前两个元素)进行操作,然后在列表的尾部(cdr)上重复该操作的事情,你都可以用这种方式来完成。命令式的forwhile循环在Scheme中并非不可能(虽然在纯函数式语言(如Haskell)中它们几乎是不可能的),在许多情况下它们都有些不恰当。但是Scheme的灵活性使你作为开发者能够做出这个决定,在某些情况下可以进行重要的性能或可维护性优化。 继续探索 我在这里回答的第一个sorted?的实现将根据列表中看到的内容决定要传递给sorted-cmp的比较运算符。当我发现一个列表可以以两个相等的数字开头'(1 1 2 3 4 5)时,我就放弃了这个想法。但是当我再考虑一下,肯定有一种方法可以跟踪你是否已经确定了方向,并且只需要一次调用sorted-cmp。你可以考虑接下来探索这个问题。

谢谢您的回复,它对我很有帮助。我认为我可以理解你的大部分答案代码,但最后一部分我不是很清楚你怎么能用or子句来调用参数,并且它会“记住”。因为当我看到那段代码时,我认为它总是会返回true,因为它检查前两个参数是高于、低于还是相等。 - Yaman Baron
此外,我已经阅读了有关如何定义匿名函数的内容,但是我认为使用let、let*、letrec等可能会很混乱,而我更喜欢使用(define sorted (lst cmp))本地方式(在我的尝试中有效),这有什么重大差异吗? - Yaman Baron
你可以考虑接下来探索一下。 - gcbenison
@YamanBaron,使用let的一个原因是你只能对给定符号进行一次define,而let可以暂时覆盖现有的define。在小程序中,您总是可以知道哪些符号已经有定义,并且可以证明使用define是安全的。在稍微大一点的程序中,最安全的方法通常是使用let(以及它的朋友let*letrec)来创建适当范围的临时定义。 - sblom

5
你差不多猜对了,看这里:

你差一点就对了,看这里:

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

稍作修改即可完成基本情况。当列表中只剩一个元素时,必须停止,否则表达式将会抛出错误。
对于你问题的第二部分:如果你想使用不同的标准来检查是否已排序,只需将比较函数作为参数传递,如下所示:
(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参数是本地定义的,但您也提供了帮助。谢谢。 - Yaman Baron

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程序:它高效但很难证明它会做正确的事情。即使对于这个简单的例子,也不是立刻显而易见的,它不能陷入无限循环,或者在逻辑上走错路。当然,对于更复杂的程序来说情况会更糟。
第二部分 - 递归思考
我发现编写可读性强的程序,遵循函数式语言的精神(而不只是试图将命令式解决方案转化为该语言)的关键是专注于程序应计算的“内容”而不是“如何”计算。如果你能够以足够精度这样做-如果你能清晰地写出问题-那么在函数式编程中,大多数时间你几乎已经得到了解决方案!
因此,让我们首先更详细地写出要计算的内容。我们想知道一个列表是否呈之字形(即在某个点下降,在另一个点上升)。哪些列表符合这个标准?好吧,如果一个列表呈之字形,则:
  • 长度大于2且
  • 一开始递增,但在某个时刻开始递减 或
  • 一开始递减,但在某个时刻开始递增 或
  • 其尾部呈锯齿状。

可以将上述语句直接或多或少地翻译成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-increasing”、“initially-decreasing”、“decreases”和“increases”。 “initially-”函数足够简单:
(define (initially-increasing xs)
  (> (cadr xs) (car xs)))

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

那么decreasesincreases呢?如果一个序列长度大于1且第一个元素大于第二个元素,或者它的尾部减少,则该序列减少:

(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函数,但很明显只需要进行一次更改:<必须变成>。复制这么多代码应该会让你感到不安。我能不能要求语言为我创建一个像decreases这样的函数,但是在那个位置使用>呢?在函数式语言中,你可以完全做到这一点,因为函数可以返回其他函数!因此,我们可以编写一个函数来实现:"给定一个比较运算符,返回一个函数,如果它的参数的任何两个连续元素之间的比较为真,则返回真"。
(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)))))

增加减少现在都可以非常简单地定义:

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

没有更多的功能需要实现了 - 我们完成了。与C版本相比,这个版本的优点是显而易见的 - 它要容易得多,可以理解这个程序会做正确的事情。这个程序的大部分都很琐碎,所有的复杂性都被推到了ever函数中,它是一个非常通用的操作,在许多其他上下文中也会很有用。我相信通过搜索,人们可以找到一个标准(因此更可靠)的实现,而不是这个定制的实现。
虽然这是一种改进,但这个程序仍然不完美。有很多自定义递归,一开始并不明显所有的递归都是尾递归(尽管确实如此)。此外,该程序保留了C语言的模糊回声,以多个条件分支和退出点的形式存在。我们可以通过使用惰性求值来获得更清晰的实现,为此我们将切换语言。
第三部分 - 惰性思考

让我们回到问题定义。它可以比第二部分中更简单地陈述为:“如果序列包含相邻元素之间的比较,这些比较在两个方向上进行,则该序列会呈现锯齿状(即非排序)。我可以将这句话直接或多或少地翻译成Haskell代码:

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

现在我需要一种方法来推导“比较”(comparisons),即列表中每个成员与其后继的比较。这并不难做,最好通过示例来解释。
> 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。我非常欣赏你的帖子,并且阅读了很多次才真正掌握了内容。我一直在阅读更深入的Scheme内容,目前正在学习著名的SICP。关于如何判断一个函数是否是尾递归,我的印象是如果递归调用没有进行任何操作/计算,则可以认为它是尾递归。可以使用trace来查看。 - Yaman Baron

0

试试这个

(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))))))

我自己也是新手,还没有测试这段代码。仍在苦苦挣扎于递归中。如果它能正常工作或者出现了错误,请告诉我。


0

我之前的回答非常糟糕。

我在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)))))

检查的案例:

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

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

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

(sorted? '()) 返回 #t

(sorted? '(1)) 返回 #t


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接