07-scala的集合

92 阅读5分钟

简介

scala集合分为可变和不可变两种,默认都使用不可变集合,即集合元素是在构造时候确定的,而且构造完成之后里面的内容就是不可变的了,每次对集合的操作都会产生一个新的集合。使用不可变集合可能在有些场景下回损失性能,但是这么做是并发安全的。

可变集合需要单独引入scala.collection.mutable包,每次是在原来集合的基础上执行操作的。

scala也有线性集合、平衡数结构和哈希结构等,这些也都有可变和不可变版本。

先看下不可变集合的关系图:

再看下可变集合关系图:

看下图例说明:

Trait Iterable

Iterable是一个trait结构,我们简单说下该结构定义的一些通用接口。

合并或新增:一般借助concat,又可以写为++,合并两个集合,或者追加另一个Iterable的所有元素到当前IterableMap: 元素映射操作,对Iterable中的元素执行某种规则的映射,映射后的元素会放入新的集合中 转换操作: 比如toListtoVector这种 还有其他一系列操作,这里不再赘述,参考:docs.scala-lang.org/overviews/c…

序列Trait

这里总共有三种,分别是:SeqIndexedSeq、 和LinearSeq

这些Trait有长度,而且可以利用索引获取数据。

具体操作可以参考:docs.scala-lang.org/overviews/c…

我们单独说一下追加元素的操作,假设序列是xs,需要追加的元素是x,以下所说的追加需要根据可变和不可变具体区分:

x +: xs    // 头部追加x
ys ++: xs  // ys中所有的元素追加到xs的前面
xs :+ x    // 尾部追加x
xs :++ ys  // ys中所有的元素追加到xs后面

Sets

包含所有不重复元素的集合。具体看docs.scala-lang.org/overviews/c…

注意下SortedSets有序集合,默认使用二叉树实现,具体可以TreeSet来实例化。

默认使用哈希前缀树实现

Map

k-v映射关系的结构,具体参考文档:docs.scala-lang.org/overviews/c…

注意下getOrElse方法即可。

默认使用哈希前缀树实现

不可变集合说明

Lists

不可变的序列,有以下几个常数时间操作:

  • 获取头部元素和剩下的Lists
  • 向头部追加元素

其余的操作基本都是线性时间。

LazyLists

类似Lists的结构,唯一的区别在于所有的元素都是都是延迟计算的。正因为这一点,该集合可以”无限长“。 我们使用#::向头部追加元素。

举个代码实例:

package example

object MyExample {

  def main(args: Array[String]): Unit = {
    val lazyList = 1 #:: 2 #:: 3 #:: LazyList.empty
    println(s"list: ${lazyList.toString}, length: ${lazyList.length}")
    println("========")
    println(lazyList.take(lazyList.length).toString)
    println("========")
    println(lazyList.take(lazyList.length).toList.toString)
  }
}

代码输出:

list: LazyList(<not computed>), length: 3
========
LazyList(<not computed>)
========
List(1, 2, 3)

只有调用toList的时候才会执行真正的计算,否则该List没有实际的元素。

我们给出一个斐波那契数列的例子来进一步说明:

package example


object MyExample {

  def fibForm(a: Int, b: Int): LazyList[Int] = a #:: fibForm(b, a + b)

  def main(args: Array[String]): Unit = {
    val fibs = fibForm(1, 1).take(7)  // 这是只是定义了形式
    val res = fibs.toList  // 这里执行真正的计算
    println(res.toString)  // 输出计算的结果
  }
}

LazyLists可以用无限递归的方式表示有限的集合,这是其最大的用处。

ArraySeqs

前面提到的Lists无法直接索引数据,因此我们引入ArraySeqs结构来根据下标索引数据。利用下标获取所有的元素都是常数时间。任何更改操作的复杂度都是O(N)O(N),因为涉及到复制原来所有的元素

注意一点,所有的更改操作不会更改集合本身,而是产生一个新的集合:

package example

import scala.collection.immutable.ArraySeq


object MyExample {

  def main(args: Array[String]): Unit = {
    val arr = ArraySeq(1, 2, 3)
    val arr1 = arr.updated(1, 5)
    println(arr.toString())
    println(arr1.toString())
  }
}

代码输出:

ArraySeq(1, 2, 3)
ArraySeq(1, 5, 3)

Vectors

前面提到的ListsArraySeq都有O(N)O(N)级别的操作。Vectors对上面两者的常数的操作类型取并集,比如头部追加元素产生新Vectors的时间也是常数级别的,可以按照下标随机索引元素等;但是花费的常数时间相对会多一些

Vectors内部使用的是因子树结构,我们只需要知道它不是一个线性结构即可,具体可以参考这篇文档

如果我们想要ArraySeqLists常数时间并集的操作,再考虑Vectors结构

Queue

快速的FIFO结构,注意dequeue的方法返回的是出队元素和剩余的队列。

Ranges

相等间隔的数据,给出实例代码:

package example

object MyExample {

  def main(args: Array[String]): Unit = {
    val rng = Range(1, 2, 3)
    println(rng.toString())
  }
}

代码输出:

inexact Range 1 until 2 by 3

scala的for循环本质上也是在Range迭代,比如:

package example

object MyExample {

  def main(args: Array[String]): Unit = {
    val rng = 1 to 3
    println(rng)
  }
}

代码输出:

Range 1 to 3

既然如此,我们可以使用Range实现间隔迭代,比如:

package example

object MyExample {

  def main(args: Array[String]): Unit = {
    for (i <- Range(0, 10, 2)) {
      println(i)
    }
  }
}

代码输出:

0
2
4
6
8

红黑树

TreeSetTreeMap 结构是红黑树,我们需要范围索引时,可以使用这种结构。

VectorMapsListMaps

两者表示,k-v索引时,命中相同key时,values存放的结构。

可变集合说明

ArrayBuffer

该结构适合构建大的总是新增元素的集合。

几个优势:

  • 常数时间下标随机索引
  • 快速在尾部追加元素

ListBuffer

链表结构,除非我们经常调用toList方法,否则还是使用ArrayBuffer比较好

StringBuilder

可以在原有的String上新增元素,而不是每次都新建String

  • +=:追加新的字符
  • ++:追加String

ArrayDeque

如果想快速在在数组头部追加元素,选择该结构;否则使用前面提到的ArrayBuffer

Queue

队列操作,不支持随机索引,但是FIFO操作最快

Stack

栈操作,FILO最快

ArraySeq

该结构有固定的size,同时满足以下几个场景再使用该结构:

  • size固定
  • 非常快速操作

HashSet & HashMap

可变的哈希表

WeakHashMap

比较适合cache功能,如果map中的元素没有外部引用,则会自动删除。

集合结构的性能表

docs.scala-lang.org/overviews/c…