我的合并排序的整体代码如下所示:
let remove array =
Array.sub array 1 (array.Length - 1)
let rec merge (chunkA : int[]) (chunkB : int[]) =
if chunkA.Length = 0 && chunkB.Length = 0 then [||]
else if chunkA.Length = 0 || chunkB.[0] < chunkA.[0] then Array.append [| chunkB.[0] |] (merge chunkA (remove chunkB))
else Array.append [| chunkA.[0] |] (merge (remove chunkA) chunkB)
let rec mergesort (array : int[]) =
let middle = array.Length / 2
let chunkA = match middle with
| 1 -> [| array.[0] |]
| _ -> mergesort [| for i in 0 .. middle - 1 -> array.[i]|]
let chunkB = match array.Length - middle with
| 1 -> [| array.[array.Length - 1] |]
| _ -> mergesort [| for i in middle .. array.Length - 1 -> array.[i]|]
merge chunkA chunkB
此代码运行良好,但我想将merge
函数中的if语句系列更改为match with
语句。
然后我尝试实现以下代码:
let rec merge (chunkA : int[]) (chunkB : int[]) =
match chunkA.Length with
| 0 when chunkA.Length = chunkB.Length -> [||]
| 0 | _ when chunkB.[0] < chunkA.[0] -> Array.append [| chunkB.[0] |] (merge chunkA (remove chunkB))
| _ -> Array.append [| chunkA.[0] |] (merge (remove chunkA) chunkB)
当我运行我的代码时,Visual Studio向我抛出了一个“IndexOutOfRangeException”,具体在这里:
| 0 when chunkA.Length = chunkB.Length -> [||]
在这种情况下,chunkA
为空,但chunkB
中只有一个数字。因此,我不完全确定为什么F#甚至试图返回这种情况,因为块A和B的长度不一样,但我也很困惑为什么这会抛出一个Index错误,特别是在空数组上
另外,我对F#和一般的函数式编程都很陌生。如果我的代码中的结构或方法不符合标准,那么请随时对此进行评论。
另外,如果我很厚,请随时告诉我。
非常感谢, 路加
答案 0 :(得分:3)
正如Fyodor Soikin指出的那样,你的例外来源是这一行:
| 0 | _ when chunkB.[0] < chunkA.[0] -> Array.append [| chunkB.[0] |] (merge chunkA (remove chunkB))
但是你可能并不明白为什么会抛出异常。 As I learned last week to my surprise,匹配表达式中的when
子句适用于所有自上一个->
以来的案例。换句话说,当您编写上面的行时,F#理解您的意思是您希望将when
子句应用于 0
案例或 _
案例。 (当然,这是多余的)。
这就是你的异常的原因:F#看到0
的情况但是仍然应用了when chunkB.[0] < chunkA.[0]
测试 - 并且由于chunkA
是空的,所以总是会抛出异常。要解决此问题,您必须将这两种情况分开,以便when
仅适用于您要申请的情况:
| 0 -> Array.append [| chunkB.[0] |] (merge chunkA (remove chunkB))
| _ when chunkB.[0] < chunkA.[0] -> Array.append [| chunkB.[0] |] (merge chunkA (remove chunkB))
不幸的是,这确实意味着一些代码重复。在这种情况下,这并不是什么大问题,因为重复的代码是单行代码,但是如果你有大量的代码最终会重复,因为必须分割出这样的两个案例(这两个案例不应该共享一个when
条件),然后您可以将该重复的块转换为函数,以便它不再重复。
编辑:我刚刚注意到您的代码中的一部分可能更简单。您的原始代码包括:
let chunkA = match middle with
| 1 -> [| array.[0] |]
| _ -> mergesort [| for i in 0 .. middle - 1 -> array.[i]|]
let chunkB = match array.Length - middle with
| 1 -> [| array.[array.Length - 1] |]
| _ -> mergesort [| for i in middle .. array.Length - 1 -> array.[i]|]
你在这里做的是获取数组的一部分,但F#有一个非常方便的语法来切割数组:array.[start..end]
其中start
和end
是包含的指数你想要的切片。因此,表达式[| for i in 0 .. middle - 1 -> array.[i]|]
可以简化为array.[0 .. middle - 1]
,表达式[| for i in middle .. array.Length - 1 -> array.[i]|]
可以简化为array.[middle .. array.Length - 1]
。让我们在代码中替换这些表达式,看看我们得到了什么:
let chunkA = match middle with
| 1 -> [| array.[0] |]
| _ -> mergesort array.[0 .. middle - 1]
let chunkB = match array.Length - middle with
| 1 -> [| array.[array.Length - 1] |]
| _ -> mergesort array.[middle .. array.Length - 1]
现在,看一下这个,我注意到两种情况下的1
条件实际上是处理与_
条件完全相同的数组切片;唯一的区别是,如果中间是1,则不要调用mergesort
。我怎么知道它是完全相同的数组切片?好吧,如果middle
为1,则array.[0 .. middle-1]
表达式将变为array.[0..0]
,这是从索引0开始的数组中长度为1的切片,与[| array.[0] |]
完全相等。如果array.Length
正好比middle
多一个,则array.[middle .. array.Length - 1]
表达式将为array.[middle .. middle]
,这与[| array.[middle] |]
完全相同。
因此,如果不是对mergesort
的调用,我们可以合并这两个表达式。事实上,这是一个非常简单的方法!只需将长度检查移至mergesort
的顶部,如下所示:
let rec mergesort (array : int[]) =
if array.Length < 2 then
array // Arrays of length 0 or 1 are already sorted
else
// rest of mergesort function goes here
现在你可以安全地合并match
的两个案例,因为你知道你不会遇到无限递归循环:
let middle = array.Length / 2
let chunkA = mergesort array.[0 .. middle - 1]
let chunkB = mergesort array.[middle .. array.Length - 1]
merge chunkA chunkB
将所有这些放在一起,我建议的原始mergesort
函数版本如下:
let rec mergesort (array : int[]) =
if array.Length < 2 then
array // Arrays of length 0 or 1 are already sorted
else
let middle = array.Length / 2
let chunkA = mergesort array.[0 .. middle - 1]
let chunkB = mergesort array.[middle .. array.Length - 1]
merge chunkA chunkB
作为奖励,这个版本的mergesort
没有你的原始版本所带来的微妙错误:你忘了考虑空数组的情况。在空数组上调用原始mergesort
会产生无限循环。你可能会为自己解决问题而不是从我解释如何获益,所以我只是提到在F#中for i in 0 .. -1
不是错误,但是会经历for
循环零次(即,for
循环的主体不会被执行)。同样,array.[0..-1]
不是错误,但会生成一个空数组。有了这些细节的知识,您应该能够处理原始mergesort
函数的代码,并且如果您将它传递给空字符串,它将会无限循环。 (虽然因为你在无限循环中的mergesort
调用不在尾部位置,所以它不会是尾调用。因此堆栈最终会溢出,从而使你免于“真正的”无限循环。