繁体   English   中英

如何使用F#获取没有第一个和最后一个项目的子列表?

[英]How to take sublist without first and last item with F#?

我已经整理了整数值列表:

let ls = [1..4]

如何在没有第一个和最后一个元素的情况下获得子列表? (以最佳方式)

预期结果是[2; 3] [2; 3]

这是我到目前为止所做的,是的,它有效,但我认为这不是最好的方法。

[1..4] |> List.tail |> List.rev |> List.tail |> List.sort

一个有点长的回答来回应你无辜的措辞资格: “以最佳的方式”

什么方面最优?

  1. 性能? (最有可能的)
  2. 性能还包括GC性能?
  3. 内存使用情况?
  4. 86?
  5. 64?

等等...

所以我决定测量问题的某些方面。

我在各种不同的上下文中测量了不同的答案(也添加了一个非惯用的版本)。

这里有一个我用来测量的程序

open System
open System.Diagnostics
open System.IO

module so29100251 =
    // Daystate solution (OP)
    module Daystate =
        // Applied minor fixes to it
        let trim = function
            | [] | [_] | [_;_] -> []
            | ls -> ls |> List.tail |> List.rev |> List.tail |> List.rev

    // kaefer solution
    module kaefer =
        type 'a State = Zero | One | Other of 'a

        let skipFirstAndLast xss =
            let rec aux acc = function
            | _,            []    -> List.rev acc
            | Zero,         x::xs -> aux acc (One, xs)
            | One,          x::xs -> aux acc (Other x, xs)
            | (Other prev), x::xs -> aux (prev :: acc) (Other x, xs)
            aux [] (Zero, xss)

    // Petr solution
    module Petr =
        let rec trimImpl ls acc =
            match ls, acc with
            | [],      _   -> acc
            | h::[],   acc -> List.rev acc
            | h::n::t, []  -> trimImpl t [n]
            | h::t,    acc -> trimImpl t (h::acc)

        let trim ls = trimImpl ls []

    // NonIdiomatic solution
    module NonIdiomatic =
        let trim (hint : int) (ls : 'T list) =
            // trims last of rest

            // Can't ask for ls.Length as that is O(n)
            let ra = ResizeArray<_> (hint)

            // Can't use for x in list do as it relies on .GetEnumerator ()
            let mutable c = ls
            while not c.IsEmpty do
                ra.Add c.Head
                c <- c.Tail

            let count = ra.Count

            let mutable result = []
            for i in (count - 2)..(-1)..1 do
                result <- ra.[i]::result
            result

open so29100251

type Time = MilliSeconds of int64

type TestKind<'T> =
     | Functional               of 'T
     | MeasurePerformance       of int*int

[<EntryPoint>]
let main argv =
    let factor  = 10000000
//    let maxHint = Int32.MaxValue
    let maxHint = 100

    let time (action : unit -> 'T) : 'T*Time =
        let sw = Stopwatch ()

        sw.Start ()

        let r = action ()

        sw.Stop ()

        r, MilliSeconds sw.ElapsedMilliseconds

    let adapt fn hint ls = fn ls

    let trimmers =
        [|
            "Daystate"      , adapt Daystate.trim
            "kaefer"        , adapt kaefer.skipFirstAndLast
            "Petr"          , adapt Petr.trim
            "NonIdiomatic"  , NonIdiomatic.trim
        |]


#if DEBUG
    let functionalTestCases =
        [|
            Functional []               , "empty"       , []
            Functional []               , "singleton"   , [1]
            Functional []               , "duoton"      , [1;2]
            Functional [2]              , "triplet"     , [1;2;3]
            Functional [2;3]            , "quartet"     , [1;2;3;4]
        |]

    let performanceMeasurements = [||]
#else
    let functionalTestCases = [||]

    let performanceMeasurements =
        [|
            "small"   , 10
            "big"     , 1000
            "bigger"  , 100000
//            "huge"    , 10000000
        |] |> Array.map (fun (name, size) -> MeasurePerformance (size, (factor / size))  , name       , [for x in 1..size -> x])
#endif

    let testCases =
        [|
            functionalTestCases
            performanceMeasurements
        |] |> Array.concat


    use tsv = File.CreateText ("result.tsv")

    tsv.WriteLine (sprintf "TRIMMER\tTESTCASE\tSIZE\tHINT\tRUNS\tMEMORY_BEFORE\tMEMORY_AFTER\tGC_TIME\tRUN_TIME")

    for trimName, trim in trimmers do
        for testKind, testCaseName, testCase in testCases do
            match testKind with
            | Functional expected ->
                let actual = trim 0 testCase
                if actual = expected then
                    printfn "SUCCESS: Functional test of %s trim on testcase %s successful" trimName testCaseName
                else
                    printfn "FAILURE: Functional test of %s trim on testcase %s failed" trimName testCaseName
            | MeasurePerformance (size,testRuns) ->

                let hint    = min size maxHint

                let before  = GC.GetTotalMemory(true)

                printfn "MEASURE: Running performance measurement on %s trim using testcase %s..." trimName testCaseName

                let timeMe () =
                    for x in 1..testRuns do
                        ignore <| trim hint testCase
                let _, MilliSeconds ms = time timeMe

                let after   = GC.GetTotalMemory(false)

                let timeGC () =
                    ignore <| GC.GetTotalMemory(true)
                let _, MilliSeconds msGC = time timeMe

                printfn "...%d ms (%d runs), %d (before) %d (after) %d ms (GC)" ms testRuns before after msGC

                tsv.WriteLine (sprintf "%s\t%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d" trimName testCaseName size hint testRuns before after msGC ms)

    0

然后我测量了x64上的执行时间和GC时间以及允许的最大大小提示:(大小提示仅由非惯用版本使用)

x64 maxhint

允许x86和max size提示: x86 maxhint

允许x64和最大100提示: x64提示100

允许x86和最大100提示: x86提示100

看一下性能图表,我们可以注意到一些令人惊讶的事情:

  1. 所有变体都迭代10000000次。 人们会期望不同变体之间的执行时间没有差别,但确实如此。
  2. 硬壳老旧的x86总体上得分更高。 我不会推测为什么。
  3. OP的初始版本虽然看似浪费得分相当不错。 它可能有助于List.rev非常优化(IIRC它只为F#开发人员做了一些安全的作弊)
  4. kaefer版本虽然在纸面上更好的解决方案似乎得分最差。 我认为这是因为它分配了基于堆的额外State对象。 (这显然不应被解释为对kaefers技能的批评)
  5. 非惯用解决方案在良好的大小提示下得分良好,但不如我预期的那么好。 可能是构建最终列表是大多数周期的成本。 也可能是列表上的尾递归函数比while循环更有效,因为IIRC模式匹配比调用List.Tail / List.Head / List.IsEmpty更有效。
  6. GC时间几乎与执行时间一样大。
  7. 我预计非惯用解决方案的GC时间明显低于其余时间。 但是,虽然ResizeArray <_>可能很快收集列表对象不是。
  8. 在x86 arch上,Petr解决方案与非惯用解决方案之间的性能差异可能无法保证额外的复杂性。

最后的一些想法:

  1. OP的原始解决方案做得非常好
  2. 垃圾收集需要时间
  3. 总是测量......

希望它有点有趣

编辑:GC性能测量数字不应过度解释为:“GC可能很昂贵”

后来我在一个列表中从while循环更改为tail-recursion,这确实提高了性能但不足以保证更新图表。

这是方法之一:

let rec trim ls acc =
    match ls, acc with
    | [],      _   -> acc
    | h::[],   acc -> List.rev acc
    | h::n::t, []  -> trim t [n]  
    | h::t,    acc -> trim t (h::acc)

let reslt = trim ls []

您不需要标准库函数来实现这一点,您只需要一种有效的方法。 使用保存中间结果的累加器定义递归函数将成为可行的解决方案,即使列表必须在其终止时被反转。

我提供了一个自定义的Discriminated Union来跟踪状态,这是根据Option type的线条建模的。

type 'a State = Zero | One | Other of 'a

let skipFirstAndLast xss =
    let rec aux acc = function
    | _,            []    -> List.rev acc
    | Zero,         x::xs -> aux acc (One, xs)
    | One,          x::xs -> aux acc (Other x, xs)
    | (Other prev), x::xs -> aux (prev :: acc) (Other x, xs)
    aux [] (Zero, xss)

[1..4] |> skipFirstAndLast // val it : int list = [2; 3]

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM