软件开发之递归操作

WBOY
发布: 2024-08-16 19:54:30
原创
1140 人浏览过

软件开发之递归操作

我们来看一下这个经典的递归阶乘:

#include
int factorial(int n)
{
int previous = 0xdeadbeef;
if (n == 0 || n == 1) {
return 1;
}
previous = factorial(n-1);
return n * previous;
}
int main(int argc)
{
int answer = factorial(5);
printf("%d\n", answer);
}
登录后复制

递归阶乘 - factorial.c
函数调用自身的这个观点在一开始是让人很难理解的。为了让这个过程更形象具体,下图展示的是当调用 factorial(5) 并且达到 n == 1这行代码 时,栈上 端点的情况:

软件开发之递归操作

每次调用 factorial 都生成一个新的 栈帧。这些栈帧的创建和 销毁 是使得递归版本的阶乘慢于其相应的迭代版本的原因。在调用返回之前,累积的这些栈帧可能会耗尽栈空间,进而使你的程序崩溃。

而这些担心经常是存在于理论上的。例如,对于每个 factorial 的栈帧占用 16 字节(这可能取决于栈排列以及其它因素)。如果在你的电脑上运行着现代的 x86 的 Linux 内核,一般情况下你拥有 8 GB 的栈空间,因此,factorial 程序中的 n 最多可以达到 512,000 左右。这是一个 巨大无比的结果,它将花费 8,971,833 比特来表示这个结果,因此,栈空间根本就不是什么问题:一个极小的整数 —— 甚至是一个 64 位的整数 —— 在我们的栈空间被耗尽之前就早已经溢出了成千上万次了。

过一会儿我们再去看 CPU 的使用,现在,我们先从比特和字节回退一步,把递归看作一种通用技术。我们的阶乘算法可归结为:将整数 N、N-1、 … 1 推入到一个栈,然后将它们按相反的顺序相乘。实际上我们使用了程序调用栈来实现这一点,这是它的细节:我们在堆上分配一个栈并使用它。虽然调用栈具有特殊的特性,但是它也只是又一种数据结构而已,你可以随意使用。我希望这个示意图可以让你明白这一点。

当你将栈调用视为一种数据结构,有些事情将变得更加清晰明了:将那些整数堆积起来,然后再将它们相乘,这并不是一个好的想法。那是一种有缺陷的实现:就像你拿螺丝刀去钉钉子一样。相对更合理的是使用一个迭代过程去计算阶乘。

但是,螺丝钉太多了,我们只能挑一个。有一个经典的面试题,在迷宫里有一只老鼠,你必须帮助这只老鼠找到一个奶酪。假设老鼠能够在迷宫中向左或者向右转弯。你该怎么去建模来解决这个问题?

就像现实生活中的很多问题一样,你可以将这个老鼠找奶酪的问题简化为一个图,一个二叉树的每个结点代表在迷宫中的一个位置。然后你可以让老鼠在任何可能的地方都左转,而当它进入一个死胡同时,再回溯回去,再右转。这是一个老鼠行走的 迷宫示例:

软件开发之递归操作

每到边缘(线)都让老鼠左转或者右转来到达一个新的位置。如果向哪边转都被拦住,说明相关的边缘不存在。现在,我们来讨论一下!这个过程无论你是调用栈还是其它数据结构,它都离不开一个递归的过程。而使用调用栈是非常容易的:

#include
#include "maze.h"
int explore(maze_t *node)
{
int found = 0;
if (node == NULL)
{
return 0;
}
if (node->hasCheese){
return 1;// found cheese
}
found = explore(node->left) || explore(node->right);
return found;
}
int main(int argc)
{
int found = explore(&maze);
}
登录后复制

递归迷宫求解 下载
当我们在 maze.c:13 中找到奶酪时,栈的情况如下图所示。你也可以在 GDB 输出 中看到更详细的数据,它是使用 命令 采集的数据。

