【问题标题】:How to split list into evenly sized chunks in Racket (Scheme)?如何在球拍(方案)中将列表拆分为大小均匀的块?
【发布时间】:2022-02-23 14:58:52
【问题描述】:

示例:
如何转换列表:
'(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)

进入列表列表:
'((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15))

根据到目前为止提供的答案,这是我想出的:

首先定义函数以从列表的开头最多占用“n”个元素:

(define (take-up-to n xs)
  (define (iter xs n taken)
    (cond
      [(or (zero? n) (empty? xs)) (reverse taken)]
      [else (iter (cdr xs) (- n 1) (cons (car xs) taken))]))
  (iter xs n '()))

第二个是列表其余部分的类似功能:

(define (drop-up-to n xs)
  (define (iter xs n taken)
    (cond
      [(or (zero? n) (empty? xs)) xs]
      [else (iter (cdr xs) (- n 1) (cons (car xs) taken))]))
  (iter xs n '()))

这可以作为一个返回两个值的函数来完成,而 Racket 有一个产生相同结果的函数“split-at”,但我这样做是为了练习。

ps。这是正确使用尾递归吗?

分割成块可以这样写:

(define (split-into-chunks n xs)
  (if (null? xs)
      '()
      (let ((first-chunk (take-up-to n xs))
            (rest-of-list (drop-up-to n xs)))
        (cons first-chunk (split-into-chunks n rest-of-list)))))

pps。可以进一步改进还是“足够好”?

【问题讨论】:

  • 您需要更具体:列表的数量或每个列表的(近似)大小是如何确定的?
  • 由于我是 Racket 的新手,我对遍历列表和递归背后的一般原则更感兴趣,但我现在真正需要的是:我想制作 4 字节块的列表长度总是 4 的倍数的大字节列表(因此没有“剩余”)。
  • 块是 RGBA 值。块数约为 100.000 如果有比列表列表更好的结构,例如这样,请随时推荐!向量,字节可能?
  • 可能会有更好的数据结构,是的。哪个最好取决于您希望用它做什么,但是向量是一个候选者,字节字符串是另一个候选者;您还可以使用 Racket 的记录类型支持将每个像素表示为记录,并具有像素列表或向量。

标签: list scheme racket


【解决方案1】:

Scheme 中有一个通用的实用函数,在 SRFI-1 library(Racket 提供,但我不记得如何导入它)中,称为 take,它从列表中获取初始的 n 元素:

(take 4 '(0 1 2 3 4 5 6 7 8))
=> '(0 1 2 3)

在同一个库中还有一个名为 drop 的函数,它从列表中删除初始的 n 元素:

(drop 4 '(0 1 2 3 4 5 6 7 8))
=> '(4 5 6 7 8)

您可以使用这些函数将问题分解为更小的部分。因此,解决您的问题的第一个(但不正确)近似值是:

(define (split-into-chunks n xs)
  (if (null? xs)
      '()
      (let ((first-chunk (take n xs))
            (rest (drop n xs)))
        (cons first-chunk (split-into-chunks n rest)))))

然而,正如我所指出的,这个解决方案不是最理想的。为什么?因为当列表xs 的元素少于n 时,(take n xs) 会给您一个错误;翻译成你的问题,如果列表有一个非n 多个元素,你会得到一个错误。但是,您可以通过编写一对函数来解决这个问题,take-up-todrop-up-to,它们的工作方式类似于 takedrop,但没有这个问题。因此,函数的示例用法如下所示:

(take-up-to 4 '(0 1 2))
=> '(0 1 2)

(drop-up-to 4 '(0 1 2))
=> '()

这就是我要告诉你的。我建议你做这些事情:

  • 编写您自己的takedroptake-up-todrop-up-to 的实现,并使用它们来编写您要实现的功能。
  • 浏览the documentation for the SRFI-1 library 并熟悉其中的功能。许多这些列表问题都分解为这些功能的简单组合,因此您想了解它们。
  • 了解如何将此库导入 Racket。 (不能帮你。)
  • 作为一个练习,尝试自己编写一些 SRFI-1 函数的实现。 (为了练习,可以稍微简化一下它们;例如,虽然其中许多函数将处理多个列表参数,但为了练习目的,编写一个只处理一个列表的版本是可以的。)

编辑:这是take-up-to的简单实现:

(define (take-up-to n xs)
  (if (or (zero? n) (null? xs))
      '()
      (cons (car xs) (take-up-to (- n 1) (cdr xs)))))

还有可能对此进行改进以仅使用尾调用(因此在恒定空间中运行)。剩下的就是另一个练习了。

【讨论】:

  • 谢谢!花了一些时间制作这些功能,但现在我明白了更多:) 将添加我的答案...
  • 这是我对这些函数的看法:` (define (take-up-to xs n) ` (if (
  • 它们似乎可以工作,但是与没有检查的简单函数相比,拆分大列表会持续很长时间。
  • @PopMilo 是的,这不是take-up-to 的良好实现;获取大列表的长度是昂贵的。我编辑了响应以添加更好的响应(但仍然不是最佳的)。
  • 我有一个模糊的想法,如何计算每个周期还有多少元素可以“抓取”,但昨晚无法弄清楚。它很简单'(function (- n 1) ...' 很好的例子,谢谢!ps。必须阅读更多内容:)
【解决方案2】:

对我来说是这样的

(define (split-by lst n)
   (if (not (empty? lst))
       (cons (take lst n) (split-by (drop lst n) n))
       '() ))

例如

(split-by '(3 2 1 43 54 25 100 -14 -42) 3)

产量

'((3 2 1) (43 54 25) (100 -14 -42))

【讨论】:

  • 看起来不错,并且适用于多个 n 长的列表。谢谢!
  • 优雅的递归解决方案。
【解决方案3】:

你问了一个很好的通用问题,但我认为你在这里想要的是使用字节字符串而不是列表的东西。这是一些代码(包括错误检查),以及一个测试用例:

#lang racket

(require rackunit)

;; given a byte string, split it into a vector of byte-strings
;; of length 4
(define (split-bytes bytes)
  (define len (bytes-length bytes))
  (unless (= 0 (modulo len 4))
    (raise-type-error 'split-bytes
                      "byte string of length divisible by 4"
                      0 bytes))
  (for/vector ([i (in-range (/ len 4))])
     (subbytes bytes (* 4 i) (* 4 (add1 i)))))

(check-equal?
 (split-bytes
  #"hhoh.h9ew,n49othsv97")
 (vector #"hhoh"
         #".h9e"
         #"w,n4"
         #"9oth"
         #"sv97"))

【讨论】:

  • 自我评论:确实,如果您只想遍历子字节字符串,您可能只想创建一个生成器,并避免创建向量的开销。不过,创建一个长度为 100K 的向量可能没什么大不了的。
  • 是的,字节串是我数据的原始类型(来自位图),我熟悉“子字节”功能(用它制作“获取像素”)。你对'for'的方法对我来说更熟悉(我对Racket很陌生,我什至不知道有一个'add1'函数:))。将不得不检查发电机。谢谢!
【解决方案4】:

另一种方法是提供一个高阶函数 map-n,它从列表中获取 n 个值并将函数应用于它们:

(define (map-n n fn l . lists)
  (if (any (lambda(l)(< (length l) n)) (cons l lists))
      '()
      (cons (apply fn (append-map (lambda(l)(take l n)) (cons l lists)))
            (apply map-n n fn (map (lambda(l)(drop l n)) (cons l lists))))))

(e.g. (map-n 4 + '(1 2 3 4 5 6 7 8)) ===> (10 26))
(e.g. (map-n 3 (lambda (a b c) a) '(1 2 3 4 5 6)) ===> (1 4))

有了这个功能,就可以很简单了

(define (split-by n l)
  (map-n n list l))

缺点可能是如果列表的长度不能被 n 整除,则剩余部分将从结果中被拒绝。

另一种时髦的方法是创建一个函数,将列表拆分为任意大小的块:

(define (chunk-list l . chunk-sizes)
  (assert (and (list? l)
               (every natual? chunk-sizes)
               (<= (fold + 0 chunk-sizes) (length l))))
  (let loop ((result '())
             (sizes chunk-sizes)
             (rest l))
    (match sizes
      (()
       (reverse result))
      ((size . sizes)
       (let-values (((this rest) (split-at rest size)))
         (loop `(,this ,@result) sizes rest))))))

(e.g. (chunk-list '(1 2 3 4 5 6 7 8 9 10) 0 1 2 3 4)
      ===> (() (1) (2 3) (4 5 6) (7 8 9 10))

然后使用 make-list 定义 split-by:

(define (split-by n l)
  (let ((size (quotient (length l) n)))
    (apply chunk-list l (make-list size n))))

注意map-n 的定义使用了srfi-1 中的any 函数,而chunk-list 使用了Alex Shinn 的pattern matcher(尽管它可以很容易地使用普通的if、eq?、car 和cdr 重写)

【讨论】:

    【解决方案5】:

    如果您正在寻找尾递归解决方案,一种方法是使用 named let

    (define (group n xs)
      (let loop ([grouped '()] [xs xs])
        (if (empty? xs)
            (reverse grouped)
            (loop (cons (take xs n) grouped)
                  (drop xs n)))))
    

    但是,如果 xs 有剩余元素,这将失败,所以我们需要添加一个案例来检查这个:

    (define (group n xs)
      (let loop ([grouped '()] [xs xs])
        (cond
          [(empty? xs)
           (reverse grouped)]
          [(<= (length xs) n)
           (loop (cons xs grouped) '())]
          [else (loop (cons (take xs n) grouped)
                      (drop xs n))])))
    

    这可行,但我们可以做得更好。这里的问题是计算(length xs) 需要线性时间,因为找到简单列表长度的唯一方法是遍历整个列表。由于这是在一个循环中运行的次数与xs 的大小成正比,因此此代码在二次时间中运行,而在线性时间内完成它应该很简单。我们可以通过计算一次xs 的长度然后在每次迭代中减去n 来规避这个问题:

    (define (group n xs)
      (let loop ([grouped '()] [xs xs] [l (length xs)])
        (cond
          [(empty? xs)
           (reverse grouped)]
          [(<= l n)
           (loop (cons xs grouped) '() 0)]
          [else (loop (cons (take xs n) grouped)
                      (drop xs n)
                      (- l n))])))
    

    我们又回到了线性时间,同时仍然保留尾递归,因此保留了恒定空间。然而,我们还可以进行另一项改进。 Racket 函数split-at 结合了takedrop 的功能,并将两个列表作为两个值返回。要使用多值返回函数,我们可以使用let-values

    (define (group n xs)
      (let loop ([grouped '()] [xs xs] [l (length xs])
        (cond
          [(empty? xs)
           (reverse grouped)]
          [(<= l n)
           (loop (cons xs grouped) '() 0)]
          [else (let-values ([(taken dropped) (split-at xs n)])
                  (loop (cons taken grouped)
                        dropped
                        (- l n)))])))
    

    这稍微快一些,因为split-at 可以避免重复忽略其功能的drop 部分中的第一个n 元素的工作,因为这些元素已被take 使用。但是,此代码不考虑错误的用户输入。如果用户提供的n 大于xs 的长度,则当它应该返回(list xs) 时会抛出错误。这很容易检查,但是由于所有这些嵌套,我们的代码向右扩展得非常厉害。除了检查这一点,我们还可以将循环拆分成一个内部定义的函数:

    (define (group n xs)
      (define (loop grouped xs l)
        (cond
          [(empty? xs)
           (reverse grouped)]
          [(<= l n)
           (loop (cons xs grouped) '() 0)]
          [else (let-values ([(taken dropped) (split-at xs n)])
                  (loop (cons taken grouped)
                        dropped
                        (- l n)))]))
      (let ([l (length xs)])
        (if (>= n l)
            (list xs)
            (loop '() xs l))))
    

    此函数是尾递归的,不会多次计算(length xs),确保(group 4 '(1 2 3)) 计算结果为'((1 2 3)),并且不均匀分组使得(group 2 '(1 2 3) 计算结果为'((1 2) (3)),以线性时间和常数运行空间。

    【讨论】:

    • 感谢您提供详细示例!今天学到了新东西:)
    【解决方案6】:

    如果您想更好地解决此类问题,我会强烈推荐The Little Schemer。它将训练您以使这些问题易于处理的方式进行思考,并且只需要几个小时即可完成,从头到尾。

    【讨论】:

    • 感谢您的建议!以前听说过,今晚开始读。我喜欢你的说法:“训练你思考……”。
    【解决方案7】:

    在我使用 Racket 一段时间后,我不禁注意到 let 的第二次用法记录在其 official doc 中,在其世界中非常普遍。

    所以我对这个问题的方法是:

    (let loop ([lst '(1 2 3 4 5 6 7 8 9 10 1 12 13 14)]
               [result '()])
      (if (< (length lst) 3)
          (append result (list lst))
          (loop (drop lst 3)
                (append  result (list (take lst 3))))))
    

    这里的秘密是,您可以将此代码设置为如下格式:

    (let loop ([lst '(...)]
               [result '()])
      (if (empty? lst)
          result
          (loop (... lst ...)
                (append result (... lst ...)))))
    

    然后在遇到似乎可以通过循环解决的问题时尝试充实它。

    另外,为了您的信息,有人已经将其实现为一个名为slice-at 的函数。它的实现代码可以在here找到。

    【讨论】:

      猜你喜欢
      • 2014-02-16
      • 1970-01-01
      • 1970-01-01
      • 2021-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-09
      相关资源
      最近更新 更多