我一直在玩J中lookandsay(OEIS A005150)的实现。我制作了两个版本,都非常简单,使用while.
类型的控制结构。一个循环,另一个循环。因为我有强迫症,我开始对版本进行比较计时。
看,说是序列1 11 21 1211 111221,一个一,两个一,等等
对于列表的早期元素(最多20个左右),循环版本获胜,但仅以很小的数量获胜。时间在30左右会导致递归版本获胜,如果堆栈空间足以支持递归版本,那么递归版本可能会更受欢迎。我研究了原因,我认为这与处理中间结果有关。序列中的第30个数字有5808位数字。(第32位,9898位,第34位,16774位。)
当您使用递归处理问题时,您可以将中间结果保存在递归调用中,并且最后的拆堆会构建结果,以便对结果的处理最少。
在列表版本中,您需要一个变量来保存结果。每次循环迭代都需要向结果中添加两个元素。
在我看来,问题是我在J中找不到任何方法来修改现存的数组而不完全重新分配它
try. o =. o,e,(0&{y) catch. o =. e,(0&{y) end.
将一个元素放入o中,当我们开始时,o可能没有值。这可能比慢得多
o =. i.0
.
.
.
o =. (,o),e,(0&{y)
关键是,如果没有拉锯,结果会得到错误的形状,或者看起来是这样。它以某种方式继承了i.0的形状。
但是,即使是像}modify这样的函数也不会修改列表,它们会返回一个对其进行了修改的列表,如果你想保存列表,你需要分配它。随着分配列表的大小增加(当你从开始到结束遍历数字以生成下一个数字时),分配似乎需要越来越多的时间。这个赋值实际上是我能看到的唯一一个东西,它会使元素329988位数字在递归版本中花费更少的时间,而元素20(408位数字)在循环版本中花费较少的时间。
递归版本使用构建返回
e,(0&{y),(,lookandsay e }. y)
上面的行既是函数的返回行,也是递归的返回行。因此,当调用到达字符串的末尾,所有内容都不稳定时,整个返回向量会立即构建。
在APL中,我认为人们可以说一些关于的内容
a[1+rho a] <- new element
但当我在NARS2000中尝试此操作时,我发现它会导致索引错误。我没有任何其他APL,我可能还记得APL Plus中的这个成语,我怀疑它在APL\360或APL\1130中是这样起作用的。我可能完全记错了。
我在J中找不到这样做的方法。可能没有办法做到这一点,但下一个想法是预先分配一个可以保存结果的数组,并更改单个条目。我也没有办法做到这一点——也就是说,J似乎不支持APL习语:
a<- iota 5
a[3] <- -1
这是因为语言纯洁而被禁止的副作用之一吗?
解释器是否将a=. a,foo
或其某些变体识别为应该在内部快速路径到a[>:#a]=.foo
的东西?
这是递归版本,只是为了好玩。我尝试了很多不同的版本,我相信程序越长,速度越慢,通常情况下,越复杂,速度就越慢。一般来说,程序可以被链接,这样如果你想要第n个数字,你就可以进行lookandsay^:n]y。我已经尝试了很多优化,但我遇到的问题是,我无法判断我将输出发送到什么环境中。如果我能告诉我要把它发送到程序的下一次迭代,我会把它作为一个数字数组而不是一个大数字发送。
我还怀疑,如果我能想出如何制作一个默认版本的代码,它会运行得更快,因为我发现,当我向代码中添加一些应该使其更短的东西时,它运行得更长。
lookandsay=: 3 : 0
if. 0 = # ,y do. return. end. NB. return on empty argument
if. 1 ~: #@$ y do. NB. convert rank 0 argument to list of digits
y =. (10&#.^:_1) x: y
f =. 1
assert. 1 = #@$ y NB. the converted argument must be rank 1
else.
NB. yw =. y
f =. 0
end.
NB. e should be a count of the digits that match the leading digit.
e=.+/*./y=0&{y
if. f do.
o=. e,(0&{y),(,lookandsay e }. y)
assert. e = 0&{ o
10&#. x: o
return.
else.
e,(0&{y),(,lookandsay e }. y)
return.
end.
)
我对产生的数字的特征很感兴趣。我发现,如果你从1开始,数字永远不会超过3。如果你从一个高于3的数字开始,它将作为一个单例存在,你也可以通过从888888888这样的数字开始在生成的数字中获得一个数字,它将生成一个包含一个9和一个8的数字。但除了单身汉,没有一个数字会超过3。
编辑:我又做了一些测量。我最初编写的程序接受向量或标量,想法是在内部使用向量。我曾想过将一个向量从一层代码传递到另一层代码,但我仍然可能使用左参数来控制代码。当我通过顶级矢量时,代码运行速度会快得多,所以我的猜测是,大部分cpu都被从矢量到数字的超长数字所消耗。递归例程在递归时总是传递一个向量,这可能就是它几乎和循环一样快的原因。
这并不能改变我的问题。
我有一个答案,我三个小时都无法发布。我会把它贴出来的,请不要做大量的研究来回答它。
分配,如
arr=. 'z' 15} arr
执行到位。(有关其他支持的就地操作,请参阅JWiki文章)解释器确定只有arr
的一小部分被更新,并且不创建要重新分配的整个新列表。
在您的情况下,发生的不是数组被重新分配,而是它以小增量多次增长,从而导致内存分配和重新分配。
如果您预先分配(通过给它分配一些大的数据块),那么您可以用}
修改它,而不会受到太多惩罚。
在我问了这个问题之后,老实说,我已经忘记了这个网站。
是的,答案是该语言没有表示"更新到位"的形式,但如果你使用两种形式
x=:x,大多数
或
x=:大多数东西}x
那么解释器将这些识别为特殊的,并在适当的位置进行更新,除非它不能。口译员还可以识别出许多其他特殊功能,如:
199(1000&|@^)199
这种组合运算就是模幂运算。它从不计算整个求幂,因为
199(1000&|^)199
如果没有@,就以_结束。
所以值得一读关于特价商品的文章。我会把别人的答案标记出来。
sverre在上面提供的链接(http://www.jsoftware.com/jwiki/Essays/In-Place%20Operations)显示了支持修改现有数组而不是创建新数组的各种操作。它们包括:
myarray=: myarray,'blah'
如果你对lookandsay序列的默认版本感兴趣,请参阅此提交给RosettaCode:
las=: ,@((# , {.);.1~ 1 , 2 ~:/ ])&.(10x&#.inv)@]^:(1+i.@[)
5 las 1
11 21 1211 111221 312211