SCALA 集合框架文档翻译-Concrete Immutable Collection Classes

来源:互联网 发布:易观是什么软件 编辑:程序博客网 时间:2024/06/05 23:01

具体不可变集合类

英文原文:http://docs.scala-lang.org/overviews/collections/concrete-immutable-collection-classes.html


        Scala 提供了许多可供选择的具体不可变集合类。他们根据对(maps、sets、sequence)这些特质的具体实现不同可以是无限的也可以是有限的,各种操作的速度也不一样。下列是 Scala 中最常用的一些不可变具体集合类。

Lists

        List 是一个有限长度不可变序列。对 List 的首个元素和除首个元素后的剩余元素列表的访问操作是常数时间复杂度的,同时对在List 首位添加元素的操作也是常数时间复杂度的。还有很多其他操作时线性时间复杂度的。

        List 一直是 scala 编程中的主力数据结构,因此没有必要在这里多费口舌。2.8 版本中最大的变化是 List 类和它的子类 :: 以及子对象 Nil 现在定义在了 scala.collection.immutable 包中,这样逻辑上更加合理。在 scala 包中仍然有 List、Nil 以及 :: 的别名定义,因为从使用者的角度他们可以像以前那边被访问。

        另外一个变化是List与整个集合框架整合得更加紧密了,而不是像以前那样作为一个特例来处理。例如,原来定义在 List 半生对象中的许多方法都不建议使用了。这些方法由被各个集合所继承的统一创建方法所替代。

Streams

        Stream 就与 List 比较类似,只不过他的元素是延迟计算的。正因为如此,Stream 才可以无限长。只有被请求的元素才被计算出来。另外,Stream 的性能特征与 List 相同。

        List 可以用 :: 操作符构建,而 Stream 可以用类似的 #:: 操作符构建。下面是一个简单的包含 1、2、3 三个整数的 Stream 例子。

scala> val str = 1#::2#::3#::Stream.emptystr: scala.collection.immtable.Stream[Int] = Stream(1, ?)

这个 Stream 的头是 1,尾部列表包含 2 和 3。因为这里的尾部还有计算,所以没有打印出来。Stream 的元素是延迟计算的,而且 Stream 的 toString 方法不会触发 Stream 的元素的计算。

        下面是一个复杂一点的例子。它用 Stream 来计算给定前两个元素的 Fibonacci 数列。一个 Fibonacci 数列满足序列中任意一个元素的值是前两个元素之和。

scala> def fibFrom(a: Int, b: Int): Stream[Int] = a#::fibFrom(b, a+b)fibFrom: (a: Int, b: Int)Stream[Int]

这个函数看起来很容易。这个序列的首个元素很清楚是 a,并且序列的剩余部分是一个以 b、a+b 开头的 Fibonacci 数列。微妙的地方在于计算这个序列不会导致无限循环。如果函数中用 :: 替代 #:: 操作符,那么对函数的每一次调用都会触发另一次调用,这将会导致无限递归。因此,这里使用 #:: 操作符,这样的话右手边的值除非被请求否则不会被计算。下面是以两个 1 开头的 Fibonacci 数列的前面一些元素。

scala> val fibs = fibFrom(1, 1).take(7)fibs: scala.collection.immutable.Stream[Int] = Stream(1, ?)scala> fibs.toListres9: List[Int] = List(1, 1, 2, 3, 5, 8, 13)

Vectors

        但算法只处理序列的首部元素时,List 是非常高效的。访问、添加和删除 List 的首部元素都只需要常数时间,但是访问 List 中首部外的元素需要正比于被访问元素深度的线性时间复杂度。

        Vector (从 scala 2.8 中引入) 这种集合类型很好的解决了 List 上随机访问操作低效的问题。Vector 类上访问任何元素都是常数时间复杂度。这里的常数要比访问 List 的首部或者是访问数组中的元素时的时间复杂度常数更大,但是仍然是一个常数时间复杂度。因此,使用 Vector 的算法无需像 List 一样为了高效而注意只访问序列的首部。他们能访问和修改任意位置上的元素,从而也更便于程序编写。

        Vector 的构建和修改方式与其他序列类型一样。

