此博客文章停止维护,访问个人博客链接
在接下来的例子中,我们将用C#和F#分别实现一个快速排序来为数组排序的。
下面是简化的快速排序算法的逻辑 :
如果list为空,就直接返回 否则: 1. 取list的第一个元素。 2. 在剩余的元素中找到小于第一个的元素,然后排序他们 3. 在剩余的元素中找到大于第一代所有元素,排序他们 4. 合并前面三部分得到最终的结果: (小于第一个元素的排序数列 + 第一个元素 + 大于第一个元素的排序数列)
注意这是一个简化的算法并没有优化过.现在我们要把它搞清楚.
下面是F#代码:
let rec quicksort list =
match list with
| [] -> // 如果 list 为空
[] // 返回一个 list
| firstElem::otherElements -> // 如果 list 不为空
let smallerElements = // 提取小于的元素
otherElements
|> List.filter (fun e -> e < firstElem)
|> quicksort // 排序他们
let largerElements = // 提取大于的元素
otherElements
|> List.filter (fun e -> e >= firstElem)
|> quicksort // 排序他们
// 用一个新数组合并三部分元素然后返回.
List.concat [smallerElements; [firstElem]; largerElements] //运行printfn "%A" (quicksort [1;5;23;18;9;1;3])
再次提醒,上面的算法并没有优化过,但这样设计更加清晰的反应了算法
让我们将代码过一遍:
- 这里并没有类型声明,这个函数可以适应任何类型的list,只要list的元素是可以比较的(大多数F#的类型都拥有默认的比较函数)
- 这个函数是一个递归函数 – 他在"
let rec quicksort list =
"中使用rec
关键字来使编译器能识别 match..with
有点像switch/case 语句. 每个分支测试条件都带有垂直竖线,就像下面代码:
match x with | caseA -> something | caseB -> somethingElse
- 对应 "
match
" 的[]
用来匹配空list, 然后返回一个空的list. - 对应"
match
" 的firstElem::otherElements
做了两件事:- 第一, 他匹配一个非空的list
- 第二, 他自动的创建了两个值. 第一个元素是 "
firstElem
",另一个是叫做 "otherElements
"的list。 他不仅仅像C#中的 "switch" 语句用于分支判断,同时它还可以对变量进行声明复制。
-
->
符号有点像C#中 lambda表达式的 (=>
) . 如用C#的lambda表达式来实现它有点像下面的样子(firstElem, otherElements) => do something
。 - "
smallerElements
" 部分来自出第一个元素以外的其他元素序列, 它利用一个内敛的lambda函数实现 "<
" 操作符来与第一个元素比较大小并过滤出结果,然后将结果传输到快速排序的函数中继续递归调用。 - "
largerElements
" 如出一辙,只是将比较符替换成">=
"。 - 最后利用"
List.concat
"函数重新构造一个返回序列,为了将第一个元素也插入其中,我们必须用方括号将第一个元素括起来。 - 再次强调这里没有return关键字。 但在"
[]
" 分支中它返回一个空list,而在主分支中, 他返回了一个新构建的list。
比较一个”老样式的“ C# 实现(没有使用 LINQ).
public class QuickSortHelper{
public static List<T> QuickSort<T>(List<T> values)
where T : IComparable
{
if (values.Count == 0)
{
return new List<T>();
}
//获取第一个元素
T firstElement = values[0];
//分别湖区大于和小于第一个元素的序列
var smallerElements = new List<T>();
var largerElements = new List<T>();
for (int i = 1; i < values.Count; i++) // 从1开始而不是0!
{
var elem = values[i];
if (elem.CompareTo(firstElement) < 0)
{
smallerElements.Add(elem);
}
else
{
largerElements.Add(elem);
}
}
//返回结果
var result = new List<T>();
result.AddRange(QuickSort(smallerElements.ToList()));
result.Add(firstElement);
result.AddRange(QuickSort(largerElements.ToList()));
return result;
}}
相比两份代码, 你会再次注意到F# 更加的紧凑,他噪声少,并且没有类型声明。
而且,F#的代码能更精确的表达真正的算法,而不像C#。F#的另一个关键优势是它和C#相比,它的代码更体现声明式(做什么)而不是命令式(怎么么做),因此它具有更强的自注性。
C#的函数式实现。
下面是更现代的”函数式“实现,他使用LINQ和扩展函数:
public static class QuickSortExtension{
/// <summary> /// 为IEnumerable实现一个扩展函数
/// </summary>
public static IEnumerable<T> QuickSort<T>(
this IEnumerable<T> values) where T : IComparable
{
if (values == null || !values.Any())
{
return new List<T>();
}
//将list的第一个元素和其他元素分开
var firstElement = values.First();
var rest = values.Skip(1);
//分别获得小于和大于第一个元素的序列
var smallerElements = rest
.Where(i => i.CompareTo(firstElement) < 0)
.QuickSort();
var largerElements = rest
.Where(i => i.CompareTo(firstElement) >= 0)
.QuickSort();
//返回结果
return smallerElements
.Concat(new List<T>{firstElement})
.Concat(largerElements);
}}
和F#一样上面的代码看上去更加的清晰,可读性更加的强。但是不幸的是他在函数签名中仍然不可避免额外的”噪声“。
正确性
最后,这个紧凑的另一个有益的副作用就是F#代码更容易一次就做对事,而C#则需要更多的调试。
事实上,但编写这些例子代码时,“老式”的C#代码一开始是不正确的,他要求一些调试然后才能正确。尤其是复制的for循环(他应该从1开始迭代,而不是0)还有
CompareTo
比较运算 (我都用错了), 而且它它很容不小心修改list。而在第二个C#的例子中,函数是风格不仅清晰而且更容易写对代码 。
但是即使是函数式版本的C#代码和F#相比也有些缺点,比如 F#因为使用了模式匹配,他不可能在将空list匹配到非空判断分支上,在C#中容易忘了下面的判断:
if (values == null || !values.Any()) ...
接下来,提取第一个元素时:
var firstElement = values.First();
可能会因为一个异常而失败。编译器不会迫使你做这些。在你的代码中你应该使用
FirstOrDefault
而不是
First
因为你要写更加健壮的代码,下面的例子中是一种很常见的C#代码编写模式,但是F#很少这样做:
var item = values.FirstOrDefault(); // 替换 .First() if (item != null)
{
// do something if item is valid }
“模式匹配和分支判断”在F#中可以让你避免很多以外的特例
补充说明
上面的F#实现的非常冗长多余:
下面给出一个更加经典,简洁的实现方式
let rec quicksort2 = function
| [] -> []
| first::rest ->
let smaller,larger = List.partition ((>=) first) rest
List.concat [quicksort2 smaller; [first]; quicksort2 larger]
// test code printfn "%A" (quicksort2 [1;5;23;18;9;1;3])
只有4行代码,如果你习惯了其语法,你会觉得它有更高的可读性。