4.F# vs C#: 快速排序

来源:互联网 发布:淘宝哪家派力肯 编辑:程序博客网 时间:2024/06/05 23:58
此博客文章停止维护,访问个人博客链接
www.wangqunxi.com

在接下来的例子中,我们将用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行代码,如果你习惯了其语法,你会觉得它有更高的可读性。






原创粉丝点击