F#Merge Sort - 尝试与结构实现匹配时的IndexOutOfRangeException

时间:2017-04-23 14:56:38

标签: arrays recursion f# mergesort outofrangeexception

我的合并排序的整体代码如下所示:

   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#和一般的函数式编程都很陌生。如果我的代码中的结构或方法不符合标准,那么请随时对此进行评论。

另外,如果我很厚,请随时告诉我。

非常感谢, 路加

1 个答案:

答案 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]其中startend是包含的指数你想要的切片。因此,表达式[| 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调用不在尾部位置,所以它不会是尾调用。因此堆栈最终会溢出,从而使你免于“真正的”无限循环。