本周的帖子将回到基础知识,因为限制是使用递归 :
计算机科学中的递归是一种解决问题的方法,其中解决方案取决于对同一问题的较小实例(而不是迭代)的解决方案。 该方法可以应用于许多类型的问题,并且递归是计算机科学的中心思想之一。
https://zh.wikipedia.org/wiki/递归((computer_science)
这是4在编程风格焦点series.Other职位练习日交包括:
- 以编程风格介绍练习
- 以编程风格进行练习,将内容堆叠起来
- 编程风格的练习,Kwisatz Haderach风格
- 编程风格的练习,递归 (本文)
- 具有高阶功能的编程风格的练习
- 以编程风格进行练习
- 以编程风格进行练习,回到面向对象的编程
- 编程风格的练习:地图也是对象
- 编程风格的练习:事件驱动的编程
- 编程风格的练习和事件总线
- 反思编程风格的练习
- 面向方面的编程风格的练习
- 编程风格的练习:FP&I / O
- 关系数据库风格的练习
- 编程风格的练习:电子表格
- 并发编程风格的练习
- 编程风格的练习:在线程之间共享数据
- 使用Hazelcast以编程风格进行练习
- MapReduce样式的练习
- 编程风格的练习总结
递归原理
我已经在将函数式编程的方法应用于Dijkstra算法的上下文中写过关于递归的文章。 让我们详细介绍其实现。 递归函数应提供两个分支:
- 停止分支返回最终结果
- 一个使用不同参数调用函数本身的分支
这是阶乘函数的简单实现:
funfact(n:Int):Int{
varresult=1
for(iin1..n){
result*=i
}
returnresult
}
fact(5)
在传统的命令式编程中,函数使用局部变量来累积临时计算。 在递归函数中,这些变量被函数参数替换。
与上一个函数的递归等效项如下:
privatefunrecurseFact(acc:Int,n:Int):Int=
if(n==1)acc (1)
elserecurseFact(acc*n,n-1) (2)
funfact(n:Int)=recurseFact(1,n) (3)
fact(5)
- 停止分支
- 自呼分支
- 与以前的功能签名相同
请注意, acc
参数与命令示例中的result
局部变量具有相同的作用。
以下是练习中的功能。 它采用与阶乘示例中所述完全相同的原理:
funwords(rest:List<String>,
stopwords:List<String>,
words:List<String>):List<String>{
returnif(rest.isEmpty())words
else{
valsplit=split(rest.last(),stopwords,listOf())
words(rest.dropLast(1),stopwords,words+split)
}
}
递归问题
尽管递归代码比命令代码要简洁得多,但是它遇到了一个巨大的问题:函数调用被推入线程的调用堆栈中。 超出调用堆栈的大小时,将引发臭名昭著的StackOverflowError
。
尽管可以在启动时设置堆栈大小,但是它的大小是有限的。
用于管理堆栈大小的命令行选项-Xss标准JVM热点选项-XX:ThreadStackSize专有选项如有更改,恕不另行通知
为了解决这个问题,Kotlin(和Scala)提供了一个编译器技巧:尽管源代码是递归的,但是编译后的字节码是通过标准循环实现的。 有两个要求:
- 递归函数调用必须是最后一个。 这称为拖尾递归 。
- 修饰符
tailrec
必须添加到功能签名中
上面的words()
函数是尾递归的,因此添加tailrec
关键字非常容易。 可以相应地对其进行更新,以使其永远不会溢出。
相反,以下函数不是尾递归的,因为有两个使用递归的调用。 因此, 字节码不能被优化。
fun<T>quicksort(list:List<Pair<T,Int>>):List<Pair<T,Int>>=
if(list.size<=1)list
elselist.random().let{pivot->
valbelow=filter(list,listOf()){it.second<=pivot.second}
valabove=filter(list,listOf()){it.second>pivot.second}
quicksort(below-pivot)+pivot+quicksort(above)
}
结论
通常,递归是一开始很难破解的。 但是,迁移代码以使用递归很简单:只需将局部变量移动到累加器参数即可。 最难的部分是使递归函数尾递归以避免堆栈溢出。 有些功能允许,有些则不允许。