首页 > 代码库 > Coursera公开课Functional Programming Principles in Scala习题解答:Week 1

Coursera公开课Functional Programming Principles in Scala习题解答:Week 1

引言

工作之余参加了Coursera的公开课Functional Programming Principles in Scala,这个课是第三次开讲了,讲师仍然是Scala的祖师爷Martin Odersky先生。个人认为学习公开课最大的阻碍在于有些老师的口音实在是……不忍直视,比如最早在Coursera开授公开课的Andrew Ng(当然他现在是小老板了)。幸好Martin大爷的英文口音不是很重,而且课程也不是很难,大家有兴趣可以去学习一下,地址在这里:https://class.coursera.org/progfun-004 

 

好了,烟鬼正转,转到文章的主题上来。每周的课程是通过在线的Video Lectures来进行的,你可以自行在线观看或者下载到本地,当然在线观看是有字幕的。本地观看你只能自求有字幕组去制作外挂字幕咯。Video Lectures之后会有Assignments,也就是所谓的作业,只有完成了这些作业并提交,才有可能完成这门课的学习哦。

 

值得一提的是,每周作业的提交会有一个soft deadline和hard deadline,其中前者也叫做due date,就是应该上交作业的最后期限。在due date之前上交作业,作业的分数会根据你作业的质量来决定,在due date之后没延迟一天,作业的分数为实际分数*(1-20%*延迟的天数)。如果你聪明的话,应该算到due date五天之后提交的作业无论多完美,分数都会为0了……恩,hard deadline就是due date后的第五天。

 

为了防止误操作,也为了给童鞋们一个改过自新,哦不是止于至善的机会,每周作业最多有5次提交机会。作业的分数按照5次中得分最高的那次计算(业界良心啊!)。

 

废话不多说,现在讲解自己关于Week 1作业的思路。

 

Week 1主要讲Function & Evaluation,内容是函数式编程的一些基本概念、Scala的基本语法以及讲了一个递归和尾递归(尾递归会在后续博文中涉及……玛德我又给自己挖坑%>_<%)。总体来说比较简单,关于课程内容这里就不多做介绍,正式进入习题讲解。

 

本周一共有三道题目,都是在下载后Eclipse工程中的Main.scala文件中完成代码编写。

 

Exercise 1

Exercise 1是关于杨辉三角的(原文成为Pascal‘s Triangle):

    1
   1 1
  1 2 1
 1 3 3 1
1 4 6 4 1
   ...


这个相信大家都不陌生,应该小学的时候就见过。为了照顾低年级同学我还是简单介绍一下,杨辉三角左右两条边上的元素都为1,内部的元素值为其上面两个元素的和。第一个作业就是要写一个函数,任意给定元素的位置(以行和列的形式),求出其值。

 

怎么样,简单吧?人家还给出了函数原型:

def pascal(c: Int, r: Int): Int


 

OK,现在就来分析一下。碰到这种问题,我们首先想到就是使用递归来做,那么递归一般来讲有两个要素,递归调用关系和终止条件。

1.递归调用关系:杨辉三角的性质已经决定了,元素的值等于其上一行的两个元素值之和,即Value(c, r) = Value(c, r - 1) + Value(c - 1, r - 1)。这样就可以看到Value的两个参数会越来越小,直到达到终止条件;

2.终止条件:杨辉三角的左右两条边上的元素均为1,即Value(0, r) = 1, Value(x, x) = 1,其中前者表示左边上的元素、后者表示右边上的元素

 

有了以上两个条件,我们的代码就水稻渠成了!

  /**
   * Exercise 1
   */
  def pascal(c: Int, r: Int): Int = 
  {
    if(0 == c || c == r) 1 else pascal(c - 1, r - 1) + pascal(c, r - 1) 
  }


 

Exercise 2

第二个作业是Parentheses Balancing,就是括号匹配。函数原型如下:

 

def balance(chars: List[Char]): Boolean

 

以List[Char]的形式给出一个字符串,字符串内包含若干左括号和右括号,要求判定其中的括号是否能够匹配。若能返回true,否则返回false。

题目中各给出了两个正反例:

