Scala之集合的理解和应用案例

  • 可变集合和不可变集合

Scala 集合类系统地区分了可变的不可变的集合;

可变集合可以在适当的地方被更新或扩展。这意味着你可以修改,添加,移除一个集合的元素。默认情况下,Scala 一直采用不可变集合类。

不可变集合类,相比之下,永远不会改变。因此,它是线程安全的;不过,你仍然可以模拟添加,移除或更新操作。但是这些操作将在每一种情况下都返回一个新的集合,同时使原来的集合不发生改变。如果你想要同时使用可变和不可变集合类,只导入collection.mutable包即可。

import scala.collection.mutable

下面的图表显示了scala.collection包中所有的集合类。这些都是高级抽象类或特性,它们通常具备和不可变实现一样的可变实现。

collections.pnguploading.4e448015.gif转存失败重新上传取消

下面的图表显示scala.collection.immutable中的所有集合类。

collections.immutable.pnguploading.4e448015.gif转存失败重新上传取消

下面的图表显示scala.collection.mutable中的所有集合类。

collections.mutable.pnguploading.4e448015.gif转存失败重新上传取消

  • Traversable

 Traversable(遍历)是容器(collection)类的最高级别特性,它唯一的抽象操作是foreach:

object TraverableDemo extends Traversable[String] {
  override def foreach[U](f: String => U): Unit = ???
}

Traversable同时定义的很多具体方法,如下表所示:

相加操作++(addition)表示把两个traversable对象附加在一起或者把一个迭代器的所有元素添加到traversable对象的尾部。

Map操作有map,flatMap和collect,它们可以通过对容器中的元素进行某些运算来生成一个新的容器。

转换器(Conversion)操作包括toArray,toList,toIterable,toSeq,toIndexedSeq,toStream,toSet,和toMap,它们可以按照某种特定的方法对一个Traversable 容器进行转换。等容器类型已经与所需类型相匹配的时候,所有这些转换器都会不加改变的返回该容器。例如,对一个list使用toList,返回的结果就是list本身。

拷贝(Copying)操作有copyToBuffer和copyToArray。从字面意思就可以知道,它们分别用于把容器中的元素元素拷贝到一个缓冲区或者数组里。

Size info操作包括有isEmpty,nonEmpty,size和hasDefiniteSize。Traversable容器有有限和无限之分。比方说,自然数流Stream.from(0)就是一个无限的traversable 容器。hasDefiniteSize方法能够判断一个容器是否可能是无限的。若hasDefiniteSize返回值为ture,容器肯定有限。若返回值为false,根据完整信息才能判断容器(collection)是无限还是有限。

元素检索(Element Retrieval)操作有head,last,headOption,lastOption和find。这些操作可以查找容器的第一个元素或者最后一个元素,或者第一个符合某种条件的元素。注意,尽管如此,但也不是所有的容器都明确定义了什么是“第一个”或”最后一个“。例如,通过哈希值储存元素的哈希集合(hashSet),每次运行哈希值都会发生改变。在这种情况下,程序每次运行都可能会导致哈希集合的”第一个“元素发生变化。如果一个容器总是以相同的规则排列元素,那这个容器是有序的。大多数容器都是有序的,但有些不是(例如哈希集合)– 排序会造成一些额外消耗。排序对于重复性测试和辅助调试是不可或缺的。这就是为什么Scala容器中的所有容器类型都把有序作为可选项。例如,带有序性的HashSet就是LinkedHashSet。

子容器检索(sub-collection Retrieval)操作有tail,init,slice,take,drop,takeWhilte,dropWhile,filter,filteNot和withFilter。它们都可以通过范围索引或一些论断的判断返回某些子容器。

拆分(Subdivision)操作有splitAt,span,partition和groupBy,它们用于把一个容器(collection)里的元素分割成多个子容器。

元素测试(Element test)包括有exists,forall和count,它们可以用一个给定论断来对容器中的元素进行判断。

折叠(Folds)操作有foldLeft,foldRight,/:,:\,reduceLeft和reduceRight,用于对连续性元素的二进制操作。

特殊折叠(Specific folds)包括sum, product, min, max。它们主要用于特定类型的容器(数值或比较)。

字符串(String)操作有mkString,addString和stringPrefix,可以将一个容器通过可选的方式转换为字符串。

视图(View)操作包含两个view方法的重载体。

  • 序列TRAIT

Seq trait用于表示序列。所谓序列,指的是一类具有一定长度的可迭代访问的对象,其中每个元素均带有一个从0开始计数的固定索引位置。

