如何在 Racket 中制作一个列表,您可以按照添加元素的相同顺序进行迭代?
How to make a list in Racket that you can iterate in the same order you add elements?
这是一个非常常见的编程用例,用于构建事物列表,然后需要按照您添加到列表中的相同顺序遍历列表。一个简单的例子可能是记录编译器错误然后为用户打印它们。您希望源代码中较早的错误首先被解析,成为最先打印到屏幕上的错误。
但是在Lisp/Scheme/Racket列表中只有头指针,没有尾指针。这意味着您只能廉价地向开头添加元素,并且只能廉价地以与添加它们的顺序相反的顺序迭代元素。在学习 Racket 的过程中,我看到了大量构建列表然后迭代 (reverse the-list)
的代码。在实践中,对于很多应用程序来说,这应该没问题,但每次出现这种情况时都必须向算法添加额外的 N 操作似乎有点愚蠢。
有解决这个问题的standard-idiom/most-common-solution吗?我总是可以用尾指针滚动我自己的列表类型,或者在 Racket 的可变向量之上重新实现 C++ std::vector,但这似乎很常见,应该有一个已经建立的最佳实践来做什么。
我想不出用普通 Racket 的列表来做这件事的任何方法。
有一个有效的替代方案,看看 Racket 的队列:https://docs.racket-lang.org/functional-data-structures/Queues.html
Banker's Queue 为入队、头和尾提供了分摊的 O(1) 时间,根据您的用例,这些是您需要的功能。
更新:有几个队列适用于您的场景,@ben-rudgers 在评论中提到的另一个是命令式队列:https://docs.racket-lang.org/data/Imperative_Queues.html
那个也为 enqueue!
和 dequeue!
提供恒定时间。
我并不完全熟悉 C++ 中的 std::vector,但我相信 Racket 的可增长向量非常相似,并且可以用于此。它们可以通过添加 (require data/gvector)
从 data-lib 包中导入:https://docs.racket-lang.org/data/gvector.html
你知道他们怎么说
The real problem is that programmers have spent far too much time
worrying about efficiency in the wrong places and at the wrong times;
premature optimization is the root of all evil (or at least most of
it) in programming.
'they' 我指的是 Donald Knuth 和 Tony Hoare。
比较队列实现
下面的代码 运行s #lang racket
's Bankers' 队列、命令式队列和一个普通的旧列表被反转。这就是它们 运行 的顺序。每个 运行 都在计时函数中。 'major
垃圾收集是在每个 运行.
之前在计时函数之外完成的
#lang racket
(require data/queue) ;; the imperative queue
(require (rename-in pfds/queue/bankers
(queue->list bq2list)))
(define (run)
(writeln "bankers' queue for 10,000")
(writeln "imperative queue for 1,000,000")
(writeln "reversed list for 1,000,00")
(collect-garbage 'major)
;; bankers' queue
(time
(define q (queue))
(for ((i (range 10000)))
(enqueue i q))
(bq2list q)
'done)
(collect-garbage 'major)
;; imperative queue
(time
(define q (make-queue))
(for ((i (range 1000000)))
(enqueue! q i))
(queue->list q))
(collect-garbage 'major)
;; reversed list
(time
(define q
(for/list ((i (range 1000000)))
i))
(reverse q))
'done)
(run)
典型输出
Welcome to DrRacket, version 6.6 [3m].
Language: racket, with debugging; memory limit: 1024 MB.
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 1748 real time: 1752 gc time: 1000
cpu time: 664 real time: 664 gc time: 272
cpu time: 436 real time: 436 gc time: 180
'done
> (run)
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 752 real time: 754 gc time: 8
cpu time: 660 real time: 661 gc time: 248
cpu time: 456 real time: 460 gc time: 192
'done
> (run)
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 776 real time: 779 gc time: 40
cpu time: 692 real time: 693 gc time: 256
cpu time: 456 real time: 458 gc time: 184
'done
>
解释结果
惯用的普通旧反向列表往往是最快的[至少在这个天真的实现中]。由于减少了簿记并做了预期的(即惯用的)事情,这并不特别令人惊讶。
命令式队列与简单列表的速度大致相同。 Racket 的实现使用 struct
的。由于这些是 Racket 生态系统中更高级别类型的基本构建块,因此构建在它们之上的数据结构具有高性能也就不足为奇了。如果队列语义很重要,队列抽象可能值得 运行ning 慢一点。
编程的时候有时候会急躁,听说是一种美德,如果因为我的急躁把银行家队列的迭代次数从一百万减少到一万也是美德,那么也许有轶事证据表明它是。无论如何,银行家队列 运行 比其他队列慢两个数量级。当然,速度只是性能的衡量标准之一。线程安全是另一个,速度权衡可能是值得的。
这是一个非常常见的编程用例,用于构建事物列表,然后需要按照您添加到列表中的相同顺序遍历列表。一个简单的例子可能是记录编译器错误然后为用户打印它们。您希望源代码中较早的错误首先被解析,成为最先打印到屏幕上的错误。
但是在Lisp/Scheme/Racket列表中只有头指针,没有尾指针。这意味着您只能廉价地向开头添加元素,并且只能廉价地以与添加它们的顺序相反的顺序迭代元素。在学习 Racket 的过程中,我看到了大量构建列表然后迭代 (reverse the-list)
的代码。在实践中,对于很多应用程序来说,这应该没问题,但每次出现这种情况时都必须向算法添加额外的 N 操作似乎有点愚蠢。
有解决这个问题的standard-idiom/most-common-solution吗?我总是可以用尾指针滚动我自己的列表类型,或者在 Racket 的可变向量之上重新实现 C++ std::vector,但这似乎很常见,应该有一个已经建立的最佳实践来做什么。
我想不出用普通 Racket 的列表来做这件事的任何方法。
有一个有效的替代方案,看看 Racket 的队列:https://docs.racket-lang.org/functional-data-structures/Queues.html
Banker's Queue 为入队、头和尾提供了分摊的 O(1) 时间,根据您的用例,这些是您需要的功能。
更新:有几个队列适用于您的场景,@ben-rudgers 在评论中提到的另一个是命令式队列:https://docs.racket-lang.org/data/Imperative_Queues.html
那个也为 enqueue!
和 dequeue!
提供恒定时间。
我并不完全熟悉 C++ 中的 std::vector,但我相信 Racket 的可增长向量非常相似,并且可以用于此。它们可以通过添加 (require data/gvector)
从 data-lib 包中导入:https://docs.racket-lang.org/data/gvector.html
你知道他们怎么说
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
'they' 我指的是 Donald Knuth 和 Tony Hoare。
比较队列实现
下面的代码 运行s #lang racket
's Bankers' 队列、命令式队列和一个普通的旧列表被反转。这就是它们 运行 的顺序。每个 运行 都在计时函数中。 'major
垃圾收集是在每个 运行.
#lang racket
(require data/queue) ;; the imperative queue
(require (rename-in pfds/queue/bankers
(queue->list bq2list)))
(define (run)
(writeln "bankers' queue for 10,000")
(writeln "imperative queue for 1,000,000")
(writeln "reversed list for 1,000,00")
(collect-garbage 'major)
;; bankers' queue
(time
(define q (queue))
(for ((i (range 10000)))
(enqueue i q))
(bq2list q)
'done)
(collect-garbage 'major)
;; imperative queue
(time
(define q (make-queue))
(for ((i (range 1000000)))
(enqueue! q i))
(queue->list q))
(collect-garbage 'major)
;; reversed list
(time
(define q
(for/list ((i (range 1000000)))
i))
(reverse q))
'done)
(run)
典型输出
Welcome to DrRacket, version 6.6 [3m].
Language: racket, with debugging; memory limit: 1024 MB.
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 1748 real time: 1752 gc time: 1000
cpu time: 664 real time: 664 gc time: 272
cpu time: 436 real time: 436 gc time: 180
'done
> (run)
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 752 real time: 754 gc time: 8
cpu time: 660 real time: 661 gc time: 248
cpu time: 456 real time: 460 gc time: 192
'done
> (run)
"bankers' queue for 10,000"
"imperative queue for 1,000,000"
"reversed list for 1,000,00"
cpu time: 776 real time: 779 gc time: 40
cpu time: 692 real time: 693 gc time: 256
cpu time: 456 real time: 458 gc time: 184
'done
>
解释结果
惯用的普通旧反向列表往往是最快的[至少在这个天真的实现中]。由于减少了簿记并做了预期的(即惯用的)事情,这并不特别令人惊讶。
命令式队列与简单列表的速度大致相同。 Racket 的实现使用
struct
的。由于这些是 Racket 生态系统中更高级别类型的基本构建块,因此构建在它们之上的数据结构具有高性能也就不足为奇了。如果队列语义很重要,队列抽象可能值得 运行ning 慢一点。编程的时候有时候会急躁,听说是一种美德,如果因为我的急躁把银行家队列的迭代次数从一百万减少到一万也是美德,那么也许有轶事证据表明它是。无论如何,银行家队列 运行 比其他队列慢两个数量级。当然,速度只是性能的衡量标准之一。线程安全是另一个,速度权衡可能是值得的。