欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Think Julia :条件和递归(第五节)

程序员文章站 2022-04-04 09:05:31
...

5.条件和递归
本章的主要内容是if语句,它根据程序的状态执行不同的代码。但首先我要介绍两个新的运算符:floor division和modulus。

floor division和modulus
所述地板除法运算符,÷(\div TAB),分两个数字和回合下来为整数。例如,假设电影的运行时间是105分钟。您可能想知道这是多长时间。常规除法返回一个浮点数:

julia> minutes = 105
105
julia> minutes / 60
1.75

但我们通常不会用小数点写小时。楼层除法返回整数小时数,向下舍入:

julia> hours = minutes ÷ 60
1

要获得剩余部分,您可以在几分钟内减去一小时:

julia> remainder = minutes - hours * 60
45

另一种方法是使用模数运算符,%它将两个数字分开并返回余数。

julia> remainder = minutes % 60
45

模数运算符比看起来更有用。例如,您可以检查一个数字是否可以被另一个整除 - 如果x % y为零,则可以x被整除y。

此外,您可以从数字中提取最右边的数字或数字。例如,x % 10产生x(在基数10中)最右边的数字。同样x % 100产生最后两位数字。

布尔表达式

甲布尔表达式是真或假的表达式。以下示例使用运算符==,该运算符比较两个操作数并生成true它们是否相等,false否则:

julia> 5 == 5
true
julia> 5 == 6
false

true并且false是属于该类型的特殊值Bool; 它们不是字符串:

julia> typeof(true)
Bool
julia> typeof(false)
Bool

该==操作员是关系运算符中的一个; 其他人是:

 x != y               # x is not equal to y
      x ≠ y                # (\ne TAB)
      x > y                # x is greater than y
      x < y                # x is less than y
      x >= y               # x is greater than or equal to y
      x ≥ y                # (\ge TAB)
      x <= y               # x is less than or equal to y
      x ≤ y                # (\le TAB)

警告 尽管您可能对这些操作很熟悉,但Julia符号与数学符号不同。常见的错误是使用单个等号(=)而不是双等号(==)。请记住,这=是一个赋值运算符,==是一个关系运算符。没有这样的东西=<或=>。

逻辑运算符

有三个逻辑运算符:(&&和),||(或)和!(不)。这些运算符的语义(含义)与它们在英语中的含义相似。例如,x > 0 && x < 10仅当x大于0 且小于时才为真10。

n % 2 == 0 || n % 3 == 0如果其中一个或两个条件为真,即如果数字可被2 或 3 整除,则为true 。

两者都&&与||右侧相关联,但&&具有更高的优先级||。

最后,!运算符否定布尔表达式,!(x > y)如果x > y为false则为true,即if x小于或等于y。

条件执行

为了编写有用的程序,我们几乎总是需要能够检查条件并相应地改变程序的行为。条件语句赋予我们这种能力。最简单的形式是if声明:

if x > 0
    println("x is positive")
end

后面的布尔表达式if称为条件。如果为true,则缩进语句运行。如果没有,没有任何反应。

if语句与函数定义具有相同的结构:标题后跟以关键字终止的主体end。像这样的语句称为复合语句。

可以在正文中出现的语句数量没有限制。有时候,拥有一个没有语句的正文很有用(通常作为你尚未编写的代码的管理员)。

if x < 0
    # TODO: need to handle negative values!
end

替代执行

该if陈述的第二种形式是“替代执行”,其中有两种可能性,条件决定哪一种运行。语法如下所示:

if x % 2 == 0
    println("x is even")
else
    println("x is odd")
end

如果余数x除以2是0,那么我们知道它x是偶数,并且程序显示适当的消息。如果条件为false,则运行第二组语句。由于条件必须为true或false,因此将运行其中一个备选方案。替代方案称为分支,因为它们是执行流程中的分支。

链式条件

有时候有两种以上的可能性,我们需要两个以上的分支。表达这样的计算的一种方法是链式条件:

if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x and y are equal")
end