特性(trait) Seq 具有两个子特征(subtrait) LinearSeqIndexedSeq。它们不添加任何新的操作,但都提供不同的性能特点:线性序列具有高效的 head 和 tail 操作,而索引序列具有高效的apply, length, 和 (如果可变) update操作。

常用线性序列有 scala.collection.immutable.Listscala.collection.immutable.Stream

常用索引序列有 scala.Array scala.collection.mutable.ArrayBuffer

Vector 类提供一个在索引访问和线性访问之间有趣的折中。它同时具有高效的恒定时间的索引开销,和恒定时间的线性访问开销。正因为如此,对于混合访问模式,vector是一个很好的基础。

  • 缓冲器

Buffers是可变序列一个重要的种类。它们不仅允许更新现有的元素,而且允许元素的插入、移除和在buffer尾部高效地添加新元素。buffer 支持的主要新方法有:用于在尾部添加元素的 += 和 ++=;用于在前方添加元素的+=: ++=: ;用于插入元素的 insertinsertAll;以及用于删除元素的remove 和 -=。如下表所示。

ListBuffer和ArrayBuffer是常用的buffer实现 。顾名思义,ListBuffer依赖列表(List),支持高效地将它的元素转换成列表。而ArrayBuffer依赖数组(Array),能快速地转换成数组。

  • 集合Set

集合是不包含重复元素的可迭代对象。

与不变集合一样,可变集合也提供了+++操作符来添加元素,---用来删除元素。但是这些操作在可变集合中通常很少使用,因为这些操作都要通过集合的拷贝来实现。可变集合提供了更有效率的更新方法,+=-=。 s += elem,添加元素elem到集合s中,并返回产生变化后的集合作为运算结果。同样的,s -= elem 执行从集合s中删除元素elem的操作,并返回产生变化后的集合作为运算结果。除了+=-=之外还有从可遍历对象集合或迭代器集合中添加和删除所有元素的批量操作符++=--=

可变集合同样提供作为 += 和 -= 的变型方法,add 和 remove,它们的不同之处在于 add 和 remove 会返回一个表明运算是否对集合有作用的Boolean值.

目前可变集合默认使用哈希表来存储集合元素,非可变集合则根据元素个数的不同,使用不同的方式来实现。空集用单例对象来表示。元素个数小于等于4的集合可以使用单例对象来表达,元素作为单例对象的字段来存储。 元素超过4个,非可变集合就用哈希前缀树(hash trie)来实现。

采用这种表示方法,较小的不可变集合(元素数不超过4)往往会比可变集合更加紧凑和高效。所以,在处理小尺寸的集合时,不妨试试不可变集合。

  • 有序集(SortedSet)

SortedSet 是指以特定的顺序(这一顺序可以在创建集合之初自由的选定)排列其元素(使用iterator或foreach)的集合。 SortedSet 的默认表示是有序二叉树,即左子树上的元素小于所有右子树上的元素。这样,一次简单的顺序遍历能按增序返回集合中的所有元素。Scala的类 immutable.TreeSet 使用红黑树实现,它在维护元素顺序的同时,也会保证二叉树的平衡,即叶节点的深度差最多为1。

  • 位集合(Bitset)

待续...

  • 映射Map

映射(Map)是一种可迭代的键值对结构(也称映射或关联)。Scala的Predef类提供了隐式转换,允许使用另一种语法:key -> value,来代替(key, value)。如:Map("x" -> 24, "y" -> 25, "z" -> 26)等同于Map(("x", 24), ("y", 25), ("z", 26)),却更易于阅读。

  • 不可变的实体类

Scala中提供了多种具体的不可变集类供你选择,这些类(maps, sets, sequences)实现的接口(traits)不同,比如是否能够是无限(infinite)的,各种操作的速度也不一样

  • List(列表)

列表List是一种有限的不可变序列式。提供了常数时间的访问列表头元素和列表尾的操作,并且提供了常数时间的构造新链表的操作,该操作将一个新的元素插入到列表的头部。其他许多操作则和列表的长度成线性关系。

  • Stream(流)

Stream与List很相似,只不过其中的每一个元素都经过了一些简单的计算处理。也正是因为如此,stream结构可以无限长。只有那些被要求的元素才会经过计算处理,除此以外stream结构的性能特性与List基本相同。

  • Vector(向量)

对于只需要处理数据结构头结点的算法来说,List非常高效。可是相对于访问、添加和删除List头结点只需要固定时间,访问和修改头结点之后元素所需要的时间则是与List深度线性相关的。

