Scala学习(十三)集合

1.主要集合特质

构成Scala集合继承层级最重要的特质

image2018-10-9_18-19-38.png?version=1&modificationDate=1539080376000&api=v2

Iterable指的是那些能够生成用来访问集合中所有元素的Iterator的集合

Seq是一个有先后次序的值得序列,比如数组或列表。IndexedSeq允许我们通过整型的下标快速地访问任意元素。举例来说ArrayBuffer是带下标的,但链表不是。

Set是一组没有先后次序的值。在SortedSet中,元素以某种排过序的顺序被访问。

Map是一组对偶。SortedMap按照键的排序访问其中的实体。

这个继承层级和Java很相似,同时有些不错的改进:

  1. 映射隶属于同一继承层级而不是一个单独的层级关系。
  2. IndexedSeq是数组的超类型,但不是列表的超类型,以便于区分。

每个Scala集合特质或类都有一个带有apply方法的伴生对象,这个apply方法可以用来构建该集合的实例,而不用使用new,这样的设计叫做"统一创建原则"。

2.可变和不可变集合

Scala同时支持可变和不可变的集合。Scala优先采用不可变集合,因此你可以安全的共享其引用。任何对不可变集合的修改操作都返回的是一个新的不可变集合,它们共享大部分元素。

3.序列

最重要的不可变序列

image2018-10-9_18-31-5.png?version=1&modificationDate=1539081063000&api=v2

Vector是ArrayBuffer的不可变版本,和C++的Vector一样,可以通过下标快速的随机访问。而Scala的Vector是以树形结构的形式实现的,每个节点可以有不超过32个子节点。这样对于有100万的元素的向量而言,只需要4层节点。
Range表示一个整数序列,例如0,1,2,3,4,5 或 10,20,30 . Rang对象并不存储所有值而只是起始值、结束值和增值。

最有用的可变序列

image2018-10-9_18-32-38.png?version=1&modificationDate=1539081156000&api=v2

我们之前介绍了数组缓冲。而栈、队列、优先级队列等都是标准的数据结构,用来实现特淡定的算法。但是Scala链表类有些特殊,它们和Java中或数据结构课程中的接触到的连表不大一样。

4.列表

 

在Scala中,列表要么是Nil(空列表),要么是一个head元素加上一个tail,而tail又是一个列表。例如:

val digits = List(4,2)
digits.head           // 4
digits.tail              // List(2)
digits.tail.head     // 2
digits.tail.tail        // Nil

::操作符从给定的头和尾创建一个新的列表。例如:

​​​​​​​9 :: List(4, 2) // List(9,4,2)
// 等同于
9 :: 4 :: 2 :: Nil // 这里是又结合的

注意::是右结合的。通过::操作符,列表姜葱末端开始构建。

遍历链表:可以使用迭代器、递归或者模式匹配

