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)。而且,因为递归的关系,对于非常大的列表,会导致栈溢出。所以,这个算法并不是很好用。
总结
- Mergesort是一个典型的分而治之、迭代的算法。它将一个数组分成两个子数组,分别排序,然后将有序的子数组归并为一个有序的数组。归并操作是将两个有序数组归并成一个有序数组的过程。
- Mergesort的时间复杂度为O(nlogn),最佳和最坏情况下的时间复杂度都是O(nlogn)。它的空间复杂度为O(n)。
- 采用函数式的方式可以以非常直观和很少的代码量实现Mergesort算法。但是这个算法很难(到底是不是不可能我还不能确定)实现尾递归优化,所以对于大的数据集,会导致栈溢出。