希尔伯特曲线分析

5

我刚开始学习Python,不理解在函数内调用相同的函数有什么作用?

以下是一个例子:

import turtle
from turtle import left, right, forward

size = 10

def hilbert(level, angle):
    if level == 0:
        return

    turtle.color("Blue")
    turtle.speed("Fastest")

    right(angle)
    hilbert(level - 1, -angle)
    forward(size)
    left(angle)
    hilbert(level - 1, angle)
    forward(size)
    hilbert(level - 1, angle)
    left(angle)
    forward(size)
    hilbert(level - 1, -angle)
    right(angle)

这个到底是怎么工作的?

谢谢。


1
酷程序!你从哪里得到这个想法的? - machine yearning
我正在学习分形图形,因为我需要在一个名为4NEC2的开源软件中生成它们。我找到的Python代码是在线上发现的。 - ElectroNerd
能提供一个链接吗?我很想自己测试一下,未来看到这篇帖子的人可能会觉得它很有参考价值。 - machine yearning
2
@机器学习:哈哈——我刚发现——turtle标准库的一部分。它附带了演示脚本。希尔伯特曲线是该模块附带的演示之一。(在我的系统上,实现在/usr/share/doc/python2.6/examples/Demo/turtle/tdemo_fractalcurves.py中。) - unutbu
@unutbu: 哈哈哈哈哈哈哈,你真是让我笑翻了!现在我可以安心地去睡觉了,心满意足的。 - machine yearning
显示剩余2条评论
6个回答

3

我无法接受递归之美是无法理解它的事实,也无法接受自己可能无法理解它的事实!所以在咨询朋友后,我最终意识到了堆栈的工作原理。如果 level = 2 and angle = 90°,以下是我的等效代码,用于空间填充希尔伯特曲线:

import turtle
from turtle import left, right, forward

size = 10
angle = 90

turtle.hideturtle()
turtle.color("Blue")
turtle.speed("Fastest")

''' Assume we have two levels. '''

right(angle)

# 1st hilbert call
right(-angle)
forward(size)
left(-angle)
forward(size)
left(-angle)
forward(size)
right(-angle)

# Continue from first call of hilbert
forward(size)
left(angle)

# 2nd hilbert call
right(angle)
forward(size)
left(angle)
forward(size)
left(angle)
forward(size)
right(angle)

# Continue from second call of hilbert
forward(size)

# 3rd hilbert call
right(angle)
forward(size)
left(angle)
forward(size)
left(angle)
forward(size)
right(angle)

# Continue from third call of hilbert
left(angle)
forward(size)

# 4th call of hilbert
right(-angle)
forward(size)
left(-angle)
forward(size)
left(-angle)
forward(size)
right(-angle)

# Continue from fourth call of hilbert
right(angle)

Eureka!


2

level等于0时,hilbert(level,angle)什么也不做。

现在考虑当level等于1时会发生什么:调用hilbert(1,angle)将执行以下语句:

turtle.color("Blue")
turtle.speed("Fastest")
right(angle)
forward(size)
left(angle)
forward(size)
left(angle)
forward(size)
right(angle)

我觉得这可能是画了一个正方形的三条边。

hilbert(level-1,...)语句已被删除,因为level-1等于0,并且我们已经确定hilbert(0,...)什么也不做。

现在,考虑一下调用hilbert(1,-angle)会发生什么。 接下来,想想当level等于2时会发生什么。希望这能给你一个思路。

顺便说一下Python很棒的地方——你可以交互式地运行程序来可视化调用hilbert(1,angle)hilbert(2,angle)等所做的操作...