def sum(lst: List[Int]): Int = {
    if (lst == Nil) 0 else lst.head + sum(lst.tail)

def sum(lst: List[Int]): Int = lst match {
    case Nil => 0
    case h :: t => h + sum(t)    //h 是lst.head 而t是lst.tail
}

注意第二种遍历模式中的::操作符,它将列表"析构"成了头部和尾部。

说明:递归之所以这么自然,是因为列表的尾部正好又是一个列表。

如果你想要当场修改可变列表元素,你可以用ListBuffer,这是一个由链表支撑的数据结构,包含一个纸箱最后一个节点的引用。这让它可以高效地从任意一端添加或移除元素。

不过在ListBuffer中添加或移除元素并不高效,这时你可以考虑Java的LinkedList。

说明:Scala中LinkedListheDoubleLinkedList类(他们都已经过时了)还有一个内部的MutableList类,这些是你不应该用的。

5.集

集是不重复的元素的集合。尝试将已有元素加入没有效果。例如:

Set(2, 0, 1) + 1 和 Set(2, 0, 1) 是一样的

和列表不同,集并不保留元素插入的顺序,默认情况下,集以哈希集实现。

其元素根据hashCode方法的值进行组织。(Scala和Java一样。每个对象都有hashCode方法)

在哈希集中查找元素要比在数组或列表中快得多。

而链式哈希集可以记住元素插入的顺序,它会维护一个链表来达到这个目的。

val weekdays = scala.collection.mutable.LinkedHashSet("Mo", "Tu", "We", "Th", "Fr")

对于SortedSet已排序的集使用红黑树实现的。

位集(bit set)是集的一种实现,以一个字节序列的方式存放非负整数。如果集中有i,则第i个字位是1.这是很搞笑的实现,只要最大元素不是特别的大。Scala提供了可变的和不可变的两个BitSet类。

集的一些常见操作:contains方法检查某个集是否包含给定的值。subsetOf 方法检查某个集当中的所有元素是否都被另一个集包含。

val digits = Set(1,7,2,9)
digits contains 0 // false
Set(1, 2) subsetOf digits // true

val primes = Set(2,3,5,7)

digits union primes // Set(1,2,3,5,7,9)
// 等同于
digits | primes // 或 digits ++ primes Set(1,2,3,5,7,9)

digits intersect primes // Set(2, 7)
// 等同于
digits & primes // Set(2, 7)

digits diff primes // Set(1, 9)
// 等同于
digits -- primes // 或digits &~ primes Set(1, 9)

6.用于添加或去除元素的操作符

下面展示了为各种集合类型定义的用于添加或去除元素的操作符

image2018-10-9_19-14-3.png?version=1&modificationDate=1539083642000&api=v2

image2018-10-9_19-14-3.png?version=1&modificationDate=1539083642000&api=v2

一般而言, + 用于将元素添加到无先后次序的集合,而+: 和 :+ 则是将元素添加到有先后次序的集合的开头或是结尾。

Vector(1,2,3) :+ 5 // Vector(1,2,3,5)
1 +: Vector(1,2,3) // Vector(1,1,2,3)

如你所见,Scala提供了许多用于添加和移除元素的操作符。以下是一个汇总:

  1. 向后(:+)或向前(+:)追加元素到序列当中。
  2. 添加(+)元素到无先后次序的集合中。
  3. 用 - 移除元素。
  4. 用++和–来批量添加和移除元素。
  5. 对于列表,有限使用 :: 和 ::: 。
  6. 改值操作有 +=、++=和 --=。
  7. 对于集合,我更喜欢++、&和–。
  8. 我尽量不用++:、+=:和++=:。

说明:对于列表,你可以用+:而不是::来保持与其他集合操作的一致性,但有一个例外,匹配模式不认 +: 操作符。

7.常用方法

下表给出了Iterator最重要方法的概览,按功能点排序。

image2018-10-10_10-52-19.png?version=1&modificationDate=1539139936000&api=v2

image2018-10-10_10-54-21.png?version=1&modificationDate=1539140058000&api=v2

Seq特质又在Iterator的基础上新添加了方法。

image2018-10-10_10-58-33.png?version=1&modificationDate=1539140309000&api=v2

image2018-10-10_10-58-50.png?version=1&modificationDate=1539140327000&api=v2

注:这些方法从不改变原有集合。它们返回与元集合相同类型的新集合。这有时被叫做“统一返回类型原则“。

8.将函数映射到集合

map方法可以将某个函数应用到集合的每一个元素并产出其结果的集合。例如:

val names = List("Peter", "Paul", "Mary")

names.map(_.toUpperCase) // List("PETER", "PAUL", "MARY")

// 等同于
for (n <- names) yield n.toUpperCase

如果函数产出一个集合而不是单个值得话,则使用flatMap将所有的值串接在一起。例如:

def ulcase(s: String) = Vector(s.toUpperCase(), s.toLowerCase())

names.map(ulcase) // List(Vector("PETER", "peter"), Vector("PAUL", "paul"), Vector("MARY", "mary"))

names.flatmap(ulcase) // List("PETER", "peter", "PAUL", "paul", "MARY", "mary")

transform方法是map的等效操作,只不过是当场执行(而不是交出新的集合)。它应用于可变集合并将每个元素都替换成函数的结果。如下代码将所有的缓冲区元素改成大写:

val buffer = ArrayBuffer("Petter","Paul","Mary")

buffer.transform(_.toUpperCase)

collect方法用于偏函数---并没有对所有可能的输入值进行定义的函数。例如:
 

"-3+4".collect {case '+' => 1; case '-' => -1} // Vector(-1,1)

"-3+4".collect {case '-' => -1} // Vector(-1)

"-3+4".collect {case '+' => 1} // Vector(1)

"-3+4".collect {case '*' => 1} // Vector()

groupBy方法交出的是这样一个映射:他的键是函数(求值后)的值,而是那些函数求值得到给丁健的元素集合。例如:

val words = ...

val map = words.groupBy(_.substring(0,1).toUpperCase)

foreach方法将函数应用到各个元素但不关心函数的返回值。

names.foreach(println)

9.化简、折叠和扫描

reduceLeft、reduceRight、foldLeft、foldRight、scanLeft、scanRight方法将会用二元函数来组合集合中的元素:

List(1,7,2,9).reduceLeft(_ - _) // ((1 - 7) - 2) - 9
List(1,7,2,9).reduceRight(_ - _) // 1 - (7 - (2 - 9))

List(1,7,2,9).foldLeft(0)(_ - _) // 可用 /: 代替,相当于(0 /: List(1,7,2,9)) 或者 0 - 1 -7 - 2 - 9
List(1,7,2,9).foldRight(0)(_ - _) // 可用 :\ 代替,相当于(List(1,7,2,9) :\ 0) 或者 1 - (7 - (2 - (9 - 0)))

说明:初始值和操作符是两个分开的柯里化参数,这样Scala就能用初始值的类型来推断出操作符的类型定义。举例来说:在List(1,7,2,9).foldLeft("")(_ + _)中,初始值是一个字符串,因此操作符必定是一个类型定义为(String,Int) => String的函数。

(1 to 10).scanLeft(0)(_ + _) // Vector(0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55)
(1 to 10).scanRight(0)(_ + _) // Vector(55, 45, 36, 28, 21, 15, 10, 6, 3, 1, 0)

说明:任何while循环都可以用折叠来代替。构建一个把循环中被更新的所有变量结合在一起的数据结构,然后定义一个操作,实现循环中的一步。我并不是说这样做总是好的,但你可能觉得循环和改值可以像这样被消除是一件很有趣的事。

10.拉链操作

前面的章节已经讲过拉链操作。除了zip方法外,还有zipAll和zipWithIndex方法

List(5.0, 20,0, 9.7, 3.1, 4.3).zipAll(List(10, 2), 0.0, 1) // List((5.0, 10), (20.0, 2), (9.7, 1), (3.1, 1), (4.3, 1))

"Scala".zipWithIndex // Vector(('S', 0), ('c', 1), ('a', 2), ('l', 3), ('a', 4) )

11.迭代器

你可以用iterator方法从集合获得一个迭代器。这种做法并不像在Java中那么普遍,你通常可以更容易的得到你所需要的结果。

迭代器的好处就是你不用将开销很大的集合全部读进内存。例如读取文件操作,Source.fromFile产出一个迭代器,使用hasNext和next方法来遍历:

while (iter.hasNext)
对 iter.next() 执行某种操作

这里要注意迭代器多指向的位置。在调用了map、filter、count、sum、length等方法后,迭代器将位于集合的尾端,你不能再继续使用它。而对于其他方法而言,比如find或take,迭代器位于已经找到元素或已取得元素之后。

12.流

流是一个尾部被懒计算的不可变列表-----也就是说,只有当你需要时它才会被计算。

def numsFrom(n: BigInt): Stream[BigInt] = n #:: numsFrom(n + 1)

#::操作符很像是列表的::操作符,只不过它构建出来的是一个流
当你调用

val tenOrMore = numsFrom(10)

得到一个 Stream(10, ?) 流对象,尾部未被求值
如果你调用

temOrMore.tail.tail.tail

得到的将会是Stream(13, ?)
流的方法是懒执行的。例如:

val squares = numsFrom(1).map(x => x * x)

将得到 Stream(1, ?)
你需要调用squares.tail来强制对下一个元素求值

squares.tail // Stream(4, ?)

如果你想得到多个答案,可以使用take,然后调用force,这将强制对所有元素求值:

squares.take(5).force 

这将得到Stream(1,4,9,16,25)

注:不要直接使用 squares.force, 这样将会是一个无穷的流的所有成员求值, 引发OutOfMemoryError 。

你可以从且待期构造一个流。举例来说,Source.getLines方法返回一个Iterator[String]。用这个迭代器,对于每一行你只能访问一次。而流将缓存访问过的每一行,允许你重新访问他们。

13.懒视图

view方法返回一个总是被懒执行的集合,例如:

val p = (1 to 1000).view.map(x => x * x).filter(x => x.toString == x.toString.reverse)

将交出一个未被求值的集合(不像流,这里连第一个元素都未被求值)。当你执行如下代码时:

p.take(10).mkString(",")

将生成足够多的二次方,直到我们得到10个回文。跟流不同,视图不会缓存任何值。再次调用相同的代码,整个计算会重新开始。

和流一样force方法可以对懒视图强制求值。你将得到与元集合相同类型的新集合。

注:apply方法会强制对整个视图求值。所以不要直接调用lazyView(i),而是用该调用lazyView.take(i).last

14.与Java集合相互操作

image2018-10-10_14-10-55.png?version=1&modificationDate=1539151852000&api=v2

image2018-10-10_14-11-16.png?version=1&modificationDate=1539151872000&api=v2

15.并行集合

集合的par方法产出当前集合的一个并行实现,例如sum求和,多个线程可以并发的计算不同区块的和,在最后这部分结果被汇总到一起。

coll.par.sum

得出coll中所有偶数的数量:

coll.par.count(_ % 2 ==0)

你可以通过对要遍历的集合应用.par并行化for循环,就像这样:

for(i <- (0 until 1000).par) print(s" $i")

image2018-10-10_14-22-38.png?version=1&modificationDate=1539152555000&api=v2

可以看到数字将不是按顺序输出而是按照线程产出顺序输出,如果我们不.par并行化的话将会是顺序输出

image2018-10-10_14-23-30.png?version=1&modificationDate=1539152606000&api=v2

而在for/yield循环中,结果是依次组装的

image2018-10-10_14-25-48.png?version=1&modificationDate=1539152745000&api=v2

注:如果并行运算修改了共享变量,则结果无法预知。举例来说,不要更新一个共享计数器:

var count = 0

for(c <- coll.par){if ( c % 2 == 0 ) count += 1} //错误

par方法返回的并行集合属于扩展自ParSeq、ParSet或ParMap特质的类型。这些并不是Seq、Set或Map的子类型,你不能像一个预期顺序集合的方法传入并行集合。你可以用 seq 方法将并行集合转换为顺序集合。

val result = coll.par.filter(p).seq

并非所有的方法都能被并行化。例如,reduceLeft和reduceRight要求操作符都要按照顺序应用。

说明:默认情况下,并行集合使用全局的fork-join线程池,该线程池非常适合于高处理器开销的计算。如果你执行的并行步骤包含阻塞调用,就应该另选一种"执行上下文"。

转载于:https://my.oschina.net/u/3687664/blog/2243396

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值