热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

数据结构复习篇:用栈实现递归

也许大家会疑问:复习完栈应该到队列了吧。我开始也是这样想的,但用栈实现递归,是一个难点。说实话,我以前学习的时候,就在这一处卡住了,当时我烦躁了好几天,但可能由于突然被什么东西转移了注意力,所以
也许大家会疑问:复习完栈应该到队列了吧。我开始也是这样想的,但用栈实现递归,是一个难点。说实话,我以前学习的时候,就在这一处卡住了,当时我烦躁了好几天,但可能由于突然被什么东西转移了注意力,所以就这样跳过去了。不知道用栈实现递归,也确实不大影响后面的学习,但我可以肯定,如果你觉得世界上有一些东西难以理解而不愿面对,那自信将会由此削弱。当然,遇到困难可以适当地把它放下,但逃避应该是暂时的,必须鼓励自己――-也许是几天,也许是几个月――但绝对要攻克它!
很兴奋,经过刚才的思考:我先是在草稿纸上进行了一些“画画”的工作,当我把用栈实现汉诺塔的搬运过程,一步步地的画在纸上的时候,思维由这些具体的步骤而变得清晰起来(“画画”确实是一种有助于思考的方法。很可惜我没有扫描工具,否则我可以把这些画插在我这篇文章中,将会非常生动)。然后我想到一个不错的比喻来帮助自己理解这一个过程。这一个比喻,我非常得意,一会与大家分享。现在,我自认为把这个问题完全攻克了(请大家原谅我的自恋^_^),所以才迫不及待地要把思考的结果写下来。
我还是按书上那个汉诺塔的例子来表述这个思想,而且把汉诺塔的递归用栈实现,也恰好有一定的难度。但我建议,大家看完我这篇文后,不妨试着一个递归用栈去实现一下,很容易就检验出你是否真的领会了其中的思想。
-、汉诺塔问题:
有三根柱子分别叫A柱、B柱、C柱。现假设有N个圆盘(都是按从大到小依次放入柱中的)已经放在了A柱上,我们的任务就是把这N个圆盘移动到C柱。但移动的过程,必须遵守大盘永远在小盘的下面这一个原则。
二、移动汉诺塔的递归思想:
1、  先把A柱上面的(N-1)个圆盘移到B柱(这一步使问题的规模减少了1)。
2、  再把A柱上剩下的那个最大的圆盘移到C柱。
3、  最后把B柱上的(N-1)圆盘移到C柱。
当我们写递归函数的时候,我们先假设我们即将写的这个函数已经能解决n个圆盘的汉诺塔问题了,递归就是这样一种思想:它告诉我们程序员,做梦也是一件有意义的事^_^。那么我们现在假设这个函数的接口是这样的:
Void TOH(int n, Pole start, Pole goal, Pole temp )(第一次调用时,我们是这样用这个函数的:
Void TOH(N, A, C, B);N是圆盘数、A是起始柱、B是暂时柱、C是目标柱。)然后,我就利用上面分析的递归步骤(当然,递归中的初始情况base case也是不能忘记的),在该函数体里面,继续调用该函数,便得到了递归函数:
void  TOH( int  n, Pole start, Pole goal, Pole temp) // 把n个圆盘从start柱移到goal柱,temp柱作为辅助柱
{
    
if  (n  ==   0 return ;     // base case
    TOH(n - 1 , start, temp, goal);     // 把n-1个圆盘从start柱移到temp柱,goal作为此次的辅助柱
    move(start,goal);         // 从start柱移动一块圆盘到goal柱
    TOH(n - 1 , temp, goal, start);     // 把temp柱中的n-1个圆盘移到goal柱
     return ;
}
三、用栈实现递归的思想:
现在,我将用一个自认为得意的比喻,来表达这个思想。我们不妨设想有这样一个环境:有一家独特的公司,这家公司的上司是这样给他们的下属分配任务的:当有一个任务来临的时候,一位上司就会把这个任务写在一张格式统一的纸上(这张纸象征着栈中的一个元素,但纸上的内容与栈中元素的内容会有一些差异),这张纸上一般会记录下面这两个信息:
这位上司A会把这张 任务纸放到公司里一张专门的办公桌上(它是栈的象征)。
好了,现假设,上司A把这张任务纸放在了那张专门的办公桌上,一个下属B查看办公桌时,发现了这个任务。他并没有立刻就去执行这个任务,因为他们公司有一个奇怪但令人鼓舞的规定:
1、 如果你可以把一个任务分解成更小的几个子任务,你便可以把这些分解后的子任务,留给别人去做。
2、 当你把任务分解后,你必须把这些子任务,分别写在任务纸上,并按照这些子任务的执行顺序,从后到先,依次叠放在那张办公桌上,即保证最上面的那张纸,就是应该最先执行的任务。
那么下属B发现,他可以把上司A写在那张纸上的任务分解成三个子任务:
然后,B把这三张纸依次从上到下地叠放在办公桌上,那么他可以下班了^_^。
之后,下属C来上班,发现了办公桌上叠放了三张纸,注意,公司有如下规定:
通常(因为还有一种特别情况,将下面给出),每个员工只需负责办公桌上,放在最上面的那张纸上的任务。
C拿起最上面那张纸,就是B写的执行顺序为1的那张纸,他立刻笑了。他也模仿B,把这个任务分解成:
1、  这里有N-2个圆盘,把这些圆盘从A柱移到C柱。
2、  这里有1个圆盘,把这个圆盘从A柱移到B柱。
3、  这里有N-2个圆盘,把这些圆盘从C柱移到B柱。
然后,他把三个子任务的三张任务纸替换掉B写的那张纸。那么他又可以下班了。
就这样,员工们很轻松的工作。直到有一个员工,假设他名叫X,比较不幸,他发现办公桌上最上面的那张纸上写着:把一个圆盘从某根柱移到另一根柱。因为这个任务根本就没办再分了,所以可怜的X就只好亲自动手去完成这个任务,但事情并没有结束,因为该公司规定:
如果你无法再分解这个任务,你就要亲自完成这个任务,并且如果办公桌上还有任务纸,那你必须继续处理下一张任务纸。
正如前面所说,办公桌上的纸的处理方式,就是栈的后进先出的方式,而任务纸就是栈的元素。这应该很容易理解。难点在于两点:
1、  栈元素的内部结构如何定义?我们可以把栈元素看作一个结构体,或者看作一个类对象,而任务的规模应该是类对象的一个整形数据成员,但任务描述,就不太好处理了。事实上,我们可以对任务进行分类,然后只要用一个枚举类型或是其他数据类型,来区分这个任务属于哪种分类。
2、  如何把上面所分析的过程,用程序表达出来?好了,如果你耐心的阅读了上面的文字,那么理解下面这个程序,应该非常容易了:
我对书中的程序稍作改写,也作更丰富的注释,读程序的时候,注意联系我上文所作的比喻:
#include  < iostream >
#include 
< conio.h >
#include 
" StackInterface.h " // 这个头文件,可以在栈那篇文章中找到,也可以自己用标准库中的stack改一下面的程序即可
using   namespace  std;
/*
现在我们来定义一个栈的元素类:TaskPaper任务纸
由下属B、C对子任务的分解情况,很容易看出,
可以把任务分成两类:
1、移动n-1个圆盘。这说明,这种任务可以继续分解。
2、移动1个圆盘。这说明,这种任务无法再分,可以执行移动操作。
那么,我们可以定义一个枚举类型,这个枚举类型作为栈元素
的一个数据成员,用来指示到底是继续分解,还是作出移动操作。
*/
enum  Flag {toMove, toDecompose}; // 移动、继续分解
enum  Pole {start, goal, temp};     // 柱的三种状态,既然起始柱、目标柱、临时柱(也叫辅助柱)

class  TaskPaper     // 任务纸类,将作为栈的元素类
{
public :
    Flag flg;    
// 任务分类
     int  num;         // 任务规模
    Pole A, B, C;     // 三根柱

    TaskPaper(
int  n, Pole a, Pole b, Pole c, Flag f)
    {
        flg 
=  f;
        num 
=  n;
        A 
=  a;        
        B 
=  b;
        C 
=  c;
    }
};

void  TOH( int  n, Pole s, Pole t, Pole g) // 用栈实现的汉诺塔函数
{
    LStack
< TaskPaper  *>  stk;
    stk.push(
new  TaskPaper(n,s,t,g, toDecompose));     // 上司A放第一张任务纸到办公桌上

    TaskPaper 
*  paperPtr;
    
long  step = 0 ;
    
while  (stk.pop(paperPtr))     // 如果办公桌上有任务纸,就拿最上面的一张来看看
    {
        
if  (paperPtr -> flg  ==  toMove  ||  paperPtr -> num  ==   1 )
        {
            
++ step;
            
if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从A柱移动一个圆盘到B柱。 " << endl;
            }
            
else   if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从A柱移动一个圆盘到C柱。 " << endl;
            }
            
else   if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从B柱移动一个圆盘到A柱。 " << endl;
            }
            
else   if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从B柱移动一个圆盘到C柱。 " << endl;
            }
            
