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都会创建一个包含函数局部变量和参数的框架。对于递归函数,堆栈上可能同时存在多个帧。
像往常一样,堆栈的顶部是框架main。它是空的,因为我们没有在其中创建任何变量main或传递任何参数。
四个countdown帧具有不同的参数值n。堆栈的底部,其中n = 0,称为基本情况。它不会进行递归调用,因此不再有帧。
小费 作为练习,绘制一个用于和printn调用的堆栈图。然后编写一个函数,它接受一个函数对象和一个数字,作为参数,并调用给定的函数ns = “Hello”n = 2do_nn 倍。
无限递归
如果递归永远不会达到基本情况,它将继续进行递归调用,并且程序永远不会终止。这被称为无限递归,通常不是一个好主意。这是一个带有无限递归的最小程序:
function recurse()
recurse()
end
在大多数编程环境中,具有无限递归的程序并不真正永远运行。当达到最大递归深度时,Julia会报告错误消息:
julia> recurse()
ERROR: StackOverflowError:
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?"
[...]
我们稍后会看到如何处理这种错误。
调试
当发生语法或运行时错误时,错误消息包含大量信息,但它可能是压倒性的。最有用的部分通常是:
这是什么样的错误,以及
它发生的地方。
语法错误通常很容易找到,但有一些问题。通常,错误消息指示发现问题的位置,但实际错误可能在代码中较早,有时在前一行。
运行时错误也是如此。假设您正在尝试以分贝计算信噪比。公式是
在朱莉娅,你可能会写这样的东西:
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 这样的
对于任何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
Koch曲线是一个看起来像A Koch曲线的分形。绘制长度为x的Koch曲线你要做的就是
- 绘制长度为x的Koch曲线3。
- 向左转60度。
- 绘制长度为x的Koch曲线3。
- 向右转120度。
- 绘制长度为x的Koch曲线3。
- 向左转60度。
- 绘制长度为x的Koch曲线3。
例外情况是x小于3:在这种情况下,你可以画一条长度为x的直线。
- 编写一个函数koch,该函数将乌龟和长度作为参数,并使用乌龟绘制具有给定长度的Koch曲线。
- 编写一个函数snowflake,绘制三条Koch曲线,绘制雪花轮廓。
- Koch曲线可以通过多种方式推广。有关示例,请参阅https://en.wikipedia.org/wiki/Koch_snowflake并实现您的最爱。