(if (zero? x) max (/ 1 x))
I told him (that it’s not (yet) done). (But he wasn’t listening)
:-)
())(


其中前两个是正例,我们可以看到其中的左右括号完全匹配,应该返回true;后两个是反例,其中第三个只有右括号没有左括号,第四个的左右括号不匹配。

我们仍然用递归来解这道题目,初步观察输入数据中会包含非括号字符,这些字符对我们的处理过程会造成一定的麻烦,并且去掉他们之后不会对结果有影响。所以我们在预处理部分首先去掉输入中的非括号字符。

剩下的就都是括号了,按照递归的思路,我们仍然要确定递归调用关系和终止条件:

1.递归调用关系:相信大家都玩过俄罗斯方块,当一行的所有像素都被填满实体时,则消除此行。我们在此处也借用这种方法,那么我们就要思考,在什么情况下可以去掉一对括号呢?有三种情况:字符串的前两个分别是(和)时;字符串的最后两个分别是(和)时;字符串的首尾分别是(和)时。那么,消除的次序对结果是否有影响呢?我们来看一个简单的例子:()(),这个输入符合以上的三种可以消除的情况,那么应该先按照哪种情况来消除呢?明显地,如果按照第三种情况消除首尾,那么剩下的)(就不能匹配了,这就造成了错误的判断。如果首先按照前两种情况消除括号对就不会出现这种问题。所以消除的次序应该是将第三种情况放到最后。

2.终止条件:从递归调用关系可以观察到,在一直有括号匹配的情况下,字符串的长度会越来越短,当长度为0时,我们到达了一个边界,在这个边界上我们可以返回true。另外,因为括号都是成对匹配的,所以当字符串的长度为奇数时,我们可以直接返回false。最后,如果满足消除条件的三种情况都不存在,也需要返回false。

 

所以代码如下(我们定义了一个辅助函数):

  /**
   * Exercise 2
   */
  def balance(chars: List[Char]): Boolean = 
  {
    val filterChars = chars.filter((c: Char) => (c == ‘(‘ || c == ‘)‘))
    balance_clear(filterChars)
  }
  
  def balance_clear(filterChars: List[Char]): Boolean = 
  {
    val length = filterChars.length
    if(0 == length) true
    else if(length % 2 != 0) false
    else if(filterChars.apply(0) == ‘(‘ && filterChars.apply(1) == ‘)‘) balance_clear(filterChars.slice(2, filterChars.length))
    else if(filterChars.apply(filterChars.length - 2) == ‘(‘ && filterChars.apply(filterChars.length - 1) == ‘)‘) balance_clear(filterChars.slice(0, filterChars.length - 2))
    else if(filterChars.head == ‘(‘ && filterChars.last == ‘)‘) balance_clear(filterChars.slice(1, filterChars.length - 1)) 
    else false
  }


 

Exercise 3


第三道题目是Counting Change,也就是算法界著名的换零钱系列问题之一。

题目给定一个数额以及零钱的种类,要求计算出使用零钱种类分解数额的可能方案个数。函数原型如下:

def countChange(money: Int, coins: List[Int]): Int

同样考虑递归调用关系和终止条件:

1.递归调用关系:从零钱种类中拿出一个零钱面值coin,那么金额money的换零钱方式有两种:要么使用零钱面值coin,要么不使用。 如果不使用零钱面试coin,那么money的换零钱方式就等于使用去掉面值coin后的零钱种类分解money;如果使用零钱面试coin,那么money的换零钱方式为money - coin使用原始零钱种类换取的方式。

这个地方说的可能比较绕。概括的说,将总数为money的现金换成n种零钱的不同方式的数目等于:

a.将现金数money换成除第一种零钱外的所有其他零钱的不同方式数目,加上

b.将现金数money - coin换成所有种类的零钱的不同方式数目

其中coin为第一种零钱的面值,a、b中第一种零钱可以替换成任意一个零钱面值。

2.终止条件:因为在递归调用关系中,money和零钱集合都是缩小的,所以考虑边界情况:

a.如果money为0,则换取零钱的方式有1种,即所有面值零钱的个数为0

b.如果money小于0,则换取零钱的方式有0种

c.如果零钱集合为空,则换取零钱的方式有0种

这三种边界条件的判断先后关系是否影响结果呢?太晚了不想分析了,留给大家去思考吧

好,贴代码:

  /**
   * Exercise 3
   */
  def countChange(money: Int, coins: List[Int]): Int = 
  {
    if(0 == money) 1
    else if(coins.isEmpty) 0
    else if(money < 0) 0
    else countChange(money, coins.tail) + countChange(money - coins.head, coins)
  } 

 

到此为止,Week 1的三个作业全都分析完成。总体来看还是比较简单的。本人代码写的很粗糙,如果有一起上这门课的,还望不吝赐教。

 

你也可以在GitHub上获取所有我关于这门课的作业代码:https://github.com/WangTaoTheTonic/Functional-Programming-Principles-in-Scala

 

参考:

《计算机程序的构造和解释》 Harold Abelson, Gerald Jay Sussman, Julia Sussman著,裘宗燕 译

声明:

本文为原创,禁止用于任何商业目的,转载请注明出处:http://blog.csdn.net/asongoficeandfire/article/details/25251709