scala> val vec = scala.collection.immutable.Vector.emptyvec: scala.collection.immutable.Vector[Nothing] = Vector()scala> val vec2 = vec :+ 1 :+ 2vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2)scala> val vec3 = 100 +: vec2vec3: scala.collection.immutable.Vector[Int] = Vector(100, 1, 2)scala> vec3(0)res1: Int = 100

        Vector 内部表示为一个高分支因子的树(一棵树或图的分支因子是每个节点上子节点的数目)。每个树节点包含最多32个元素或者最多32个其他树节点。最多含有32个元素的 Vector 可以用单个节点表示。最多含有 32*32 个元素的 Vector 可以表示为一个两层的树。从树的根节点到最终的元素节点经过两跳就足够表示最多 2^15 个元素的 Vector,经过3跳就足够表示最多含有 2^20 个元素的 Vector,四跳就足够表示最多 2^25 个元素的 Vector,5跳就足够表示最多含有 2^30 个元素的 Vector。因此,对于所有合理长度的 Vector 来说,一个元素的选择操作涉及到最多 5 次数组的选择操作。这就是所说的“常数时间”复杂度的意思。

        Vector 是不可变的,因此无法原地改变当前 Vector 中的元素。但是,利用 updated 方法可以创建一个与所给定 vector 只有一个元素差异的新的 Vector。

scala> val vec = Vector(1, 2, 3)vec: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)scala> vec updated (2, 4)res0: scala.collection.immutable.Vector[Int] = Vector(1, 2, 4)scala> vecres1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

如上最后一行所显示,可以知道对 updated 的调用对原 Vector 没有任何效果。就像元素选择,函数式的 Vector 更新同样是“常数时间”复杂度的。更新 Vector 中的一个元素可以通过拷贝含有这个元素的节点以及从树根开始指向这个节点的所有节点。这意味着一个次函数式的更新将创建 1 至 5 个节点,每个节点包含最多 32 个元素或者子节点。这相对于可变集合里的数组当然是开销大些,但是比起拷贝整个数组来说还是开销少很多。

        由于 Vector 在快速随机访问和快速随机函数式更新之间做了一个很好的权衡,因此它被作为不可变索引序列集合类的默认实现类。

scala> collection.immutable.IndexedSeq(1, 2, 3)res2: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)

Immutable stacks

        如果你需要一个后进先出的序列,可以选择使用Stack。使用 push 操作将元素推入栈中,使用 pop 操作将元素弹出栈,使用 top 操作选择栈顶元素但是不弹出元素。所有这些操作都是常数时间复杂度的。

        下面是 Stack 上的一些简单操作。

scala> val stack = scala.collection.immutable.Stack.emptystack: scala.collection.immutable.Stack[Nothing] = Stackscala> val hasOne = stack.push(1)hasOne: scala.collection.immutable.Stack[Int] = Stack(1)scala> stackstack: scala.collection.immutable.Stack[Nothing] = Stack()scala> hasOne.topres20: Int = 1scala> hasOne.popres19: scala.collection.immutable.Stack[Int] = Stack()

不可变的 Stack 在 scala 编程中很少使用,因为他们的功能被归入到 List 中:一个不可变 Stack 上的 push 操作等同于 List 上的 :: 操作,pop 操作等同于 List 上的 tail 操作。

Immutable Queues

        Queue 是一个先进先出的序列,这点与 Stack 不同。

        下面描述如何创建一个不可变 Queue:

scala> val empty = scala.collection.immutable.Queue[Int]()empty: scala.collection.immutable.Queue[Int] = Queue()

可以使用 enqueue 操作将元素加入队列尾部:

scala> val has1 = empty.enqueue(1)has1: scala.collection.immutable.Queue[Int] = Queue(1)

如果想将多个元素加入队列尾部,可以将一个集合作为 enqueue 的参数:

scala> val has123 = has1.enqueue(List(2, 3))has1: scala.collection.immutable.Queue[Int] = Queue(1, 2, 3)

可以使用 dequeue 操作移除队列首部元素:

scala> val (element, has23) = has123.dequeueelement: Int = 1has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)

注意 dequeue 操作返回一个元组包括被移除的元素以及剩余的队列。

Ranges

    一个 Range 是一个等距间隔的有序整数序列。例如,“1, 2, 3” 是一个 Range,“5, 8, 11, 14” 也是一个 Range。在 scala 中创建 Range,可以使用 to 以及 by 操作。

scala> 1 to 3res2: scala.collection.immutable.Range.Inclusive with scala.collection.immutable.Range.ByOne = Range(1, 2, 3)scala> 5 to 14 by 3res3: scala.collection.immutable.Range = Range(5, 8, 11, 14)

