C++ 数学与算法系列之排列与组合
1. 前言
本文将聊聊排列和组合,排列组合是组合学最基本的概念,排列组合在程序运用中也至关重要。
- 排列问题:指从给定个数的元素中取出指定个数的元素进行排序,并统计排序的个数。
- 组合问题:指从给定个数的元素中仅仅取出指定个数的元素,不排序,并统组合的个数。
2.排列
排列的定义:
- 从
n
个不同元素中,任取m(m≤n,m与n均为自然数)
个不同的元素按照一定的顺序排成一列,叫做从n
个不同元素中取出m
个元素的一个排列。如从1,2,3,4,5
中选择3
个数字进行排列,则认为1,2,3
和3,2,1
是两种不同的排列。 - 从
n
个不同元素中取出m(m≤n)
个元素的所有排列的个数,叫做从n
个不同元素中取出m
个元素的排列数,用符号A(n,m)
表示。
Tips: 排列的英文是
Permutation
或者Arrangement
,故使用P
或者A
表示都可以,二者含义一样。
计算从 5
个数字中任选择3
个数字有多少种排列方式?
解决此问题时,先把问题演变成从 5
个数字中选择 5
个数字进行排列,其有多少种方案?
- 第
1
数字可以在5
个数字中任选择一个,故有5
种选择。
- 因第
1
个数字已经选择了一个,第2
个数字只能在剩下的数字中选择,也就是只能在剩下的4
个数字中选择,则有4
种选择。
- 同理,第
3
个数字有3
种选择,第4
个数字只有2
种选择,第五个数字只能有1
种选择。 - 所有的排列数是
5*4*3*2*1=120
种方案,是不是看起来很熟悉,就是求5
的阶乘。
下面使用穷举法求解上述问题中排列的个数:
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
int count=0;
for(int a=1; a<=5; a++) {
for(int b=1; b<=5; b++) {
if(b==a)continue;
for(int c=1; c<=5; c++) {
if(c==b || c==a )continue;
for(int d=1; d<=5; d++) {
if(d==a || d==b || d==c)continue;
for(int e=1; e<=5; e++) {
if(e==d || e==c || e==b || e==a) continue;
count++;
}
}
}
}
}
cout<<count<<endl;
return 0;
}
//输出结果:120
既然是求 5
的阶乘,可以简化程序。
#include <iostream>
#include <cmath>
using namespace std;
int main(int argc, char** argv) {
int num=5;
int result=1;
for(int i=1; i<=num; i++)
result*=i;
cout<<result;
return 0;
}
// 120
如果不是选择 5
个数字,而是选择 4
个数字?
- 则第
1
个数字有5
种选择,第2
个数字有4
种选择,第3
个数字有3
种选择,第4
个数字有2
种选择,最终可选择的个数为5*4*3*2=120
,和前面相比较,即为5的阶乘
除以1的阶乘
。
如果不是选择 4
个数字,而是选择 3
个数字?
- 则第
1
个数字有5
种选择,第2
个数字有4
种选择,第3
个数字有3
种选择,最终可选择的个数为5*4*3=60
。即为5!
除以2!
的阶乘。
可推导出从 n
个数字中选择m
个数字的排列个数的公式:
从推论可知,求A(n,m)
的排列个数可以通过乘法原理求解:
- 计算排列的个数,先确定高位的可能个数,再逐一确认次高位可能个数,一直到最低位的可能个数……完成它需要分成
m
个步骤。 - 最高位有
n
种方法,次高位有n-1
种方法……最低位有n-m+1
种方法。则最终的排列个数有:n*(n-1)*(n-2)……(n-m-1)
种。
利用A(n,m)
排列公式求解个数的算法:
#include <iostream>
#include <cmath>
using namespace std;
/*
*求阶乘函数
*/
int getJc(int num) {
int res=1;
for(int i=1; i<=num; i++)
res*=i;
return res;
}
/*
*求A(n,m)的排列个数
*/
int main(int argc, char** argv) {
int n;
int m;
int count=0;
cin>>n;
do {
cout<<"m小于n:"<<endl;
cin>>m;
} while(m>n);
//求 n 的阶乘
int nJc=getJc(n);
//求 n-m 的阶乘
int nmJc=1;
//如果 n 等于 m
if(n==m)
nmJc==1;
else {
nmJc=getJc(n-m);
}
count=nJc/nmJc;
cout<<count;
return 0;
}
输出结果:
3. 组合
组合的定义:
- 从
n
个不同元素中,任取m(m≤n)
个元素并成一组,叫做从n
个不同元素中取出m
个元素的一个组合。 - 从
n
个不同元素中取出m(m≤n)
个元素的所有组合的个数,叫做从n
个不同元素中取出m
个元素的组合数。用符号C(n,m)
表示。
Tips:
C是单词Combination
的缩写。
组合与排列的区别:
组合对于找出来的数字的顺序没有要求,也就是说1,2,3
和3,2,1
只能算一种组合方案。
如何统计组合的个数?
可以根据排列公式推导。
如从 1,2,3
选择 2
个数字进行组合。先套用排列计算公式,共有 3*2=6
种排列方案。即 [1,2]、[2,1]、[1,3]、[3,1]、[2,3]、[3,2] 6
种方案。求组合个数,则需要减去数字一样、顺序不一样重复方案,最终结果为 3
。
求解组合的个数可以先求解排列个数后,再排除重复的部分。问题转为具体重复的会有多少?
- 如果从
4
个数字中选择3
个数字,则任意选择的3
个数字会有3!=6
种排列方案,但是,对于组合而言,是一种方案。
- 同时,从
5
个数字中选择4
个数字排列,任意4
个数字会有4!
种排列。或者说从n
个数字中任意选择m
个数字,则m
个数字的排列有m!
种,对于组合而言,这m!
个排列数只计数1
次。 - 所以,求解
n
个数字中选择m
个数字的组合数可以先计算排列数后,再在结果上除以m!(m的阶乘)
。
在程序中套用上述公式,可以求解出 C(3,2)
有 3
种组合数。
#include <iostream>
#include <cmath>
using namespace std;
/*
*求阶乘函数
*/
int getJc(int num) {
int res=1;
for(int i=1; i<=num; i++)
res*=i;
return res;
}
/*
*求C(n,m)的组合个数
*/
int main(int argc, char** argv) {
int n;
int m;
int count=0;
cin>>n;
do {
cout<<"m小于n:"<<endl;
cin>>m;
} while(m>n);
//求 n 的阶乘
int nJc=getJc(n);
//求 n-m 的阶乘
int nmJc=1;
//如果 n 等于 m
if(n==m)
nmJc==1;
else {
nmJc=getJc(n-m);
}
//求 m 阶乘
int mJc=m==0?1:getJc(m);
count=nJc/(mJc*nmJc);
cout<<count;
return 0;
}
输出结果:
在上述组合公式的基础上,组合公式还可以发生如魔术般的变化,也许这就是数学的神奇之处。
3.1 运算法则一
如下图所示:
通过一个案例求证:假设有 4
名学生,选择 3
名学生打扫卫生,有多少种选择?
显然,这是一个组合问题,没有顺序的要求,即C(4,3)
。有 2
种思路求解:
- 从
4
个学生中选择3
名学生打扫卫生。套用组合的基础公式可知结果=4!/ 3!*1!=4
种选择。如下图所示,可以理解为是正向选择。
- 另一种方案,称为反向选择,因为有
4
个学生,每次选择一个学生回家,剩下的搞卫生,同样满足要求。相当于4
个学生里面选择1
名学生。结果4!/1!*3!=4
。
组合公式的如上运算法则很容易理解。根据下面的组合公式,可知,从 n
中选择 m
和 从 n
中选择 n-m
的最终表达式是一样的。
编程实现:
#include <iostream>
#include <cmath>
using namespace std;
/*
*求阶乘函数省略
*/
/*
* 求C(n,m)=C(n,n-m)的组合个数相同
*/
int main(int argc, char** argv) {
int n;
int m;
int count=0;
int count_=0;
cin>>n;
do {
cout<<"m小于n:"<<endl;
cin>>m;
} while(m>n);
//求 n 的阶乘
int nJc=getJc(n);
//求 m!阶乘
int mJc=getJc(m);
//求 n-m 阶乘
int nmJc=1;
if (n!=m)
nmJc=getJc(n-m);
//求 C(n,m)的组合数
count=nJc / (mJc*nmJc);
//求 C(n,n-m)的阶乘,根据公式可知,分母仅是交换了相乘两数的位置
count_=nJc / (nmJc* mJc);
if(count==count_) {
//验证通过
cout<<"OK"<<endl;
} else {
cout<<"NO"<<endl;
}
return 0;
}
3.2 运算法则二
如下图所示,当从 n
中取m-1和 m
个数字得到的组合总数,可归纳为求解 n+1
中取 m
个数字的组合数。
直接套用公式验证 C(3,1)+C(3,2)
和 C(4,2)
的结果:
-
3
个数字选择1
个数字进行组合,结果=3!/1!*2!=3
。 -
3
个数字选择2
个数字进行组合,结果=3!/2!*1!=3
。 -
4
个数字选择2
个数字进行组合,结果=4!/2!*2!=6
。
结论是:C(3,1)+C(3,2)=C(4,2)
。
Tips:
m
和m-1
必须连续!如C(4,2)+C(4,4)
并不等于C(5,4)
。C(7,3)+C(7,4)=C(8,4)
是成立的。
根据场景验证:
- 如果有
4
名学生,需要2
名学生留下来搞卫生,显然,可选择方案有C(4,2)=4!/2!*2!=6
种方案。
- 换一种理解,如果学号为
1
的学生必须留下来,显然,只需要在剩下的3
名学生中选择1
名学生留下来。如果学号为1
的学生不留下来,则需要从剩下的3
名学生中选择2
名。
严格的证明,可以由原始公式直接推导。如下图所示:
编程实现:
#include <iostream>
#include <cmath>
using namespace std;
/*
*求阶乘函数省略……
*/
/*
* 求证 C(n,m)+C(n,m-1)=C(n+1,m)
*/
int main(int argc, char** argv) {
int n;
int m;
int count=0;
int count_=0;
cin>>n;
do {
cout<<"m小于n:"<<endl;
cin>>m;
} while(m>n || m<1) ;
//求 n 的阶乘
int nJc=getJc(n);
//求 m!阶乘
int mJc=getJc(m);
//求 n-m 阶乘
int nmJc=1;
if (n!=m)
nmJc=getJc(n-m);
//求 m-1 的阶乘
int moneJc= getJc(m-1);
//求 n-(m-1) 的阶乘
int nmoneJc= getJc(n-m+1 );
//求 n+1-m 阶乘
int nonemJc= getJc( n+1-m );
//求C(n,m)+C(n,m-1) 的组合数
count=nJc / (mJc*nmJc) + nJc / ( moneJc * nmoneJc ) ;
//求 C(n+1,m)的阶乘 根据公式可知,分母仅是交换了相乘两数的位置
count_= nJc*(n+1) / (mJc* nonemJc);
if(count==count_) {
//验证通过
cout<<"OK"<<endl;
} else {
cout<<"NO"<<endl;
}
return 0;
}
3.3 运算法则三
如下图所示:
先直接套用公式验证 C(2,0)+C(2,1)+C(2,2)
的结果。
-
C(2,0) 2
个数字中选择0
个,结果为1
。 -
C(2,1) 2
个数字中选择1
个,结果为2!/1!*1!=2
。 -
C(2,2) 2
个数字中选择2
个,结果为1
。 - 所以
C(2,0)+C(2,1)+C(2,2)=4
和 22 结果一样。但这只是个例,不足以证明普适性。
用另一种方式验证公式的合理性:假设现有一个箱子,里面有 2
个苹果,请问选择任意个苹果数的方案有多少种?
方案一:你的角度。
- 不选择(
C(2,0)
),可以认为是1
种方案。 - 选择
1
个苹果(C(2,1)
),可以在2
个苹果中任一个,则有2
种方案。 - 选择 2 个苹果(
C(2,2)
),只有 1 种方案。 -
C(2,0)+C(2,1)+C(2,2)=4
。
方案二:苹果的角度。
- 每个苹果都是独立的个体,可以出来,也可以不出来。所以每个苹果都有
2
种选择。 - 箱子中现在有
2
个苹果,根据乘法原理,也就是2
个2
相乘(2
的2
次方 )。 - 所以 22=4。如果有
3
个苹果,则共有 23 种方案。
编程验证:
#include <iostream>
#include <cmath>
using namespace std;
/*
*求阶乘函数省略
*/
/*
* 求证 C(n,0)+C(n,1)+C(n,n)=2^n
*/
int main(int argc, char** argv) {
int n;
cin>>n;
//当不取,或取全部时,组合个数都为 1
int s=2;
//求 n 阶乘
int nJc =getJc(n);
for(int i=1; i<n; i++) {
//求 i 阶乘
int iJc=getJc(i);
//求 n -i 阶乘
int niJc=getJc(n-i);
s+= nJc / (iJc*niJc );
}
//求 2 的 n 次方
int res= pow(2,n);
if(res==s) {
cout<<"OK"<<endl;
} else {
cout<<"NO"<<endl;
}
return 0;
}
//输入 2
//输出 OK
3.4 运算法则四
如下图所示:
Tips: 组合公式的上面的数字是相同的,下面的数字必须连续。
可以用选择值日生的例子推导公式的正确。如果需要在学号为 1、2、3、4、5
的这 5
名学生中选择 3
名学生留下来当值日生,且必须在选择出来的 3
名学生中指定一人为组长。则选择方案可以由以下的分解方式组成:
- 学号为
1
学生当组长,则只需要在剩下的4
名学生中选择2
名学生,即C(4,2)
。 - 学号为
2
学生当组长,则只需要在剩下的3
名学生中选择2
名学生,即C(3,2)
。 - 学号为
3
学生当组长,则只需要在剩下的2
名学生中选择2
名学生,即C(2,2)
。 - 学号
4,5
当组长,剩下人数不够凑成3
个。没得选择。 - 故
C(5,3)=C(4,2)+C(3,2)+C(2,2)
。
3.5 运算法则五
如下图所示:
还是以上面的值日生为例,现在有 7
名学生,4
男 3
女,需要从 7
人中选择 3 人留下来值日,其组合数为 C(7,3)
,在所有组合数中一定出现如下的搭配:
- 没有男生
C(4,0)
,则选择女生(3,3)
,即选择方案有C(4,0)*C(3,3)
。 - 选择
1
名男C(4,1)
,则选择女生C(3,2)
,选择出来的男生可以和选择出来的任一组女生搭配,显然方案数是C(4,1)*C(3,2)
。 - 选择
2
名男C(4,2)
,则选择女生C(3,1)
,共有C(4,2)*C(3,1)
种方案。 - 选择
3
名男C(4,3)
,则选择女生C(3,0)
,共有C(4,3)*C(3,0)
种方案。
4. 总结
排列和组合公式是在数学上已经验证过的公式,本文中所提供的代码,都是使用此公式,解决具体的问题。
上一篇: 组合数学概要 1:基本组合数学和组合原理
下一篇: 用计算机计算合并数_计算合并数
推荐阅读
-
C++ 数学与算法系列之排列与组合
-
组合数学] 排列与组合 ( 多集组合数 | 所有元素的重复数大于组合数 | 多集组合数推导 1 分割线的推导 | 多集组合数推导 2 不确定方程的非负整数解的推导 )
-
C + 数学和算法系列的排列和组合
-
组合数学] 排列与组合(两个计数原理、集合排列示例 | 集合排列、圆排列示例 )
-
组合数学_第 1 章_排列与组合
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面