Scala学习系列-集合2:用好List
List是我们使用最为广泛集合类型,用好List在一定程序度来讲是进入Scala集合世界的第一步,本篇我们从原理、使用和性能的角度来详细介绍如何用好Scala中的List。
List的性质
列表是不可变的
列表是递归的链表
列表内部是递归的,它的数据结构是链表
列表元素是协变的
List的类型
抽象类List
1 | sealed abstract class List[+A] extends AbstractSeq[A] |
case类::
1 | final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] { |
对象Nil
1 | case object Nil extends List[Nothing] { |
List的模式匹配
列表可以使用模式匹配解开,列表模式可以逐一对应到列表表达式。我们既可以用List(…)这样的模式来匹配列表的所有元素,也可以用::操作符和Nil逐步将列表解开。
示例变量:
1 | val fruit = "apples" :: "oranges" :: "pears" :: Nil |
示例1:
1 | val a :: b :: rest = fruit |
示例2:
1 | val List(a, b, c) = fruit // 可行 |
示例3:
1 | val msg = statuses match { |
示例4:
1 | val msg = statuses match { |
示例5:
1 | val fruit = "apples" :: "oranges" :: "pears" :: Nil |
List中的方法
初阶方法
下表列出了List接口中的一些常用的初阶方法,其中有些方法是在List中直接定义的,有些则是继承自其它接口中的方法,更多可用的方法还有待继续探索。
方法名 | 示例 | 说明 | 时间复杂度 |
---|---|---|---|
isEmpty: Boolean | scala> List(1, 2).isEmpty res3: Boolean = false |
检查列表是否为空列表 | 常量 |
length: Int | scala> List(1, 2).length res2: Int = 2 |
返回列表长度 | O(n),n为列表长度 |
::[B >: A](x: B): List[B] | scala> 1 :: List(2, 3) res0: List[Int] = List(1, 2, 3) |
在列表前部添加元素 | 常量 |
:::[B >: A](prefix: List[B]): List[B] | scala> List(1, 2) ::: List(3, 4) res1: List[Int] = List(1, 2, 3, 4) |
拼接两个链表 | O(n),n为参数prefix的长度 |
head: A | scala> List(1, 2).head res4: Int = 1 |
返回列表中第一个元素 | 常量 |
tail: List[A] | scala> List(1, 2, 3).tail res5: List[Int] = List(2, 3) |
返回列表中除第一个元素外的所有其它元素 | 常量 |
init: Repr | scala> List(1, 2, 3).init res6: List[Int] = List(1, 2) |
返回除最后一个元素外的所有其它元素 | O(n),n为列表长度 |
last: A | scala> List(1, 2, 3).last res7: Int = 3 |
返回最后一个元素 | O(n),n为列表长度 |
apply(n: Int): A | scala> List(1, 2, 3).apply(1) res15: Int = 2 |
返回指定索引位置的元素 | O(n),n为索引位置 |
indices: Range | scala> List(1, 2, 3).indices res18: scala.collection.immutable.Range = Range 0 until 3 |
创建一个表示列表索引的Range | |
take(n: Int): List[A] | scala> List(1, 2, 3).take(2) res9: List[Int] = List(1, 2) |
返回列表的前n个元素 | O(n),n为要获取的元素个数 |
drop(n: Int): List[A] | scala> List(1, 2, 3).drop(2) res10: List[Int] = List(3) |
丢弃列表中前n个元素 | O(n),n为要丢弃掉的元素个数 |
splitAt(n: Int): (List[A], List[A]) | scala> List(1, 2, 3).splitAt(1) res14: (List[Int], List[Int]) = (List(1),List(2, 3)) |
从指定位置切分列表 | O(n),n为切分位置的索引 |
reverse: List[A] | scala> List(1, 2, 3).reverse res8: List[Int] = List(3, 2, 1) |
将列表中元素按顺序反转 | O(n),n为列表长度 |
flatten[B]: CC[B] | scala> List(Set(1, 2),Set(1, 2)).flatten res16: List[Int] = List(1, 2, 1, 2) |
||
zipWithIndex[A1 >: A, That]: That | scala> List(1, 2, 3).zipWithIndex res17: List[(Int, Int)] = List((1,0), (2,1), (3,2)) |
||
zip[A1 >: A, B, That] (that: GenIterable[B]): That | scala> List(1, 2, 3).zip(List(“a”, “b”)) res19: List[(Int, String)] = List((1,a), (2,b)) |
对两个列表执行拉链操作 | O(n),n为长度较小的列表长度 |
unzip[A1, A2](CC[A1], CC[A2]) | scala> List((1,”a”), (2,”b”)).unzip res21: (List[Int], List[String]) = (List(1, 2),List(a, b)) |
对列表执行解拉链操作 | O(n),n为列表长度 |
copyToArray[B >: A](xs: Array[B], start: Int, len: Int) | 将列表中的元素复制到数组,start为数组的起始索引 | ||
高阶方法
fold操作
如下图,List
和LinearSeqOptimized
都间接继承自TraversableOnce
:
TraversableOnce
中同时定义了fold、foldLeft和foldRight三个方法;LinearSeqOptimized
提供了优化过的foldLeft和foldRight实现;List
中直接定义了foldRight;- 这样对于
List
而言,fold、foldLeft和foldRight分别来自TraversableOnce
、LinearSeqOptimized
和List
中的定义(如,下图中绿色字体所标识);
我们先来看TraversableOnce.fold()
的定义(如下),它直接调用了foldLeft
,对于List而言,也就是LinearSeqOptimized
中定义的foldLeft()
:
1 | def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 = foldLeft(z)(op) |
对于List.foldRight()
(如下),它通过将列表进行反转操作之后再进行foldLeft()
来实现,也最终落实到了LinearSeqOptimized
中定义的foldLeft()
,(注意:因为要先将列表进行反转操作,反转操作需要一定的开销,所以在使用这个方法时要考虑到这一点):
1 | override def foldRight[B](z: B)(op: (A, B) => B): B = |
接下来,我们看LinearSeqOptimized.foldLeft()
的定义(如下):通过迭代,将op()
函数的计算结果作为每次迭代的临时结果,临时结果又作为下一次迭代的输入,以最后一次op()
函数的计算结果作为最终结果并返回:
1 | override /*TraversableLike*/ |
总结如下:
方法名 | 图示 | 说明 | 时间复杂度 |
---|---|---|---|
fold() | 1.方法的“零值”数据类型A1 必须是当前列表元素下界(即,必须是列表元素同类型或为其父类型);2.方法的输出结果的数据类型与 零值 的数据类型A1 是一致的; |
||
foldLeft() | 1.方法的“零值”数据类型B 可以是任何数据类型;2.方法的输出结果的数据类型与“零值”的数据类型 B 是一致的;3.方法迭代 op() 函数是按列表由前向后进行的;4. op: (B, A) => B |
||
foldRight() | 1.方法的“零值”数据类型B 可以是任何数据类型;2.方法的输出结果的数据类型与“零值”的数据类型 B 是一致的;3.方法迭代 op() 函数是按列表由后向前进行的,因为需要先进行列表的反转,因此需要考虑反转操作带来的性能开销;4. op: (A, B) => B :参数类型与foldLeft()中op() 参数类型相反,这是为了保持代表零值的类型和中间聚合的类型,靠右侧,以代表是右向进行的; |
reduce操作
我们先来看TraversableOnce.reduce()
的定义(如下),它直接调用了reduceLeft
,对于List而言,也就是LinearSeqOptimized
中定义的reduceLeft()
:
1 | def reduce[A1 >: A](op: (A1, A1) => A1): A1 = reduceLeft(op) |
对于LinearSeqOptimized.reduceLeft()
(如下),它直接使用的了foldLeft()
进行实现:
1 | override /*TraversableLike*/ |
对于LinearSeqOptimized.reduceRight()
(如下),它采用了递归调用op()
函数的方式进行实现,没有进行反转的操作,所以该方法相对于foldRight()会有一定执行效率优势,但考虑到采用递归的方式,所以对于大规模的数据可能会存在一定的性能风险:
1 | override /*IterableLike*/ |
总结如下:
方法名 | 图示 | 说明 | 时间复杂度 |
---|---|---|---|
reduce() | 1.方法的“零值”数据类型A1 必须是当前列表元素下界(即,必须是列表元素同类型或为其父类型);2.方法的输出结果的数据类型与 零值 的数据类型A1 是一致的; |
||
reduceLeft() | 参考foldLeft()图示 | 1.使用foldLeft() 进行实现的;2.方法的“零值”数据类型 B 可以是任何数据类型;3.方法的输出结果的数据类型与“零值”的数据类型 B 是一致的;4.方法迭代 op() 函数是按列表由前向后进行的;5. op: (B, A) => B |
|
reduceRight() | 参考foldRight()图示 | 1.使用递归的方式进行实现的; 2.方法的“零值”数据类型 B 可以是任何数据类型;3.方法的输出结果的数据类型与“零值”的数据类型 B 是一致的;4. op: (A, B) => B :参数类型与reduceRight()中op() 参数类型相反,这是为了保持代表零值的类型和中间聚合的类型,靠右侧,以代表是右向进行的; |
scan操作
scan操作的语义不同于fold和reduce操作:fold和reduce操作最终返回的是最后一次迭代中op
函数结果,而scan操作则返回的是由每一次迭代中op
函数结果所组成的一个集合,零值为集合中的第一项,即它记录了每一次迭代的结果。
TraversableLike.scan()
:
1 | def scan[B >: A, That](z: B)(op: (B, B) => B)(implicit cbf: CanBuildFrom[Repr, B, That]): That = scanLeft(z)(op) |
TraversableLike.scanLeft()
:
1 | def scanLeft[B, That](z: B)(op: (B, A) => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { |
TraversableLike.scanRight()
,该方法先进行一次反转,再进行了一次迭代计算,最后进行了一次结果的拼装:
1 | def scanRight[B, That](z: B)(op: (A, B) => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = { |
总结如下:
方法名 | 图示 | 说明 | 时间复杂度 |
---|---|---|---|
scan() | 1.方法的“零值”数据类型A1 必须是当前列表元素下界(即,必须是列表元素同类型或为其父类型);2.方法的输出结果是元素类型与 零值 的数据类型A1 是一致的元素组成的集合; |
||
scanLeft() | 参考foldLeft()图示 | 1.方法的“零值”数据类型B 可以是任何数据类型;2.方法的输出结果是元素类型与 零值 的数据类型B 是一致的元素组成的集合;3.方法迭代 op() 函数是按列表由前向后进行的;4. op: (B, A) => B |
|
scanRight() | 参考foldRight()图示 | 1.使用三次迭代的方式进行实现的; 2.方法的“零值”数据类型 B 可以是任何数据类型;3.方法的输出结果是元素类型与 零值 的数据类型B 是一致的元素组成的集合;4. op: (A, B) => B :参数类型与scanLeft()中op() 参数类型相反,这是为了保持代表零值的类型和中间聚合的类型,靠右侧,以代表是右向进行的; |
高阶方法汇总
方法名 | 示例 | 说明 | 时间复杂度 |
---|---|---|---|
map[B, That](f: A => B): That | |||
flatMap[B, That] (f: A => GenTraversableOnce[B]): That | |||
foreach[U](f: A => U) | |||
filter(p: A => Boolean): Repr | |||
partition(p: A => Boolean): (Repr, Repr) | |||
find(p: A => Boolean): Option[A] | |||
takeWhile(p: A => Boolean): List[A] | |||
dropWhile(p: A => Boolean): List[A] | |||
span(p: A => Boolean): (List[A], List[A]) | |||
forall(p: A => Boolean): Boolean | |||
exists(p: A => Boolean): Boolean | |||
sortWith(lt: (A, A) => Boolean): Repr |
ListBuffer类型
TODO
Notes
本文基于Scala2.12.13
参考
1.《Programming in Scala Third Edition》16章、22章