else   if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从C柱移动一个圆盘到A柱。 " << endl;
            }
            
else   if  (paperPtr -> ==  start  &&  paperPtr -> ==  goal)
            {
                cout
<< " " << step << " 步:从C柱移动一个圆盘到B柱。 " << endl;
            }
        }
        
else
        {
            
int  num  =  paperPtr -> num;
            Pole a 
=  paperPtr -> A;
            Pole b 
=  paperPtr -> B;
            Pole c 
=  paperPtr -> C;
            
            
if  (a == start  &&  c == goal) 
            {
                
// 书中仅写了这一种情况,而后面的五种的情况被作者大意地认为是相同的,
                
// 于是程序出错了。我估计没有几个人发现这个问题,因为只我这种疑心很重的人,
                
// 才会按照书中的思路写一遍这种程序^_^
                stk.push( new  TaskPaper(num - 1 , b, a, c, toDecompose)); // 子任务执行顺序为3
                stk.push( new  TaskPaper( 1 ,a,b,c,::toMove));     // 子任务中执行顺序为2
                stk.push( new  TaskPaper(num - 1 , a, c, b, toDecompose)); // 子任务中执行顺序为1
            }
            
else   if  (a == start  &&  b == goal)
            {
                stk.push(
new  TaskPaper(num - 1 , c, b, a, toDecompose)); // 为goal的柱状态不变,其它两根柱的状态互换
                stk.push( new  TaskPaper( 1 ,a,b,c,::toMove));     // 移动操作中,柱的状态不变
                stk.push( new  TaskPaper(num - 1 , a, c, b, toDecompose)); // 为start的柱状态不变,其它两根柱的状态互换
            }
            
else   if  (b == start  &&  a == goal)
            {            
                stk.push(
new  TaskPaper(num - 1 , a, c, b, toDecompose));
                stk.push(
new  TaskPaper( 1 ,a,b,c,::toMove));
                stk.push(
new  TaskPaper(num - 1 , c, b, a, toDecompose));
            }
            
else   if  (b == start  &&  c == goal)
            {
                
                stk.push(
new  TaskPaper(num - 1 , b, a, c, toDecompose));
                stk.push(
new  TaskPaper( 1 ,a,b,c,::toMove));
                stk.push(
new  TaskPaper(num - 1 , c, b, a, toDecompose));
            }
            
else   if  (c == start  &&  a == goal)
            {
                
                stk.push(
new  TaskPaper(num - 1 , a, c, b, toDecompose));
                stk.push(
new  TaskPaper( 1 ,a,b,c,::toMove));
                stk.push(
new  TaskPaper(num - 1 , b, a, c, toDecompose));
            }
            
else   if  (c == start  &&  b == goal)
            {
                
                stk.push(
new  TaskPaper(num - 1 , c, b, a, toDecompose));
                stk.push(
new  TaskPaper( 1 ,a,b,c,::toMove));
                stk.push(
new  TaskPaper(num - 1 , b, a, c, toDecompose));
            }

        }

        delete paperPtr;
        
    }
}

