F#奇妙游(18):F#与百万浮点数MergeSort

Mergesort

mergesort是一个典型的分而治之、迭代的算法。它将一个数组分成两个子数组,分别排序,然后将有序的子数组归并为一个有序的数组。归并操作是将两个有序数组归并成一个有序数组的过程。

mergesort的时间复杂度为O(nlogn),最佳和最坏情况下的时间复杂度都是O(nlogn)。它的空间复杂度为O(n)。

算法的基本思路是:将数组分成两半,分别对两半进行排序,然后将结果归并起来。归并操作的实现是:创建一个临时数组,将两个有序的子数组中较小的元素放入临时数组中,然后将临时数组中的元素复制回原数组。

C#代码实现

MergeSort算法的C#代码实现如下:

public static void MergeSort(int[] array)
{
    int[] aux = new int[array.Length];
    MergeSort(array, aux, 0, array.Length - 1);
}

private static void MergeSort(int[] array, int[] aux, int lo, int hi)
{
    if (lo >= hi)
    {
        return;
    }

    int mid = lo + (hi - lo) / 2;
    MergeSort(array, aux, lo, mid);
    MergeSort(array, aux, mid + 1, hi);
    Merge(array, aux, lo, mid, hi);
}

private static void Merge(int[] array, int[] aux, int lo, int mid, int hi)
{
    for (int k = lo; k <= hi; k++)
    {
        aux[k] = array[k];
    }

    int i = lo;
    int j = mid + 1;
    for (int k = lo; k <= hi; k++)
    {
        if (i > mid)
        {
            array[k] = aux[j++];
        }
        else if (j > hi)
        {
            array[k] = aux[i++];
        }
        else if (aux[i] < aux[j])
        {
            array[k] = aux[i++];
        }
        else
        {
            array[k] = aux[j++];
        }
    }
}

这个算法实现的是in-place的,但是需要额外的空间来保存归并时的临时数组。所以,它的空间复杂度为O(n)。

F#代码实现

按照这个思路,可以直接翻译出F#的实现:

let split (lo: int) (hi: int) =
    let mid = lo + (hi - lo) / 2
    (lo, mid, hi)

let merge<'T when 'T: comparison> (a: 'T array) (aux: 'T array) (lo: int) (mid: int) (hi: int) =
    for k in lo..hi do
        aux[k] <- a[k]

    let mutable i = lo
    let mutable j = mid + 1

    for k in lo..hi do
        if i > mid then
            a[k] <- aux[j]
            j <- j + 1
        elif j > hi then
            a[k] <- aux[i]
            i <- i + 1
        elif aux[i] < aux[j] then
            a[k] <- aux[i]
            i <- i + 1
        else
            a[k] <- aux[j]
            j <- j + 1

let mergesort<'T when 'T: comparison> (a: 'T array) =
    let aux = Array.create a.Length a[0]

    let rec sort (a: 'T array) (aux: 'T array) lo hi =
        if hi <= lo then
            ()
        else
            let (lo, mid, hi) = split lo hi
            sort a aux lo mid
            sort a aux (mid + 1) hi
            merge a aux lo mid hi

    sort a aux 0 (a.Length - 1)

调用这个算法的代码如下:

let a = [| 3; 2; 1; 4; 5; 6; 9; 8; 7 |]
mergesort a
printfn "%A" a

或者,使用百万级别的数据来测试一下:

open System
open System.Diagnostics

let n = 100000000 // 100 million
let a = List.init n (fun _ -> Random().NextDouble())


let k = 5

printfn "%A" a[0 .. k - 1]

// print last 10 elements
printfn "%A" a[n - k .. n - 1]

let sw = Stopwatch.StartNew()
let b = mergesort a
sw.Stop()

printfn "%A" b[0 .. k - 1]

// print last 10 elements
printfn "%A" b[n - k .. n - 1]

printfn "%d elements sorted in %A seconds" n (float sw.ElapsedMilliseconds / 1000.0)

输出的结果:

[|0.4535403336; 0.4689574615; 0.8036827634; 0.01010983729; 0.548441069|]
[|0.3559696886; 0.1329930819; 0.1535221465; 0.7671093143; 0.5670336269|]
[|7.55451135e-09; 9.204160878e-09; 1.561528273e-08; 1.919768666e-08;
  3.959270578e-08|]
[|0.9999999739; 0.999999974; 0.9999999792; 0.999999981; 0.9999999876|]
100000000 elements sorted in 131.298 seconds

唯一的问题是,这个代码很不函数式,基本上式命令式的,而且涉及到对数组的修改。如果要更加函数一点呢?

函数式的mergesort

函数式的mergesort的思路是:将数组分成两半,分别对两半进行排序,然后将结果归并起来。归并操作的函数式描述可以很好的以函数式的方式实现。

let rec split (a: 'T list) =
    match a with
    | [] -> ([], [])
    | [x] -> ([x], [])
    | x::y::xs ->
        let (l1, l2) = split xs
        (x::l1, y::l2)

let rec merge (a: 'T list) (b: 'T list) =
    match (a, b) with
    | ([], _) -> b
    | (_, []) -> a
    | (x::xs, y::ys) ->
        if x < y then
            x::(merge xs b)
        else
            y::(merge a ys)

let rec mergesort (a: 'T list) =
    match a with
    | [] -> []
    | [x] -> [x]
    | _ ->
        let (l1, l2) = split a
        merge (mergesort l1) (mergesort l2)

当算法这么写的时候,理解起来就更加容易了。特别是关于merge的实现,把两个有序的list合并成一个有序的list,就是把两个list中较小的元素放在前面,然后再把剩下的元素合并。这个思路是非常清晰的。因为这个部分是mergesort的核心。

但是这个递归算法每次递归都会创建新的list,所以空间复杂度是O(nlogn)。而且,因为递归的关系,对于非常大的列表,会导致栈溢出。所以,这个算法并不是很好用。

总结

  1. Mergesort是一个典型的分而治之、迭代的算法。它将一个数组分成两个子数组,分别排序,然后将有序的子数组归并为一个有序的数组。归并操作是将两个有序数组归并成一个有序数组的过程。
  2. Mergesort的时间复杂度为O(nlogn),最佳和最坏情况下的时间复杂度都是O(nlogn)。它的空间复杂度为O(n)。
  3. 采用函数式的方式可以以非常直观和很少的代码量实现Mergesort算法。但是这个算法很难(到底是不是不可能我还不能确定)实现尾递归优化,所以对于大的数据集,会导致栈溢出。
posted @ 2023-07-22 16:54  大福是小强  阅读(6)  评论(0编辑  收藏  举报  来源