10.2.2 有效地处理列表

10.2.2 有效地处理列表

 

    尾递归函数通常是首先提高一点效率,但通常选择算法比其实施的微优化更为重要。 让我们演示一个例子,把元素添加到现有的清单中。

 

添加元素到列表

 

    到目前为止,我们已经看到了如何追加元素,以现有的(函数式)列表的前面。如果我们想在列表的末尾追加元素?这听起来像一个合理的要求,所以,让我们尝试实现它。清单 10.10 显示了在列表的前面和后面插入之间性能的不同。

 

Listing 10.10 Adding elements to a list (F# Interactive)

 

> let prepend el list = el::list;;
val prepend : 'a -> 'a list -> 'a list

> let rec append el list =
     match list with
     | [] -> [el]
     | x::xs -> x::(append el xs)
val append : 'a -> 'a list -> 'a list

> #time;;
> let l = [ 1 .. 30000 ];;
val l : int list

> for i = 1 to 100 do ignore(prepend 1 l);;
Real: 00:00:00.000, CPU: 00:00:00.000

> for i = 1 to 100 do ignore(append 1 l);;
Real: 00:00:00.434, CPU: 00:00:00.421

 

    prepend 的实现的时间士敏土可以忽略,因为,我们可以简单地构造一个新的列表,细胞使用 cons 操作(::)。而追加一个元素到列表的末尾需要编写递归函数。这符合正常的递归列表处理模式 ,一种情况 是空列表,另一个是 cons cell。

    接下来,我们键入一个非常有用的 F# Interactive 命令,#time,打开了遍野器。在这种模式下,F# 会自动打印执行键入的命令的时间。我们可以看到,在大列表的末尾追加元素慢得多。我们在一个 for 循环中运行一百次,在前面追加所需的时间仍为零,但在后面追加元素,需要一个相当长的时间。任何只需要半秒钟“简单”的操作,迭代百次就是一个问题。

    我们的追加函数不是尾递归,但这不是问题。尾递归有助于我们避免堆栈溢出,但它只会轻微影响性能。问题是函数式列表是不适合我们尝试执行的操作。

    图 10.5 显示了此操作对于函数式列表,为什么不能有效地实现,而追加一个元素到前面很容易。因为,列表是不可变的数据结构,我们可以创建一个单独的单元,引用原始列表。不可变性保证以后没有人能改变原始列表,背着我们改变“新”列表的内容。与此相比,追加元素到后面,需要改变最后一个元素。以前,最后一个元素的“知道”它是在最后,而我们需要在它的后面有新的元素。这个列表是不可变的,所以,不能 改变存储在最后一个元素中的信息。相反,我们必须克隆最后一个元素,这也意味着,克隆前面的元素(这样,它就知道,它后面跟着克隆的最后一个元素),等等。

10-5

图 10.5 当在前面追加元素,我们创建了一个新的 cons cell 和 原始列表的引用。要在后面追加元素,我们需要遍历并克隆整个列表。

 

    当然,有不同的数据结构,每一种都有不同的操作可以有效地执行。总有一个权衡,这就是为什么对于不同的问题,选择正确的数据结构,是非常重要的。

 

算法的复杂性

 

    计算机科学家使用非常精确的数学术语来讨论算法的复杂性,但这些术语背后的概念更重要的,即使当我们非正式使用时。在一般情况下,操作的复杂性告诉我们,算法需要的“原始”步骤数,依赖于输入的大小。它并不预测步骤数,只与输入的大小有关系。

    让我们来分析一下前面的例子。追加一个元素到列表前面,总是涉及到一个单独的步骤:创建一个新的列表 cons cell。在正规表示法中,写成 O(1),这意味着,步数是不变的,不管列表如何大。在有一百万个元素的列表的前面添加元素,与在只有一个元素的列表前面添加元素花费一样!

    在列表的后面追加元素是棘手。开始的时候,如果列表中有 N 个元素,我们需要处理和重复 N 个 cons cell。这可写成 O(N),表示步骤数大致与列表的大小是成正比:在有 1000 个元素的列表后面添加元素大约是为在有 1000 个元素的列表后面添加元素的两倍。

    例如,如果我们想追加 M 个新元素列表中的,其复杂性应该乘以 M。这意味着,在前面追加将需要 O(M) 步,因为,1*M = M。使用类似的道理,追加到后面将需要 O(N * M) 步,这可能是一个更大的数量级。

 

    到目前为止,我们已经讨论了函数式列表,在函数编程中是最重要的集合。让我们来一个大的飞跃,看一看在几乎所有命令式编程语言都存在的集合:不起眼的数组。F# 是一种 .NET 语言,所以,它也可以使用正常 .NET 数组。

你可能感兴趣的:(职场,列表,休闲,处理列表)