热门标签 | 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

推荐阅读
  • 第3章 感受(一)——3.1. Hello world 经典版
    [回到目录]白话C++第3章.感受Helloworld!,HelloC++,我们来了!3.1.Helloworld经典版毫无疑义,一 ... [详细]
  • 本题要求在一组数中反复取出两个数相加,并将结果放回数组中,最终求出最小的总加法代价。这是一个经典的哈夫曼编码问题,利用贪心算法可以有效地解决。 ... [详细]
  • 深入解析 Android IPC 中的 Messenger 机制
    本文详细介绍了 Android 中基于消息传递的进程间通信(IPC)机制——Messenger。通过实例和源码分析,帮助开发者更好地理解和使用这一高效的通信工具。 ... [详细]
  • 题目:poj2342Anniversaryparty题意:话说一个公司的一些然要去参加一个party,每个人有一个愉悦值,而如果某个人的直接上司在场的话会非常扫兴,所以避免这样 ... [详细]
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
  • 微软Exchange服务器遭遇2022年版“千年虫”漏洞
    微软Exchange服务器在新年伊始遭遇了一个类似于‘千年虫’的日期处理漏洞,导致邮件传输受阻。该问题主要影响配置了FIP-FS恶意软件引擎的Exchange 2016和2019版本。 ... [详细]
  • 探讨如何真正掌握Java EE,包括所需技能、工具和实践经验。资深软件教学总监李刚分享了对毕业生简历中常见问题的看法,并提供了详尽的标准。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 本文详细介绍了C语言中的指针,包括其基本概念、应用场景以及使用时的优缺点。同时,通过实例解析了指针在内存管理、数组操作、函数调用等方面的具体应用,并探讨了指针的安全性问题。 ... [详细]
  • 本文深入探讨了POJ2762问题,旨在通过强连通分量缩点和单向连通性的判断方法,解决有向图中任意两点之间的可达性问题。文章详细介绍了算法原理、实现步骤,并附带完整的代码示例。 ... [详细]
  • 在现代Web应用中,当用户滚动到页面底部时,自动加载更多内容的功能变得越来越普遍。这种无刷新加载技术不仅提升了用户体验,还优化了页面性能。本文将探讨如何实现这一功能,并介绍一些实际应用案例。 ... [详细]
  • 本文详细介绍了C语言的起源、发展及其标准化过程,涵盖了从早期的BCPL和B语言到现代C语言的演变,并探讨了其在操作系统和跨平台编程中的重要地位。 ... [详细]
  • 本问题探讨了在特定条件下排列儿童队伍的方法数量。题目要求计算满足条件的队伍排列总数,并使用递推算法和大数处理技术来解决这一问题。 ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • ProblemDescriptionAninchwormisatthebottomofawellninchesdeep.Ithasenoughene ... [详细]
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社区 版权所有