F#中的尾部递归性:使用Quicksort进行反转

嗨,我在理解尾递归方面有一些困难。 我知道这是避免无限循环和内存使用的重要。 我在“F#专家”中看到了一些简单函数例如斐波那契的例子,但是当结果不仅仅是一个数字时,我不认为我看过代码。

那么累加器会是什么? 我不确定...

这是我写的一个递归函数。 它使用快速排序算法来计算数组中的反转次数。 [它来自斯坦福大学Coursera MOOC Algo I的练习]

如果有人能解释如何使尾部递归,我将不胜感激。 [另外,我已经从命令式代码中翻译了这些代码,因为我之前在R中写过这样的代码,所以风格根本不起作用...]

另一个问题是:语法正确,A是一个(可变)数组,我写了let A = ....到处都是? 是A <- ....更好/相同?

open System.IO
open System


let X = [|57; 97; 17; 31; 54; 98; 87; 27; 89; 81; 18; 70; 3; 34; 63; 100; 46; 30; 99;
    10; 33; 65; 96; 38; 48; 80; 95; 6; 16; 19; 56; 61; 1; 47; 12; 73; 49; 41;
    37; 40; 59; 67; 93; 26; 75; 44; 58; 66; 8; 55; 94; 74; 83; 7; 15; 86; 42;
    50; 5; 22; 90; 13; 69; 53; 43; 24; 92; 51; 23; 39; 78; 85; 4; 25; 52; 36;
    60; 68; 9; 64; 79; 14; 45; 2; 77; 84; 11; 71; 35; 72; 28; 76; 82; 88; 32;
    21; 20; 91; 62; 29|]

// not tail recursive. answer = 488

let N = X.Length

let mutable count = 0

let swap (A:int[]) a b =
    let tmp = A.[a]
    A.[a] <- A.[b]
    A.[b] <- tmp
    A

let rec quicksortNT (A:int[]) = 
    let L = A.Length


    match L with 
         | 1 -> A
         | 2 -> count <- count + 1
                if (A.[0]<A.[1]) then A 
                   else [|A.[1];A.[0]|]

         | x -> let p = x
                let pval = A.[p-1]
                let A = swap A 0 (p-1)
                let mutable i = 1
                for j in 1 .. (x-1) do 
                     if (A.[j]<pval) then let A = swap A i j
                                          i <- i+1
                // end of for loop

                // putting back pivot at its right place
                let A = swap A 0 (i-1)
                let l1 = i-1
                let l2 = x-i

                if (l1=0) then
                            let A =  Array.append [|A.[0]|] (quicksortNT A.[1..p-1])               
                            count <- count + (l2-1)
                            A
                     elif (l2=0) then 
                            let A = Array.append (quicksortNT A.[0..p-2]) [|A.[p-1]|]
                            count <- count + (l2-1)
                            A
                else
                            let A = Array.append ( Array.append (quicksortNT A.[0..(i-2)]) [|A.[i-1]|] ) (quicksortNT A.[i..p-1])
                            count <- count + (l1-1)+(l2-1)
                            A


let Y = quicksortNT X
for i in 1..N do printfn "%d" Y.[i-1]
printfn "count = %d" count

Console.ReadKey() |> ignore

非常感谢您的帮助


正如我在我的评论中所说的那样:你进行就地交换,所以重新创建和返回数组是没有意义的。

但是,当您问尾部递归解决方案时,请使用列表和continuation-passing-style来查看此版本,以使算法具有尾递归功能:

let quicksort values =
    let rec qsort xs cont =
        match xs with
        | [] -> cont xs
        | (x::xs) ->
            let lower = List.filter (fun y -> y <= x) xs
            let upper = List.filter (fun y -> y > x) xs
            qsort lower (fun lowerSorted ->
                qsort upper (fun upperSorted -> cont (lowerSorted @ x :: upperSorted)))
    qsort values id

备注:

  • 你可以这样想:
  • 首先将输入分为upperlower
  • 然后开始排序(递归)的lower ,当你完成这个继续... ...
  • ...采取lowerSorted并排序upper部分,并继续...
  • ...采取两个排序的部分,加入他们,并将他们传递到外延续
  • 最外面的延续当然应该是id功能
  • 有些人会认为这不是快速排序,因为它不能排序!
  • 也许很难看,但它是尾递归的,因为最后一次调用是qsort ,结果将是当前调用的结果
  • 我使用了List,因为模式匹配非常好 - 但是您也可以在数组中使用它
  • 在这些情况下(这里),如果有多个递归调用我总是发现cont -passing解决方案,以更容易编写,更自然-但蓄电池可以作为很好(但你需要通过你在哪里,它会变得一团糟太)
  • 这不会花费较少的内存比没有版本cont -passing在所有-它只是将放在堆而不是堆栈(你通常可以有更多的方式堆)) -所以这是一个有点像作弊
  • 这就是命令式算法在性能方面仍然更好的原因 - 所以通常的折衷办法是(例如)复制数组,在副本上使用inplace算法,然后返回副本 - 这样算法的行为就好像它是纯粹的在外面

  • 对于快速排序的交换分区过程来说,它可以改变同一个数组; 您只需将它必须处理的数组范围的低指数和高指数传递给它。

    因此,建立一个嵌套函数并将其传递给2个索引。 为了使其递归尾部,添加第三个参数,列表范围到进程; 当它变空时,你就完成了。 Wikibook说你用A.[i] <- A.[j]改变数组。

    嵌套函数可以直接访问其父函数的参数,因为它在范围内。 所以, swap嵌套的:

    let rec quicksort (A:int[]) = 
    
        let swap a b =
            let tmp = A.[a]
            A.[a] <- A.[b]
            A.[b] <- tmp
    
        let todo =  ... (* empty list *)
    
        let rec partition low high = 
           .... (* run the swapping loop, 
                   find the two new pairs of indices,
                   put one into TODO and call *)
           partition new_low new_high
    
        let L = A.Length
    
        match L with 
         | 1 -> (* do nothing   A *)
         | 2 -> count <- count + 1
                if (A.[0]<A.[1]) then (* do nothing   A *)
                   else (* [|A.[1];A.[0]|] *) swap 1 0
    
         | x -> ....
                partition 0 L
    

    因此partition将是尾部递归的,在由quicksort为其设置的环境内工作。

    (免责声明:我不知道F#并且从未使用它,但我在某种程度上知道Haskell和Scheme)。

    链接地址: http://www.djcxy.com/p/80531.html

    上一篇: Tail Recursivity in F# : Inversions with Quicksort

    下一篇: Is there an obvious way to confirm if a function is tail recursive?