软件开发之递归操作

它展示了递归的良好表现,因为这是一个适合使用递归的问题。而且这并不奇怪:当涉及到算法时,递归是规则,而不是例外。它出现在如下情景中——进行搜索时、进行遍历树和其它数据结构时、进行解析时、需要排序时——它无处不在。正如众所周知的 pi 或者 e,它们在数学中像“神”一样的存在,因为它们是宇宙万物的基础,而递归也和它们一样:只是它存在于计算结构中。

Steven Skienna 的优秀著作 算法设计指南 的精彩之处在于,他通过 “战争故事” 作为手段来诠释工作,以此来展示解决现实世界中的问题背后的算法。这是我所知道的拓展你的算法知识的最佳资源。另一个读物是 McCarthy 的 关于 LISP 实现的的原创论文。递归在语言中既是它的名字也是它的基本原理。这篇论文既可读又有趣,在工作中能看到大师的作品是件让人兴奋的事情。

Berbalik kepada masalah maze. Walaupun sukar untuk meninggalkan rekursi di sini, ini tidak bermakna ia mesti dicapai melalui timbunan panggilan. Anda boleh menggunakan rentetan seperti RRLL untuk menjejak belokan, dan kemudian gunakan rentetan ini untuk menentukan langkah seterusnya tetikus. Atau anda boleh menetapkan sesuatu yang lain untuk merekodkan keseluruhan status pencarian keju. Anda masih melaksanakan proses rekursif, anda hanya perlu melaksanakan struktur data anda sendiri.

Nampaknya lebih rumit kerana panggilan tindanan adalah lebih sesuai. Setiap bingkai tindanan merekodkan bukan sahaja nod semasa, tetapi juga keadaan pengiraan pada nod tersebut (dalam kes ini, sama ada kita hanya membiarkannya pergi ke kiri, atau telah cuba pergi ke kanan). Oleh itu, kod itu menjadi tidak penting. Walau bagaimanapun, kadangkala kami meninggalkan algoritma yang sangat baik ini kerana takut limpahan dan prestasi yang dijangkakan. Itu bodoh!

Seperti yang kita lihat, ruang tindanan adalah sangat besar, dan batasan lain sering ditemui sebelum ruang tindanan habis. Di satu pihak, anda boleh menyemak saiz masalah untuk memastikan ia boleh dikendalikan dengan selamat. Kebimbangan CPU didorong oleh dua contoh bermasalah yang diedarkan secara meluas: faktorial bodoh dan rekursi Fibonacci O(2n) tanpa ingatan yang mengerikan. Ia bukan perwakilan yang betul bagi algoritma rekursif tindanan.

Malah operasi tindanan adalah sangat pantas. Biasanya, timbunan mengimbangi kepada data adalah sangat tepat, ia adalah data panas dalam cache, dan arahan khusus beroperasi padanya. Pada masa yang sama, overhed yang dikaitkan dengan menggunakan struktur data anda sendiri yang diperuntukkan pada timbunan adalah penting. Ia sering dilihat bahawa orang menulis kaedah pelaksanaan yang lebih kompleks dan mempunyai prestasi yang lebih buruk daripada rekursi panggilan tindanan. Akhir sekali, prestasi CPU moden adalah sangat baik, dan secara amnya CPU tidak akan menjadi hambatan prestasi. Berhati-hati apabila mempertimbangkan untuk mengorbankan kesederhanaan program, sama seperti anda sentiasa mempertimbangkan prestasi program dan pengukuran prestasi itu.

Artikel seterusnya akan menjadi yang terakhir dalam siri Exploring Stack Kami akan mempelajari tentang panggilan ekor, penutupan dan konsep lain yang berkaitan. Kemudian, tiba masanya untuk menyelami rakan lama kita, kernel Linux. Terima kasih kerana membaca!

软件开发之递归操作

以上是软件开发之递归操作的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:linuxprobe.com
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板