问题描述
我是大学的学生,正在学习 Scala。我有一个练习是使用模式匹配而不使用列表上的任何操作来制作笛卡尔积。
def find[A,B](aList: List[A],bList: List[B]): Set[(A,B)] = {
def findHelper[A,bList: List[B],acc: Set[(A,B)]): Set[(A,B)] = {
(aList,bList) match {
case (head1 :: tail1,head2::tail2) => {
findHelper[A,B](tail1,tail2,acc ++ Set((head1,head2)))
}
case (List(),head2::tail2) => acc
case (List(),List()) => acc
}
}
findHelper(aList,bList,Set())
}
println(find(List(1,2,3,4,5),List(7,8,9,10,11)))
结果我只得到像 (1,7),(2,8)
等。
我显然知道为什么,但我不知道如何将每一对与自身结合起来。当我的第一个列表在 ::
操作后变空时,我不知道该怎么办。
解决方法
如评论中所述,您只需遍历一个 List
一次,但对于第一个 List
中的每个项目,另一个 List
都会遍历一次。
这是一种方法。
def cartPrd[A,B](aList: List[A],bList: List[B]): Set[(A,B)] = {
def getAs(as: List[A]): List[(A,B)] = as match {
case Nil => Nil
case hd::tl => getBs(hd,bList) ++ getAs(tl)
}
def getBs(a: A,bs: List[B]): List[(A,B)] = bs match {
case Nil => Nil
case hd::tl => (a,hd) :: getBs(a,tl)
}
getAs(aList).toSet
}
cartPrd(List(1,2,1),List('A','B','B'))
//res0: Set[(Int,Char)] = Set((1,A),(1,B),(2,B))
通过简单的 for
理解,这一切都变得容易多了。
如评论中所述,问题在于您同时迭代两个列表,而您需要为第一个列表的每个元素迭代第二个列表一次。
def cartesianProduct[A,B](as: List[A],bs: List[B]): Set[(A,B)] = {
@annotation.tailrec
def loop(remainingAs: List[A],remainingBs: List[B],acc: Set[(A,B)]): Set[(A,B)] =
(remainingAs,remainingBs) match {
case (remainingAs @ (a :: _),b :: tailB) =>
loop(remainingAs,remainingBs = tailB,acc + (a -> b))
case (_ :: tailA,Nil) =>
loop(remainingAs = tailA,remainingBs = bs,acc)
case (Nil,_) =>
acc
}
loop(remainingAs = as,acc = Set.empty)
}
那一行是什么意思? " case (remainingAs @ (a :: ),b :: tailB) " 我的意思是,"@" 和 (a :: _) 有什么作用?
语法 case foo @ bar
表示如果您的模式匹配与模式 bar
匹配,则将其分配给新变量 foo
。
所以,在这种情况下,我是说如果 as 的列表不为空 (即是 cons ::
) 然后将其头部作为新变量 {{1} } 和整个列表作为一个新变量 a
。请注意,在这种情况下,根本不需要它,因为我可以只使用前面的 remainingAs
进行模式匹配,它也包含整个列表;我个人只是喜欢定义我将在 remainingAs
部分中使用的所有变量,但您只需使用 case
,代码就会按预期编译和工作。
你可能做了我需要的事情:remainingAs 和 as 不同的值,你只是将完整的 List 保存在 as/bs 值中,当它变空时,你只是再次使用完整的?例如这里:“case (:: tailA,Nil) => loop(remainingAs = tailA,acc)”
我不完全确定我是否理解您的意思,但您是正确的,我会跟踪原始的第二个列表,以便当我用完它时我可以从头开始。
因此,如您所见,代码具有三种情况,可以或多或少地阅读如下:
- 虽然第一个列表不为空,但取其头部。
- 然后通过取其头部并将两个头部的对添加到集合中来迭代第二个列表,并使用第二个列表的尾部继续该过程。
- 当您到达第二个列表的尾部时,从第一个列表的尾部重新开始,并将第二个列表重新启动到其原始形式。
- 继续该过程直到第一个列表为空,此时返回当前累加器。
注:我个人认为有两个递归函数的版本更容易理解。因为这看起来更像是两个循环,第二个循环嵌套在第一个循环中,这就是您在命令式语言中所做的。
其他解决方案包括:
两个递归函数:
case ((a :: _),b :: tailB)
或高阶函数:
(您也可以使用 def cartesianProduct[A,B)] = {
@annotation.tailrec
def outerLoop(remaining: List[A],B)] =
remaining match {
case a :: tail =>
@annotation.tailrec
def innerLoop(remaining: List[B],B)] =
remaining match {
case b :: tail =>
innerLoop(remaining = tail,acc + (a -> b))
case Nil =>
acc
}
val newAcc = innerLoop(remaining = bs,acc)
outerLoop(remaining = tail,newAcc)
case Nil =>
acc
}
outerLoop(remaining = as,acc = Set.empty)
}
语法编写)
for
您可以看到在 Scastie 中运行的代码。