void  main()
{
    
    TOH(
3 ,start,temp,goal);        
    getch();

}
总结下一下用栈实现递归的算法
1、进栈初始化:把一张TaskPaper放到办公桌面上。
2、出栈,即从办公桌上取一张TaskPaper,如办公桌上没有任务纸(出栈失败),则到第4步。
3、取出任务纸后,根据任务的信息,分两种情况进行处理:
      A、如果任务不可再分,则执行这个任务(在上面这个程序中,体现为把搬运动作打印出来),返回到第2步。
      B、否则,划分成若干个子任务,并把这些子任务,按照执行任务的相反顺序放进栈中,保证栈顶的任务永远是下一次出栈时最应优先处理的,返回到第2步。
4、其它处理。
 
补充:刚才(现在是10月3号)我试着用我上文的思想,用栈实现Fibonacci(斐波那契)数列的递归,真的很有用,思路非常清晰。我把这段程序发到这里来,相信通过上面的汉诺塔程序和这个斐波那契程序,可以更好的帮助大家理解上文的思想(由于昨晚电脑崩溃,用ghost重装了系统,发现我前几天写的程序全丢失了,下面这个程序中用到的栈,是标准库中的stack):
 
/*
这是我个人的头文件,用来定义各种函数
文件名:zyk.h
*/
#ifndef ZYK_H
#define  ZYK_H
#include 
< iostream >
#include 
< stack >
using   namespace  std;

namespace  zyk
{
    
// Fibonacci数列的递归函数
     long  fibr ( int  n);

    
// 用栈实现fibr递归
     long  fibr_stack( int  n);
}

long  zyk::fibr( int  n)
{
    
if  (n  ==   1   ||  n  ==   2 )
    { 
return   1 ;}
    
    
return  fibr(n - 1 +  fibr(n - 2 );
}

long  zyk::fibr_stack( int  n)
{
    
long  val  =   0 ;
    stack
< int >  stk;
    stk.push(n);      
// 初始化栈

    
while  ( ! stk.empty())     // 如果办公桌上不是空的,即有任务纸
    {
        
int  nn  =  stk.top();     // 查看这张纸
        stk.pop();             // 拿开这张纸
        
        
if  (nn == 1   ||  nn == 2 )
        {
            val 
+=   1 ;         // 累加。
        }
        
else
        {
            stk.push(nn
- 1 );     // 分成两个子任务
            stk.push(nn - 2 );     // 对于这两个子任务,其先后执行顺序是无关紧要的。
        }

    }

    
return  val;
}


#endif

推荐阅读
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 第五周项目一——体验常成员函数(1)
    设计平面坐标点类,计算两点之间距离、到原点距离、关于坐标轴和原点的对称点等。在设计中,由于求距离、求对称点等操作对原对象不能造成任何改变,所以,将这些函数设计为常成员函数是合适的,能够避免数据成 ... [详细]
  • 题目:poj2342Anniversaryparty题意:话说一个公司的一些然要去参加一个party,每个人有一个愉悦值,而如果某个人的直接上司在场的话会非常扫兴,所以避免这样 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
author-avatar
爆米花来爆料V
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有