- 💬 写在前面:本章我们来比较在 C 语言中用循环实现的插入排序函数,与前面定义的函数式编程版本。
-
目录
0x00 函数式编程 vs. 命令式编程
下面是用 C 语言编写的插入排序:
void insert_sort(int arr[], int len) {
int i, j;
int tmp;
for(i = 1; i < len; i++) {
tmp = arr[i]; // 当前要插入的元素
j = i - 1; // 已经排序部分的最后一个元素的索引
// 将大于tmp的元素向后移动
while(j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j];
j = j - 1;
}
// 插入tmp到正确位置
arr[j + 1] = tmp;
}
}
在使用递归函数编写的情况下,代码更为简洁且可读性更高。
使用递归函数的函数式程序之所以更容易理解,
是因为它们促使我们描述问题本身而不是解决问题的步骤。
例如,重新审视前面定义的排序函数 sort 的定义,如果列表不为空,则需要排序 hd::tl ,
这意味着排序后的列表应该与排序 并插入 得到的列表相匹配,
即满足排序列表应具备的条件,这里描述了要实现的 sort 函数应满足以下等价关系的内容:
在 OCaml 实现中,sort 函数需要满足上述条件,这是通过递归函数来实现的。
.
另一个例子是计算阶乘的函数 factorial,当传入的参数大于 0 时,需要满足以下条件。
可以将上述规范与参数为 0 的情况一起,通过递归函数实现如下:
let rec factorial n =
if n = 0 then 1 else n * factorial (n-1)
另一方面,命令式编程是一种引导你描述解决问题的步骤的范式。
例如,如果用 C 语言编写阶乘函数,大多数情况下将会像下面这样实现。
int factorial (int n) {
int i; int r = 1;
for (i = 0; i < n; i++)
r = r * i;
return r;
}
这段代码是满足上述阶乘函数条件的多种实现之一。
也就是说,在命令式编程中,不仅要描述问题,还要进一步提供具体的实现方法。
相比之下,函数式编程侧重于描述问题,因此也被称为 声明式编程 (declarative programming) 。
.
0x01 递归函数的代价
参考 OCaml 等函数式编程语言,递归函数相比循环语句的代价并不更昂贵。
例如,在 C 语言中调用如下函数会发生 栈溢出 (stack overflow) ,但在函数式编程语言中则不会。
void f() { f(); } /* stack overflow */
在 OCaml 中,函数调用时不会额外消耗内存,并且可以无限递归运行。
let rec f () = f () (* 死循环 *)
两种情况下,函数 都表示了一个无限循环,而 OCaml 能够按照其字面意义执行。
由此可见,在 OCaml 中,函数即使是递归定义的,也并不比循环语句更昂贵。
0x02 尾递归函数(tail-recursive function)
函数调用如果代价高昂,是因为该函数描述的计算本身代价高,而不是因为计算是递归描述的。
更详细地解释,for 或 while 等循环语句总是可以。
转换成 尾递归函数 (tail-recursive function) 的形式,而尾递归函数的调用并不昂贵。
尾递归函数是指调用递归函数后没有其他工作要做的函数。
例如,考虑返回列表最后一个元素的函数 last :
let rec last l =
match l with
| [a] -> a
| _::tl -> last tl (* 尾递归调用 *)
在这里,函数调用 last tl 指的是尾递归调用。
last tl 的结果计算完成后,意味着没有额外的任务需要执行。
.
因为没有额外任务依赖于该结果,所以当函数以尾递归形式调用时,
无需返回到调用点,因此不需要保存额外的内存。
许多函数式语言的编译器支持这种尾调用优化。
相反,对于像阶乘函数这样的非尾递归函数,递归调用时则需要额外的内存。
let rec factorial n =
if n = 1 then 1 else n * factorial (n - 1)
在计算递归调用 factorial (a-1) 后,由于还需要进行乘以 a 的操作,所以这不是尾递归调用。
在这种情况下,递归调用会消耗内存,对于较大的数,可能会导致栈溢出。
需要注意的是,总是可以将不是尾递归形式的函数转换为尾递归形式。
.
这与能够用循环表达所有递归函数的原理相同。
例如,可以将 factorial 定义为以下形式的尾递归:
let rec factorial n r =
if n = 1 then r else factorial (n - 1) (r * n)
例如,5 的阶乘可以表示为 factorial 5。
📌 [ 笔者 ] 王亦优
📃 [ 更新 ] 2024.6.20
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
📜 参考资料 - R. Neapolitan, Foundations of Algorithms (5th ed.), Jones & Bartlett, 2015. - T. Cormen《算法导论》(第三版),麻省理工学院出版社,2009年。 - T. Roughgarden, Algorithms Illuminated, Part 1~3, Soundlikeyourself Publishing, 2018. - J. Kleinberg&E. Tardos, Algorithm Design, Addison Wesley, 2005. - R. Sedgewick&K. Wayne,《算法》(第四版),Addison-Wesley,2011 - S. Dasgupta,《算法》,McGraw-Hill教育出版社,2006。 - S. Baase&A. Van Gelder, Computer Algorithms: 设计与分析简介》,Addison Wesley,2000。 - E. Horowitz,《C语言中的数据结构基础》,计算机科学出版社,1993 - S. Skiena, The Algorithm Design Manual (2nd ed.), Springer, 2008. - A. Aho, J. Hopcroft, and J. Ullman, Design and Analysis of Algorithms, Addison-Wesley, 1974. - M. Weiss, Data Structure and Algorithm Analysis in C (2nd ed.), Pearson, 1997. - A. Levitin, Introduction to the Design and Analysis of Algorithms, Addison Wesley, 2003. - A. Aho, J. Hopcroft, and J. Ullman, Data Structures and Algorithms, Addison-Wesley, 1983. - E. Horowitz, S. Sahni and S. Rajasekaran, Computer Algorithms/C++, Computer Science Press, 1997. - R. Sedgewick, Algorithms in C: 第1-4部分(第三版),Addison-Wesley,1998 - R. Sedgewick,《C语言中的算法》。第5部分(第3版),Addison-Wesley,2002 |