顺便说一句,我发现在函数外部设置turtle.color("Blue")和turtle.speed("Fastest")更有效率。 - ElectroNerd
我不理解 level = 2 的顺序。这是我的语句:right(angle) right(-angle) forward(size) left(-angle) forward(size) left(-angle) forward(size) right(-angle) forward(size) left(-angle) forward(size) left(-angle) forward(size) right(-angle) - ElectroNerd
@ElectroNerd:哈哈,你很少能够理解递归过程的每个层次,这就是它的美妙之处!一个简单的问题很快就会演变成越来越复杂的统一和分形逻辑混乱;最重要的是要理解你的问题,并且从给定的层次出发,了解如何进入下一个层次。 - machine yearning

1

正如@Tom Zych和其他人所提到的,这是一个叫做递归的问题,一开始可能很难理解(有时需要用与平常完全不同的方式思考问题)。

最经典且易于理解(但不一定有用或强大)的递归示例是阶乘生成器。给定一个正整数nn的阶乘等于小于或等于n的所有正整数的乘积。我们将n的阶乘写为n!

n! = n * (n-1) * (n-2) * ... * 3 * 2 * 1

现在,通过递归,我们想看看“这里重复并随着我尝试解决更大的问题而变得更简单的模式是什么?”例如,让我们尝试5!

5! = 5 * 4 * 3 * 2 * 1

在解这个方程时,我们要做的第一件事是什么? 我们取5并将其乘以某些东西。 这非常完美,因为如果我们有一个阶乘函数fact(n),我们可能会将n作为其参数:
def fact(n):
    return n * #something

现在只需要弄清楚那个#something是什么。但我们刚刚已经写过它了:

5! = 5 * (4 * 3 * 2 * 1)

但是我们的新#something如何成为一个较小的问题,而且与原问题本质上相同? 看吧,这就是递归的魔力:

5! = 5 * 4 * 3 * 2 * 1
   = 5 * 4!
4! = 4 * 3 * 2 * 1
   = 4 * 3!
3! = 3 * 2 * 1
   = 3 * 2!
2! = 2 * 1
   = 2 * 1!
1! = ???

正如您所看到的,对于大多数给定的n

n!= n *(n-1)!

我们已经接近成功了,现在我们已经发现了我们的模式,我们需要担心的是例外情况,即最简单的情况(当n = 1时,没有其他可以相乘的数字)。在递归中,我们称之为基本情况,幸运的是,我们知道在这种基本情况下应该怎么做:1!= 1。因此,在总结我们的逻辑时,我们可以编写我们的递归阶乘函数:

def fact(n):
    # Take care of the base case first
    if n == 1:
        return 1
    # Then break down the larger case into its recursive sub-problems
    else:
        return n * fact(n-1)

你的Hilburtle程序稍微有点复杂,但如果你了解一些Hilbert曲线分析的知识,并且开始理解递归程序逻辑,你应该能够弄清楚它是如何工作的。祝你好运!


1

这是一种强大的编程技术,称为递归,它通过将问题分解成类似但更小的问题来解决问题,直到达到一个足够小而容易解决的点。


谢谢Tom,我一定会特别研究那个术语。 - ElectroNerd

0

这被称为递归。递归函数通常解决小问题,但也有能力将较大的问题分解成更容易解决的小问题。

在你的例子中,程序看起来可能是一些路径穷尽程序。


0

其实它相当简单。一般来说,略去几个(虽然对于这个讨论很重要,但不在此讨论范围内)例外情况:

  • 函数是一系列操作数据的指令列表。
  • 其中的一部分数据是“堆栈” - 一块内存块。
  • 函数用“堆栈指针”跟踪正在使用的堆栈部分。
  • 当调用函数时,堆栈指针会移动,并且指令会作用于当前的堆栈指针(可能将其进一步移动)。
  • 当函数完成(返回)时,指针会后移。
  • 当函数调用另一个函数时,它会像以前一样移动堆栈指针。
  • 因此,当函数调用自身时,没有任何区别:函数指针会移动,然后执行该函数。

换句话说,计算机并不真正关心您是否调用不同的函数或递归调用相同的函数。事实上,它甚至可以优化递归函数,因为它知道每次会发生相同的事情,就像 for 循环。


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