向量Vector是用来解决列表(list)不能高效的随机访问的一种结构。Vector结构能够在“更高效”的固定时间内访问到列表中的任意元素。虽然这个时间会比访问头结点或者访问某数组元素所需的时间长一些,但至少这个时间也是个常量。因此,使用Vector的算法不必仅是小心的处理数据结构的头结点。由于可以快速修改和访问任意位置的元素,所以对Vector结构做写操作很方便。

Vector结构通常被表示成具有高分支因子的树(树或者图的分支因子是指数据结构中每个节点的子节点数目)。每一个树节点包含最多32个vector元素或者至多32个子树节点。包含最多32个元素的vector可以表示为一个单一节点,而一个间接引用则可以用来表示一个包含至多32*32=1024个元素的vector。从树的根节点经过两跳到达叶节点足够存下有2的15次方个元素的vector结构,经过3跳可以存2的20次方个,4跳2的25次方个,5跳2的30次方个。所以对于一般大小的vector数据结构,一般经过至多5次数组访问就可以访问到指定的元素。这也就是我们之前所提及的随机数据访问时“运行时间的相对高效”。

由于Vectors结构是不可变的,所以您不能通过修改vector中元素的方法来返回一个新的vector。尽管如此,您仍可以通过update方法从一个单独的元素中创建出区别于给定数据结构的新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> vec
res1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

由于vector在快速随机选择和快速随机更新的性能方面做到很好的平衡,所以它目前正被用作不可变索引序列的默认实现方式。

  • Immutable stacks(不可变栈)

 如果您想要实现一个后入先出的序列,那您可以使用Stack。您可以使用push向栈中压入一个元素,用pop从栈中弹出一个元素,用top查看栈顶元素而不用删除它。所有的这些操作都仅仅耗费固定的运行时间。

不可变stack一般很少用在Scala编程中,因为List结构已经能够覆盖到它的功能:push操作同List中的::基本相同,pop则对应着tail。

  • Immutable Queues(不可变队列)

Queue是一种与stack很相似的数据结构,除了与stack的后入先出不同,Queue结构的是先入先出的。

  • Ranges (等差数列)

 [Range]表示的是一个有序的等差整数数列。比如说,“1,2,3,”就是一个Range,“5,8,11,14,”也是。在Scala中创建一个Range类,需要用到两个预定义的方法to和by。

  • 具体的可变容器类

  • Array Buffers

一个ArrayBuffer缓冲包含数组和数组的大小。对数组缓冲的大多数操作,其速度与数组本身无异。因为这些操作直接访问、修改底层数组。另外,数组缓冲可以进行高效的尾插数据。追加操作均摊下来只需常量时间。因此,数组缓冲可以高效的建立一个有大量数据的容器,无论是否总有数据追加到尾部。

  • List Buffers

ListBuffer 类似于数组缓冲。区别在于前者内部实现是链表, 而非数组。如果你想把构造完的缓冲转换为列表,那就用列表缓冲,别用数组缓冲。

  • StringBuilders

数组缓冲用来构建数组,列表缓冲用来创建列表。类似地,StringBuilder 用来构造字符串。作为常用的类,字符串构造器已导入到默认的命名空间。直接用 new StringBuilder就可创建字符串构造器 ,

  •  链表

链表是可变序列,它由一个个使用next指针进行链接的节点构成。它们的支持类是LinkedList。在大多数的编程语言中,null可以表示一个空链表,但是在Scalable集合中不是这样。因为就算是空的序列,也必须支持所有的序列方法。尤其是 LinkedList.empty.isEmpty 必须返回true,而不是抛出一个 NullPointerException 。空链表用一种特殊的方式编译: 

它们的 next 字段指向它自身。链表像他们的不可变对象一样,是最佳的顺序遍历序列。此外,链表可以很容易去插入一个元素或链接到另一个链表。

  • 数组

在Scala中,数组是一种特殊的collection。一方面,Scala数组与Java数组是一一对应的。即Scala数组Array[Int]可看作Java的Int[],Array[Double]可看作Java的double[],以及Array[String]可看作Java的String[]。但Scala数组比Java数组提供了更多内容。首先,Scala数组是一种泛型。即可以定义一个Array[T],T可以是一种类型参数或抽象类型。其次,Scala数组与Scala序列是兼容的 - 在需要Seq[T]的地方可由Array[T]代替。最后,Scala数组支持所有的序列操作。

请关注作者公众号:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章