同样,只有一个分支将运行。elseif声明数量没有限制。如果有一个else条款,它必须在最后,但不一定是一个。

if choice == "a"
    draw_a()
elseif choice == "b"
    draw_b()
elseif choice == "c"
    draw_c()
end

按顺序检查每个条件。如果第一个为假,则检查下一个,依此类推。如果其中一个为真,则相应的分支运行并且语句结束。即使多个条件为真,也只有第一个真分支运行。

嵌套条件

一个条件也可以嵌套在另一个条件中。我们可以像上面这样在上一节中编写示例:

if x == y
    println("x and y are equal")
else
    if x < y
        println("x is less than y")
    else
        println("x is greater than y")
    end
end

外部条件包含两个分支。第一个分支包含一个简单的语句。第二个分支包含另一个if语句,它有两个自己的分支。这两个分支都是简单的语句,尽管它们也可以是条件语句。

尽管语句的非强制缩进使得结构明显,但嵌套条件很难很快阅读。尽可能避免使用它们。(((缩进)

逻辑运算符通常提供简化嵌套条件语句的方法。例如,我们可以使用单个条件重写以下代码:

if 0 < x
    if x < 10
        println("x is a positive single-digit number.")
    end
end

该print语句仅在我们通过两个条件时运行,因此我们可以与&&运算符获得相同的效果:

if 0 < x && x < 10
    println("x is a positive single-digit number.")
end

对于这种情况,Julia提供了更简洁的选择:

if 0 < x < 10
    println("x is a positive single-digit number.")
end

递归

一个函数调用另一个函数是合法的; 一个函数调用自身也是合法的。可能并不明显为什么这是一件好事,但事实证明这是程序可以做的最神奇的事情之一。例如,查看以下函数:

function countdown(n)
    if n ≤ 0
        println("Blastoff!")
    else
        print(n, " ")
        countdown(n-1)
    end
end

如果n为0或负数,则输出单词; “Blastoff!”否则,输出n,然后调用名为countdown-itself-passing 的函数n-1作为参数。

如果我们这样调用这个函数怎么办?

julia> countdown(3)
3 2 1 Blastoff!

执行countdown开始于n = 3,并且因为n大于0,它输出值3,然后调用自身……

执行countdown开始于n = 2,并且因为n大于0,它输出值2,然后调用自身……

执行countdown开始于n = 1,并且因为n大于0,它输出值1,然后调用自身……

执行countdown开始于n = 0,并且n从不大于0,它输出单词,”Blastoff!”然后返回。

倒计时n = 1返回。

倒计时n = 2返回。

倒计时n = 3返回。

然后你回来了main

调用自身的函数是递归的 ; 执行它的过程称为递归。

另一个例子,我们可以编写一个打印字符串n的函数 倍。

function printn(s, n)
    if n ≤ 0
        return
    end
    println(s)
    printn(s, n-1)
end

如果n <= 0该return语句退出的功能。执行流程立即返回到调用者,并且函数的其余行不会运行。

该函数的其余部分类似于countdown:它显示s然后调用自身显示n - 1s 额外的时间。所以输出行数是1 + (n - 1 ),总计n。

对于像这样的简单示例,使用for循环可能更容易。但是我们稍后会看到很难用for循环编写并且易于使用递归编写的示例,因此最好早点开始。

递归函数的堆栈图

在Stack Diagrams中,我们使用堆栈图来表示函数调用期间程序的状态。相同类型的图可以帮助解释递归函数。

每次调用函数时,Julia都会创建一个包含函数局部变量和参数的框架。对于递归函数,堆栈上可能同时存在多个帧。

Think Julia :条件和递归(第五节)

像往常一样,堆栈的顶部是框架main。它是空的,因为我们没有在其中创建任何变量main或传递任何参数。

四个countdown帧具有不同的参数值n。堆栈的底部,其中n = 0,称为基本情况。它不会进行递归调用,因此不再有帧。

小费 作为练习,绘制一个用于和printn调用的堆栈图。然后编写一个函数,它接受一个函数对象和一个数字,作为参数,并调用给定的函数ns = “Hello”n = 2do_nn 倍。

无限递归

如果递归永远不会达到基本情况,它将继续进行递归调用,并且程序永远不会终止。这被称为无限递归,通常不是一个好主意。这是一个带有无限递归的最小程序:

function recurse()
    recurse()
end

在大多数编程环境中,具有无限递归的程序并不真正永远运行。当达到最大递归深度时,Julia会报告错误消息:

julia> recurse()
ERROR: *Error:
Stacktrace:
 [1] recurse() at ./REPL[1]:2 (repeats 80000 times)

这个堆栈跟踪比我们在前一章中看到的要大一些。发生错误时,recurse堆栈上有80000 帧!

如果您偶然遇到无限递归,请检查您的函数以确认存在不进行递归调用的基本情况。如果有基本情况,请检查您是否确保达到它。

键盘输入

到目前为止我们编写的程序不接受用户的输入。他们每次都做同样的事情。

Julia提供了一个内置函数readline,用于停止程序并等待用户输入内容。当用户按下RETURN或时ENTER,程序恢复并readline返回用户键入的字符串。

julia> text = readline()
What are you waiting for?
"What are you waiting for?"

在从用户那里获得输入之前,最好打印一个告诉用户输入内容的提示:

julia> print("What...is your name? "); readline()
What...is your name? Arthur, King of the Britons!
"Arthur, King of the Britons!"

分号;允许在同一行上放置多个语句。在REPL中,只有最后一个语句返回其值。

如果您希望用户键入一个整数,您可以尝试将返回值转换为Int64:

julia> println("What...is the airspeed velocity of an unladen swallow?"); speed = readline()
What...is the airspeed velocity of an unladen swallow?
42
"42"
julia> parse(Int64, speed)
42

但是,如果用户键入除数字字符串之外的其他内容,则会出现错误:

julia> println("What...is the airspeed velocity of an unladen swallow? "); speed = readline()
What...is the airspeed velocity of an unladen swallow?
What do you mean, an African or a European swallow?
"What do you mean, an African or a European swallow?"
julia> parse(Int64, speed)
ERROR: ArgumentError: invalid base 10 digit 'W' in "What do you mean, an African or a European swallow?"
[...]

我们稍后会看到如何处理这种错误。

调试

当发生语法或运行时错误时,错误消息包含大量信息,但它可能是压倒性的。最有用的部分通常是:

这是什么样的错误,以及

它发生的地方。

语法错误通常很容易找到,但有一些问题。通常,错误消息指示发现问题的位置,但实际错误可能在代码中较早,有时在前一行。

运行时错误也是如此。假设您正在尝试以分贝计算信噪比。公式是

Think Julia :条件和递归(第五节)

在朱莉娅,你可能会写这样的东西:

signal_power = 9
noise_power = 10
ratio = signal_power ÷ noise_power
decibels = 10 * log10(ratio)
print(decibels)

你得到:

-Inf

这不是您期望的结果。

为了找到错误,打印比率值可能是有用的,结果是0。问题在第3行,它使用地板划分而不是浮点除法。

警告 您应该花时间仔细阅读错误消息,但不要假设他们说的一切都是正确的。

词汇表

地板部门
表示的运算符,÷它将两个数字和向下舍入(向负无穷大)舍入为整数。

模数算子
一个运算符,用百分号(%)表示,对整数有效,当一个数字除以另一个数时返回余数。

布尔表达式
其值的表达式是任一true或false。

关系运算符
一个比较它的操作数的运算符:==,≠(!=), ,>,<(≥),>=和≤(⇐)。

逻辑运算符
结合布尔表达式的运算符之一:( &&和),||(或)和!(不)。

条件陈述
根据某些条件控制执行流程的语句。

条件
条件语句中的布尔表达式,用于确定运行哪个分支。

复合陈述
由标题和正文组成的语句。正文以关键字终止end。


条件语句中的一个替代语句序列。

有条件的
带有一系列替代分支的条件语句。

嵌套条件
条件语句,出现在另一个条件语句的一个分支中。

退货声明
导致函数立即结束并返回调用者的语句。

递归
调用当前正在执行的函数的过程。

基本情况
递归函数中的条件分支,不进行递归调用。

无限递归
递归,没有基本情况,或永远不会到达它。最终,无限递归会导致运行时错误。

演习
练习5-1
该函数time在“纪元”中返回当前的格林威治标准时间,这是用作参考点的任意时间。在UNIX系统上,时代是1970年1月1日。

julia> time()
1.536693351511752e9

编写一个脚本,读取当前时间并将其转换为小时,分钟和秒的时间,以及自纪元以来的天数。

练习5-2
费马的最后定理说没有正整数a,b和c 这样的
Think Julia :条件和递归(第五节)

对于任何n值 大于2。

编写一个名为的函数checkfermat,它接受四个参数a- b,c和n- 并检查Fermat定理是否成立。如果n大于2并且a^n + b^n == c^n程序应该打印,“圣洁抽烟,费马错了!”否则程序应该打印,“不,这不起作用。”

编写提示用户输入值的函数a,b,c和n,将它们转换为整数,并使用checkfermat以检查其是否违反了费马大定理。

练习5-3
如果给你三根棍子,你可能会或可能不会将它们排成三角形。例如,如果其中一根棍子长12英寸而另外两根长度为1英寸,那么你将无法让短棍在中间相遇。对于任何三种长度,都有一个简单的测试,看看是否可以形成三角形:

小费
如果三个长度中的任何一个长度大于另外两个长度的总和,那么就不能形成三角形。否则,你可以。(如果两个长度的总和等于第三个长度,则它们形成所谓的“退化”三角形。)

编写一个名为的函数istriangle,它将三个整数作为参数,并打印“是”或“否”,具体取决于您是否可以使用给定长度的棒形成三角形。

编写一个函数,提示用户输入三个棒长度,将它们转换为整数,并用于istriangle检查具有给定长度的棒是否可以形成三角形。

练习5-4
以下程序的输出是什么?绘制一个堆栈图,显示打印结果时程序的状态。

function recurse(n, s)
    if n == 0
        println(s)
    else
        recurse(n-1, n+s)
    end
end

recurse(3, 0)

如果你这样调用这个函数会发生什么:recurse(-1, 0)?

编写一个文档字符串,解释某人为了使用这个功能而需要知道的一切(而不是别的)。

以下练习使用案例研究:界面设计中ThinkJulia描述的模块:

练习5-5
阅读以下函数,看看是否可以弄清楚它的作用(参见案例研究中的示例:界面设计)。然后运行它,看看你是否正确。

function draw(t, length, n)
    if n == 0
        return
    end
    angle = 50
    forward(t, length*n)
    turn(t, -angle)
    draw(t, length, n-1)
    furn(t, 2*angle)
    draw(t, length, n-1)
    turn(t, -angle)
    forward(-length*n)
end

练习5-6
Think Julia :条件和递归(第五节)

Koch曲线是一个看起来像A Koch曲线的分形。绘制长度为x的Koch曲线你要做的就是

  1. 绘制长度为x的Koch曲线3。
  2. 向左转60度。
  3. 绘制长度为x的Koch曲线3。
  4. 向右转120度。
  5. 绘制长度为x的Koch曲线3。
  6. 向左转60度。
  7. 绘制长度为x的Koch曲线3。

例外情况是x小于3:在这种情况下,你可以画一条长度为x的直线。

  1. 编写一个函数koch,该函数将乌龟和长度作为参数,并使用乌龟绘制具有给定长度的Koch曲线。
  2. 编写一个函数snowflake,绘制三条Koch曲线,绘制雪花轮廓。
  3. Koch曲线可以通过多种方式推广。有关示例,请参阅https://en.wikipedia.org/wiki/Koch_snowflake并实现您的最爱。