如果创建时不需要其上限值,则可以使用更加便利的 until 操作替换 to 操作:

scala> 1 until 3res2: scala.collection.immutable.Range.Inclusive with scala.collection.immutable.Range.ByOne = Range(1, 2)

Range 使用常数空间就可以存储,因为他们可以由三个数进行定义:开始整数、结束整数、步长。由于这种表示方式,Range 上的许多操作都非常快速。

Hash Tries

        Hash Trie 是实现高效不可变集合和映射的标准方式。他们由类 immutable.HashMap 提供实现。他们的内部表示方式与 Vector 类似,同样是多分枝的树,树的每个节点含有 32 个元素或者 32 个子树节点。但是这里元素的选择是基于哈希编码。例如,查找一个映射中的给定 Key,首先要计算出该 Key 的哈希编码。接着,哈希编码的低 5 位将用于选择第一个子树节点,之后 5 位一段一次类推。当所遍历节点中的所有元素的哈希编码中到当前层次为止所使用到的索引编码各不相同的时候选择将停止。

        Hash Trie 在合理快速查找与合理快速添加(+)和删除(-)之间做了一个很好的权衡。所以,Hash Trie 被作为 scala 中不可变 Map 和 Set 的默认实现方式。事实上,scala 在集合元素少于 5 个时对不可变 Set 和 Map 有进一步的优化。含有 1 到 4 个元素的 Set 或者 Map 将表示为一个将元素(或者 Map 中的键值对)定义为字段的单例对象。空的不可变 Set 和空的不可变 Map 分别是一个单例对象 - 没有必要重复存储这种空对象,因为他们总是空的。

Red-Black Trees

        Red-Black 树是一种平衡二叉树,其中一些节点被标为“红”,其余被标为“黑”。就像任何平衡二叉树一样,它的操作具有相对树大小的对数时间复杂度。

        scala 为不可变 Set 和 Map 提供了一种内部使用红黑树实现的方式。可以通过 TreeSet 或 TreeMap 使用红黑树实现的 Set 或 Map。

scala> scala.collection.immutable.TreeSet.empty[Int]res11: scala.collection.immutable.TreeSet[Int] = TreeSet()scala> res11 + 1 + 3 + 3res12: scala.collection.immutable.TreeSet[Int] = TreeSet(1, 3)

红黑树是 scala 中 SortedSet 的标准实现方式,它提供一个按顺序返回所有元素的高效迭代器。

Immutable BitSets

        BitSet 用一个大整数的比特位来表示一个小整数集合。例如,一个包含 3,2 和 0 的 BitSet 将表示为二进制整数 1101,也即十进制整数 13。

        在 Scala 内部,BitSet 使用一个64位长整型数组表示。数组中的第一个长整型数表示 0 到 63,第二个长整型数表示 64 到 127,以此类推。因此,只要集合中的最大整数小于几百左右,BitSet 将会很紧凑。

        BitSet 上的操作非常快速。元素包含测试操作耗费常数时间。添加一个元素到 BitSet 中耗费的时间与 BitSet 中长整型数的个数成正比,通常是一个很小的数。下面是一些使用 BitSet 的简单示例:

scala> val bits = scala.collection.immutable.BitSet.emptybits: scala.collection.immutable.BitSet = BitSet()scala> val moreBits = bits + 3 + 4 + 4moreBits: scala.collection.immutable.BitSet = BitSet(3, 4)scala> moreBits(3)res26: Boolean = truescala> moreBits(0)res27: Boolean = false

List Maps

ListMap 使用一个键值对链表来表示 Map。一般来说,ListMap 上的操作不得不迭代整个链表。因此,ListMap 上操作的时间复杂度与链表的大小成线性正比关系。事实上,ListMap 在 Scala 中的使用很少,这是由于在 scala 中标准不可变 Map 大多数情况下总是比 ListMap 要快些。唯一可能的例外情况是:当这个 Map 由于某些原因要以某种特殊方式构建,这种方式下链表首部元素被选中的次数比其他元素多得多。

scala> val map = scala.collection.immutable.ListMap(1->"one", 2->"two")map: scala.collection.immutable.ListMap[Int, java.lang.String] = Map(1->one, 2->two)scala> map(2)res30: String = "two"


0 0
原创粉丝点击