Programming In Scala笔记-第十六章、Scala中的List

时间:2022-02-26 16:40:54

  本章主要分析Scala中List的用法,List上可进行的操作,以及需要注意的地方。

一、List字面量

  首先看几个List的示例。

val fruit = List("apples", "oranges", "pears")
val nums = List(1, 2, 3, 4)
val diag3 =
List(
List(1, 0, 0),
List(0, 1, 0),
List(0, 0, 1)
)
val empty = List()

  代码运行结果略。

  

  与arrays不同的是,List是imutable的,即List中的元素不能被修改。其次List具有递归结构,比如linked list,而arrays是连续的。

  有关List中的元素不能被修改,可以看下面代码。

fruit = "banana"

  运行结果:

  Programming In Scala笔记-第十六章、Scala中的List

二、List类型

  List变量中的元素都有相同的类型,比如List[T]表示该List对象中所有元素的类型都是T,可以在定义变量时加一个显示的类型声明,如下所示

val fruit: List[String] = List("apples", "oranges", "pears")
val nums: List[Int] = List(1, 2, 3, 4)
val diag3: List[List[Int]] =
List(
List(1, 0, 0),
List(0, 1, 0),
List(0, 0, 1)
)
val empty: List[Nothing] = List()

  运行结果略。

  

  List类型是协变的。即,如果ST的子类,那么List[S]List[T]的子类。

三、构造List对象

  List对象由两个关键字生成,一个是Nil另一个是::。其中Nil代表空的list,::操作符表示将该元素追加到list的前面。所以,前面的代码还可以写成

val fruit = "apples" :: ("oranges" :: ("pears" :: Nil))
val nums = 1 :: (2 :: (3 :: (4 :: Nil)))
val diag3 = (1 :: (0 :: (0 :: Nil))) ::
(1 :: (0 :: (0 :: Nil))) ::
(1 :: (0 :: (0 :: Nil))) :: Nil
val empty = Nil

  ::操作符从后往前匹配,即A :: B :: C等价于A :: (B :: C),所以,对上面代码中的变量fruit可以写成

val fruit = "apples" :: "oranges" :: "pears" :: Nil

四、List上的基本操作

  List上的所有操作,底层都可以表示成以下三个:

head:获取list中的第一个元素

tail:获取list中除了第一个元素之外的其他元素组成的新list

isEmpty:如果当前list为空,返回true

  需要注意的是headtail方法只能作用在非空list上,否则会报错,如下所示

Nil.head

  结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  下面使用者三个方法,实现一个List[Int]类型变量的插入排序代码(升序)。

def isort(xs: List[Int]): List[Int] =
if (xs.isEmpty) Nil
else insert(xs.head, isort(xs.tail)) def insert(x: Int, xs: List[Int]): List[Int] =
if (xs.isEmpty || x <= xs.head) x :: xs
else xs.head :: insert(x, xs.tail)

  在方法isort中,如果xs为空,直接返回Nil,否则将xs的第一个元素xs.head调用insert方法插入到xs.tail中。

  在方法insert中,如果待插入的xs为空,或者待插入变量xxs的第一个元素还小,则直接插在xs前面。否则将xs的第一个元素放在最前面,继续排列xxs除第一个元素之外的其他元素。

五、List模式

  List也可以作用在模式匹配的情况下。比如下面代码

val List(a, b, c) = fruit

  运行结果如下,直接将fruit中的内容依次赋给变量a, b, c

  Programming In Scala笔记-第十六章、Scala中的List

  使用上面的代码,表示明确知道fruit变量中的元素个数为3。否则会出现元素个数不匹配的报错,如下

val List(x, y) = fruit

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  这时如果仍然想要使用模式匹配,可以使用::操作符,如下所示

