题记:
《Excel圣经》1:3 微软说,“要有循环”,便有了循环。
引子:
keyword: one of and as each else error false if in is let meta not otherwise or section shared then true try type #binary #date #datetime #datetimezone #duration #infinity #nan #sections #shared #table #time
可以发现M语言的关键字里面并没有for/while这样的关键字,那么M如何实现循环呢?
遍历:
M实现循环是通过特定的函数或者运算符来实现的,按照实现原理的不同,可以初步地分为——遍历、迭代与递归三大类,我们先看一些简单的遍历案例:
~~~~~~~~~~
案例1:不使用Repeat函数,生成一个list,里面有20个Number "1"。
~~~~~~~~~~
在M语言中,就有一个函数可以遍历一个list,它就是List.Transform ——
List.Transform(list as list, transform as function) as list
这个函数实现的功能其实是“list内元素转化”的功能,但由于这个过程会遍历list中的每一个元素,因此我们可以把这个函数当做for循环来使用:
= List.Transform({1..20},(x)=>1)
我们再看看这个实现过程:将构造的{1..20}的每一个元素都转化为1,构造了1~20共20个元素的序列,因此最终转化的结果也是有20个数字1的list。
~~~~~~~~~~
案例2:使用List.Transform函数,生成20以内的全部奇数。
(List.Numbers可以快捷实现,本节重点介绍的是循环,因此不使用)
~~~~~~~~~~
我们知道全体奇数可以通过自然数来构造,因此我们容易想到:
= List.Transform({1..10},(x)=>2*x-1)
我们回顾下实现过程,20以内最大的奇数是19,而19=2*10-1,因此我们需要构造{1..10}这样的list来进行遍历,通项公式就是(x)=2*x-1。
~~~~~~~~~~
案例3:使用质数的定义证明19是质数。
~~~~~~~~~~
质数定义:在大于1的自然数中,除了1和它本身以外不再有其它的因数。
所谓因数就是能够整除给定的数,我们可以通过Mod函数来判断,只要余数为0,那就是因数,因此我按照定义把2~18全部逐一验证遍即可:
= List.Min(List.Transform({2..18},(x)=>Number.Mod(19,x)))
上述表达式结果为1,说明2~18分别除19得到的余数list中最小值为1,没有0,也就是19除了1和自身外不再有其它的因数了,因此19为质数。
迭代:
我们先考察一个简单的计数问题:10以内偶数的个数是多少?
显然使用List.Count是无法得到结果的,最简单的思路就是我们假设有一个容器专门用来实现计数的功能,逐一判断10以内的每一个自然数是不是偶数,如果是偶数,计数器就加一。那么对于计数器的上一个结果来说,每次新出现一个偶数,计数器的结果就“迭代”更新了一次。
上述解决问题的过程就是接下来要介绍的迭代循环了。
~~~~~~~~~~
案例4:10以内的奇数有多少个?
~~~~~~~~~~
在M中,Accumulate函数具有积累的功能,可以实现“容器”迭代的功能,我们通过Number.IsOdd函数来对每一个数做是否为奇数的判断:
List.Accumulate(list as list, seed as any, accumulator as function) as any
这个函数需要给定一个种子参数作为起始值,然后根据指定的function进行迭代运算。
= List.Accumulate({1..10},0,(x,y)=>if Number.IsOdd(y) then x+1 else x)
我们回顾下迭代的过程:
首先计数器(seed)的初始值为0,然后开始对{1..10}依次做判断,(x,y)的x就是上一次的seed,而y就是当前{1..10}的一个值;最初x为0,y为1,然后y依次为2~10,x对应的为上一次function运算结束后的那个值;因此1判断为奇数时x就执行了then x+1(这里面的x还是seed的值:0),因此当对y=2做判断时,就执行了else x,x值不变,当全部判断完时,输出x最后的值,也就是5,这样就完成了整个迭代循环。
~~~~~~~~~~
案例5:使用迭代循环的方法倒序输出“Hello World!”。
~~~~~~~~~~
既然要求使用迭代,那么就要想清楚“容器”里面到底装什么内容,这个例子需要倒序输出,我们不妨将最终输出的结果理解为一个一个字符拼接成的字符串,这样我们就知道怎么构造这个字符串容器了:
= List.Accumulate({0..(Text.Length("Hello World!")-1)},"",(x,y)=>
x&Text.At("Hello World!",Text.Length("Hello World!")-y-1))
这里使用了一个小技巧,使用了N-x的结构构造一个降序的list,实现顺序迭代时是从后向前进行的,然后使用字符串容器保存这些迭代的字符串,最终输出。
递归:
前面介绍过@这个符号,它的作用是调用后面的内容,后面接函数时,就是递归了。
我们通过一个简单的例子来看看递归的作用和用法:
~~~~~~~~~~
案例6:计算9!(不使用Number.Factorial)。
~~~~~~~~~~
let
factorial=(x)=>if x=1 then 1 else x*@factorial(x-1)
in
factorial(9)
我们把这个语句和高中学的数列对应起来就好理解了,首先告知首项f(1)=1,然后使用递归的方法写出递推公式f(n)=n*f(n-1),最后求第9项。
不难发现,递归具备如下几个特点:
1.函数调用了自身,并用“@”进行函数名的标识;
2.调用自身时,参数会发生变化,避免无限递归;
3.通过 if 语句对特定参数的函数值进行了定义。
~~~~~~~~~~
案例7:求36和63的最小公倍数。
~~~~~~~~~~
我们都知道两个整数的最小公倍数等于两数之积除以两数的最大公约数,因此这个案例如果能够解决如何求最大公约数,问题也就迎刃而解了,下面先介绍一种比较高效的算法。
辗转相除法:
用较小数除较大数,再用出现的余数(第一余数)去除除数,再用出现的余数(第二余数)去除第一余数,如此反复,直到最后余数是0为止,最后的除数就是这两个数的最大公约数。
这里面核心递归的是什么步骤?这个一定要理清楚,否则就写不出正确的递归语句。看完介绍后可以发现,最核心的一步就是用余数去除除数,如果不好理清余数除数的关系,你就这样理解:用最小的那个数去除中间的数。(被除数最大,除数第二排中间,余数最小)
let
GCD=(x,y)=>if y=0 then x else @GCD(y,Number.Mod(x,y))
in
36*63/GCD(36,63)
我们用数列的递推公式思路把这个案例的核心步骤理一理:
GCD(x,y)=GCD(y,Number.Mod(x,y))
用语言描述就是:大数和小数的最大公约数等于小数和两数取余的最大公约数。
for循环和while循环:
前面的遍历基本上可以实现for循环的效果了,那么while循环与哪一个函数最接近呢?
这里介绍一个用得稍微少一点的循环函数:
List.Generate(initial as function,condition as function,next as function, optional selector as nullable function) as list
~~~~~~~~~~
例如下面这段Python代码:
#!/usr/bin/python3
i=0
while i<10:
i=i+1
print(i)
~~~~~~~~~~
使用M语句就是:
let
list=List.Generate(()=>1, each _ <11, each _ + 1)
in
list
这个函数的特点也是有初始值,然后有一个条件判断,条件内会一直循环下去。
下篇笔记:《循环嵌套与综合应用》