val a :: b :: c = fruit
val a :: b :: c :: d = fruit
val a :: b :: c :: d :: e = fruit

  运行结果如下,会将fruit变量中的第一个元素赋给变量a,第二个元素赋给b,然后将剩下的以List形式赋给变量c

  但是可赋值的变量个数不能超过List中的元素个数加一,在这种情况下最后一个变量d的内容为一个空的List

  如果变量个数超过List中的元素个数加一,就会报错。

  Programming In Scala笔记-第十六章、Scala中的List

  使用这个特性改造一下插入排序代码。

def isort(xs: List[Int]): List[Int] = xs match {
case List() => List()
case x :: xs1 => insert(x, isort(xs1))
} def insert(x: Int, xs: List[Int]): List[Int] = xs match {
case List() => List(x)
case y :: ys => if (x <= y) x :: xs
else y :: insert(x, ys)
}

  isort方法中,如果传入的xs是一个空List直接返回一个空List,否则,将第一个元素赋值给变量x,调用insert方法插入到剩余元素组成的xs1中。

  insert方法中,如果待插入的xs为空,直接返回包含一个元素的List。否则,比较待插入元素xxs的第一个元素y的大小,将值小的放在前面,组成一个新的List

六、List上的一阶操作(First-order)

1、:::操作符,合并两个List对象

  :::操作符作用于两个List对象上,xs ::: ys表达式的结果是得到一个新的List对象,该对象包含xsys中的全部元素,并且xs在前。

List(1, 2) ::: List(3, 4, 5)
List() ::: List(1, 2, 3)
List(1, 2, 3) :: List(4)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  :::操作符和::类似,也是从右向左执行的。所以xs ::: ys ::: zs等价于xs ::: (ys ::: zs)

2、使用分治原则自定义:::功能

  前面已经看到了Scala中实现了连接两个List对象的操作:::,如果想要手动实现该功能,比如定义一个append方法,作用与:::相同,应该怎么做?

  首先看一下append方法的定义,

def append[T](xs: List[T], ys: List[T]): List[T]

  接下来使用分治策略来实现该方法的方法体。前面可以看到,使用List模式匹配能够将一个List对象划分成单个元素或者子List的情况。

def append[T](xs: List[T], ys: List[T]): List[T] =
xs match {
case List() =>
case x :: xs1 =>
}

  对于第一个分支,如果xs为空,那么直接返回ys即可,即

case List() => ys

  对于第二个分支,则继续将其切割成小的元素,

def append[T](xs: List[T], ys: List[T]): List[T] =
xs match {
case List() => ys
case x :: xs1 => x :: append(xs1, ys)
}

3、length方法,获取list中元素个数

  length方法,获取当前list的长度。所谓长度,是指当前list中有多少个元素。对于List来说,计算其长度,是一个比较耗费时间的操作,时间复杂度与List中的元素个数成正比,因为需要遍历其中每一个元素才能得到List的长度。

  所以,xs.isEmptyxs.length == 0的效率高,虽然两者的作用是相同的。

  下面看一个示例,

List(1, 2, 3).length

  结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

4、initlast方法,访问List的尾部

  对应前面的head方法,List上有一个last方法,用于访问该list中最后一个元素。如下所示,

List('a', 'b', 'c', 'd', 'e').last

  对应于tail方法,init方法获取除去最后一个元素之外其他元素组成的新的List对象,如下所示,

List('a', 'b', 'c', 'd', 'e').init

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  需要注意的是,这两个方法不能作用于空的List上,否则会报错。并且,initlast方法会比tailhead方法是效率低,因为操作List后面的元素,需要遍历整个List对象才能实现。因此,在生成List对象时,最好将经常访问的元素置于List前面。

5、reverse方法,翻转List中的元素顺序

  该方法会将List中的元素顺序翻转得到一个与原List顺序相反的新List对象,而不改变之前那个List对象的内容,如下所示,

val abcde = List('a', 'b', 'c', 'd', 'e')
abcde.reverse

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  将reverse方法和前面的init, last, head, fail方法结合,有以下一些规律:

- xs.reverse.reverse结果为xs

- xs.reverse.init结果为xs.tail.reverse

- xs.reverse.tail结果为xs.init.reverse

- xs.reverse.head结果为xs.last

- xs.reverse.last结果为xs.head

6、droptakesplitAt方法,前缀和后缀

  take方法,xs take n返回xs对象的前n个元素,形成一个新的List对象。

  drop方法,xs drop n返回xs对象除了前n个元素之外的其他元素组成的新List对象。

abcde take 2
abcde drop 2

  结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  而splitAt方法,xs splitAt n表示从n个元素处,将xs分割成两个List对象。等价于xs take n, xs drop n

abcde splitAt 2
val (x, y) = abcde splitAt 2

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

7、apply方法和下标,获取指定元素

  List也可以使用下标访问指定元素abcde(2)List对象的apply`方法,等价于上面所示的访问指定位置的元素,所以下面代码和上面的是等价的

abcde apply 2

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  indicies方法,获取当前List对象中所有元素的下标,

abcde.indices

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

8、flatten方法,展平List[List]对象中的所有元素

  这个方法可以将List[List]结构的对象中所有元素展开,返回一个包含所有子List元素的新的List对象,如下,

List(List(1, 2), List(3), List(), List(4, 5)).flatten

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  注意,该方法只能应用于List[List]的对象上,直接由List对象调用,会报错

List(1, 2, 3).flatten

  结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

9、zipunzip方法,组合两个list对象为pairs形式

  直接看示例吧。

abcde.indices zip abcde

  结果如下,将前一个List对象的每一个元素与后一个List对象的每一个元素组合成pairs

  Programming In Scala笔记-第十六章、Scala中的List

  如果前后两个List对象长度不一致,将会返回较短长度的那个结果,

abcde zip List(1, 2, 3)
List(1, 2, 3) zip abcde

  结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  在这里还有一个特殊的方法zipWithIndex,作用是将该List对象中的每一个元素与其下标进行zip操作。

abcde.zipWithIndex

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  unzip方法的作用与zip方法相反,将一个pairs形式的List拆分成两个List对象,

val zipped = abcde zip List(1, 2, 3)
zipped.unzip

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

10、toStringmkString方法,将list中元素拼接成字符串

  toString方法略。

  mkString方法可以传入一个连接符,用该连接符将List对象中的各个元素拼接起来最终形成一个字符串。也可以指定其前缀和后缀。

abcde mkString "-"
abcde mkString ("[", "-", "]")

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

11、iteratortoArraycopyToArray方法,转换List对象类型

(1)ListArray互转

  List对象转为Array

val arr = abcde.toArray

  ArrayList

arr.toList

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(2)copyToArray方法

  该方法可以将List对象中的元素复制到指定Array对象的指定位置,比如下面代码中,将List(1, 2, 3)中的三个元素插入到arr2对象的第3~5位置上。然后arr2这个Array中的元素个数和内容发生变化。

val arr2 = new Array[Int](10)
List(1, 2, 3) copyToArray (arr2, 3)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(3)inerator方法

  获得当前List对象的迭代器,可以用此迭代器访问下一个元素。

val it = abcde.iterator
it.next
it.next

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

七、List上的高阶操作(Higher-order)

1、map, flatMap, foreach方法,处理List中的每个元素

(1)map方法

  map方法接收一个函数参数,将该函数作用在List的每一个元素上,得到的结果组合成一个新的List对象。

List(1, 2, 3) map (_ + 1)
val words = List("the", "quick", "brown", "fox")
words map (_.length)
words map (_.toList.reverse.mkString)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(2)flatMap方法

  和map方法有点类似,区别在于flatMap方法接收到的函数对单一元素作用后得到多个结果,会将这些结果返回到一个List对象中。和前面的flatten方法有点类似。

words map (_.toList)
words flatMap (_.toList)

  运行结果如下,传入的方法是将当前元素拆分成List对象,所以一个元素经过map后会返回一个List对象,最终形成List[List]的结构。但是flatMap方法会将map得到的每一个List打散,取出其中每一个元素,组合成一个新的List对象并返回。

  Programming In Scala笔记-第十六章、Scala中的List

(3)foreach方法

  该方法和map方法类似,也是接收一个函数参数,这个函数参数会作用在List中的每一个元素上。但是这个函数的返回值为Unit,所以其副作用仅仅是比如打印其中的每一个元素,或者改变某个变量的值,比如下面例子中的求和。

List(1, 2, 3, 4, 5) foreach (println)
var sum = 0
List(1, 2, 3, 4, 5) foreach (sum += _)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

2、filter, partition, find, takeWhile, dropWhile, span方法,过滤List中的元素

(1)filter方法

  传入一个函数参数,最终得到一个新的List对象,包含原List中使该函数参数值为true的元素。

List(1, 2, 3, 4, 5) filter (_ % 2 == 0)     // 返回所有偶数
words filter (_.length == 3) // 返回所有字符长度为3的元素

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(2)partition方法

  该方法是filter方法的加强版。filter只会返回时函数参数值为true的一个新List。而partition方法,会同时返回两个List,第一个为函数参数为true的所有元素,第二个为函数参数为false的所有元素。

List(1, 2, 3, 4, 5) partition (_ % 2 == 0)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(3)find方法

  和filter方法类似,但是find方法只返回List中第一个满足该函数表达式的元素,而不是返回一个List对象。返回值类型为Some(x)或者None

List(1, 2, 3, 4, 5) find (_ % 2 == 0)
List(1, 2, 3, 4, 5) find (_ <= 0)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(4)takeWhile方法

  xs takeWhile p获取xs中最前面的所有满足p表达式的元素。

List(1, 2, 3, -4, 5) takeWhile (_ > 0)

  运行结果如下,直到遇到-4时不满足该表达式,尽可能长的从第一个元素取满足该表达式的元素。

  Programming In Scala笔记-第十六章、Scala中的List

(5)dropWhile方法

  和takeWhile方法正好相反,尽可能的从第一个元素删除所有满足表达式的元素。

List(1, 2, 3, -4, 5) dropWhile (_ > 0)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

(6)span方法

  是takeWhiledropWhile两个方法的结合形式。有点类似于splitAt方法综合了takedrop方法一样。

  xs span p等价于xs takeWhile p, xs dropWhile p

List(1, 2, 3, -4, 5) span (_ > 0)

   运行结果如下,

   Programming In Scala笔记-第十六章、Scala中的List

3、forall, exists方法,判断满足条件元素是否存在

  xs forall p,其中xs是一个List类型对象,p是一个判断表达式,只有当xs中每一个元素均使p的结果为true时,整个结果才返回true。而xs exists p表示xs中只要存在任意一个元素使p的结果为true,整个结果返回true

val diag3 = (1 :: 0 :: 0 :: Nil) ::
(0 :: 1 :: 0 :: Nil) ::
(0 :: 0 :: 1 :: Nil) :: Nil
def hasZeroRow(m: List[List[Int]]) =
m exists (row => row forall (_ == 0)) hasZeroRow(diag3)

  上面代码遍历diag3变量,如果diag3中某个子List的值全部为0,则结果为true。运行结果如下

  Programming In Scala笔记-第十六章、Scala中的List

4、/:, :\方法,折叠List

  所谓的折叠List即对List中的元素进行汇总。比如说如果有一个函数sum(List(a, b, c))接收一个List参数,然后将该List中各元素的和进行累加,最终得到一个和,这个过程就可以称为对List的折叠。

  Scala中提供了两个方法实现折叠功能

(1)/:方法,左折叠

  对于左折叠,完整的调用形式是(z /: xs) (op),包含三个部分,其中z是一个初始值,xs是一个List对象,op是一个二元操作表达式。最终结果是将表达式opz开始,依次从左到右的遍历xs中的每个元素。

(z /: List(a, b, c)) (op)     等价于     op(op(op(z, a), b), c)

  用图表示的话,如下所示,

  Programming In Scala笔记-第十六章、Scala中的List

  下面举个例子,

val words = List("the", "quick", "brown", "fox")
("" /: words) (_ + " " + _)

  运行结果如下,最终返回一个字符串

  Programming In Scala笔记-第十六章、Scala中的List

(2):\方法,右折叠

  右折叠与左折叠功能类似,只不过是从右往左遍历List中的元素。

(List(a, b, c) :\ z) (op)     等价于     op(a, op(b, op(c, z)))

  用图表示,

  Programming In Scala笔记-第十六章、Scala中的List

  最后,用左折叠定义一个求数组和的方法,

def sum(xs: List[Int]): Int =
(0 /: xs) (_ + _)
sum(List(1, 2, 3))

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

5、sortWith方法,排序List中的元素

  xs sortWith before表达式中,xs是一个List对象,before是一个可用比较两个元素值的函数表达式。这个表达式的含义是,对xs中的元素进行排序,根据before方法来确定xs中哪个元素在前,哪个元素在后。

List(1, -3, 4, 2, 6) sortWith (_ < _)
words sortWith (_.length > _.length)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

八、List对象上的方法

1、List.apply方法,使用传入的元素生成List对象

  根据给定元素生成一个新的List对象。

List.apply(1, 2, 3)

  结果,

  Programming In Scala笔记-第十六章、Scala中的List

2、List.range方法,生成包含范围内值的List

  给定一个范围,根据起始值或者阶跃单位量生成一个List对象,

List.range(1, 5)     // 从1到5
List.range(1, 9, 2) // 从1到9,每隔2生成一个元素
List.range(9, 1, -3) // 从9到1,每个-3生成一个元素

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

3、List.fill方法,根据给定元素和个数生成List

  示例,

List.fill(5)('a')     // 生成一个List对象,其中的元素为5个a字符
List.fill(3)("hello") // 生成一个List对象,其中元素为3个hello字符串
List.fill(2, 3)('b') // 也可以生成多维List,这里展示一个二维

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

4、List.tabulate方法,根据给定维度动态生成List元素值

  该方法与List.fill有点类似,也是传入两个参数列表,第一个仍然是维度及长度,第二个参数列表中传入一个函数表达式而不是List.fill中那样传入一个固定值。这样,List.tabulate方法中的元素值就是动态变化的,由第二个参数列表中的表达式计算得到,改表达式的输入参数为当前元素的下标值。

val xs = List.tabulate(5)(n => "xs" + n)     // 在每个下标前拼接一个xs
val squares = List.tabulate(5)(n => n * n) // 每个元素取其下标的平方值
val multiplication = List.tabulate(5, 5)(_ * _) // 每个元素取其纵横下标的乘积

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

5、List.concat方法,拼接多个List

  将多个List对象中的元素拼接到一起,可以拼接元素类型不同的List对象,形成一个新的List对象。类似于前面的flatten方法。

List.concat(List('a', 'b'), List('c'))
List.concat(List('a', 'b'), List(1))
List.concat()

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

九、zipped方法,处理多个List对象

  通过前面介绍的zip方法,可以将两个List对象组合到一起,形成元素为pairs对的新List对象。Scala中还提供了一个zipped方法,在该方法之后就可以同时处理两个List对象中的元素了。

(List(10, 20), List(3, 4, 5)).zipped.map(_ * _)

  运行结果如下,map方法中的两个_分别表示第一个和第二个List对象中对应位置上的元素。

  Programming In Scala笔记-第十六章、Scala中的List

  zipped方法后,还可以使用forall或者exists方法,如下所示

(List("abc", "de"), List(3, 2)).zipped.forall(_.length == _)
(List("abc", "de"), List(3, 2)).zipped.exists(_.length != _)

  运行结果如下,

  Programming In Scala笔记-第十六章、Scala中的List

  这里需要注意的是zipped方法,只取较短那个List的长度,超过该长度的长List中的元素会被丢弃。