栈论 : 递归与栈式访问,如何用栈实现所有递归操作(幼儿园题目篇,题目2)

时间:2022-07-25
本文章向大家介绍栈论 : 递归与栈式访问,如何用栈实现所有递归操作(幼儿园题目篇,题目2),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上一篇 :

栈论 : 递归与栈式访问,如何用栈实现所有递归操作(幼儿园题目篇)

题目2

题目2和题目1最大的不同点是访问顺序变了。

我们对应的伪代码应该如下:

  1,2,3表示的是先递归读取左子树,再是右子树,最后读取自己

  void postOrderRead(BiTree tree){

    if(tree == NULL){

      return;

    }

    preOrderRead(tree -> lchild); //1

    preOrderRead(tree -> rchild);//2

    visit(tree);//3

  }

看完上一道题的解析,应该熟能生巧了吧~

首先,我们列出每个栈帧应该具有的信息 :

1.当前节点

其次,我们理一下逻辑思路

下面的左子函数 = 左节点的子函数

首先,因为父函数中对节点的读取是在子函数退出之后的(3在1和2之后),所以父函数的栈帧在子函数栈帧入栈时不能出栈(不能退出),要等待子函数出栈,

操作完3之后才能出栈。如果我们现在我们从栈中访问了一个节点(注意是访问,不是弹出,因为父栈帧不能随意弹出),因为是后序遍历,所以要访问左子树先

也就是执行1处,所以总是要把包含左子节点的栈帧入栈。只有等到左子树是空才停止。

但是现在有一个问题,当我们访问到一个节点,我们怎么知道他的子函数栈帧该不该创建呢(子函数调用),因为此时可能是子函数调用过并退出,当前栈帧才露出来给我们获取到。另一种是子函数还没有调用,现在的栈帧是刚创建的,需要马上调用子函数。总结来说就是我们在当前节点不知是该调用子函数还是自己退出。造成这种情况的原因是,因为函数是顺序执行的,即使在同一个栈帧中,这段栈帧对应的程序是可以知道当前程序执行到的行号的。也就是说知道是否该调用子函数。而我们从栈帧得到栈帧,如果不带有类似行号的信息,根本不知道是否该调用子函数。

所以,栈帧里的信息,我们需要修改一下。

每个栈帧应该具有的信息 :

1.当前节点

2.当前节点是否已经调用过左/右子函数

第二点和行号有差不多的功能,但我们只需要是否调用过子函数的信息,行号太细了,可以但是没必要。

那么用什么来存储这个信息呢?

我的想法是用一个int型的变量,一个int型的变量一般是32位,也就是说他可以存储32个“是与否的信息”。

我用最低位为1表示还需要将左子函数栈帧入栈(还没调用过),为0表示已经把左子函数栈帧入栈了。

依次类推,第二位来对应右子函数。

你可能会问我这样选是否合理,我个人觉得还是相对合理的。

原因如下:

1.首先存储信息的载体体积较小,只用一个变量,充分利用每一位的信息。

2.其次是虽然每次获取信息都需要进行与掩码的操作(例如 A | (00000000 00000000 00000000 00000001) = 是否左子函数栈帧入栈),但是这样的操作耗时还是相对较少的。相比之下,如果我们用了很多个变量,频繁读取这些变量的时候,高速缓存的cache line 可能就会被提前填满,导致我们缓存的优势发挥效能降低,CPU运行速度下降。而且 | 操作在硬件层面讲是时间复杂度为O(1)的操作,因为每一位的信号都可以并行通过与门。而移位则需要等待下一位的触发器接受到上一位的触发器信息,上一位的触发器才能接受上上一位的触发器信息,存在等待问题,所以硬件层面的时间复杂度是O(n)。选与操作还是比较好的。当然,这只是从我有限的硬件知识推理分析的,并没有考虑操作系统之类的因素,如果有说错的地方请赐教。当然你也可以不运算,直接将这个int的不同值对应不同的情况,比如0表示调用左子函数,1不是不要,2表示调用右子函数,3表示不要......但是这样没有了0和1这样相反的思维逻辑条理性,而且情况一多处理麻烦。

实现代码如下 :

栈帧定义 :

typedef struct FunctionFrame {
    BiTNode * node;
    int tag; // 标志是应该往右走还是往左走
};

具体实现

   Stack stack;
    FunctionFrame frame;
    int initial;
    
    initial = 0b0011; //初始值 表示两边都还要调用
    init(stack);
    frame = {&node, initial};
    push(stack, &frame);

    while (!stackEmpty(stack)) { //栈不空表示还有函数在调用中
        FunctionFrame* frame = getTop(stack); //不是弹出的访问

        if (frame -> tag & 0b0001) { //如果最低位是1
            if (frame ->node ->lchild != NULL) { //左孩子节点不为空
                FunctionFrame* lc = (FunctionFrame*) malloc(sizeof(FunctionFrame)); //创建左子节点的函数栈帧
                lc->node = frame->node->lchild;
                lc->tag = initial; 
                push(stack, lc); // 左子函数栈帧入栈
                frame->tag =  frame->tag & 0b0010; //将最低位(调用左子函数的标志)抹除掉
                continue; //左子函数栈帧入栈 右子函数就不要入了,因为要等待左子函数调用完右边才能调用
            }
            
        }

     //如上,以此类推
        if (frame ->tag & 0b0010) {
            if (frame->node->rchild != NULL) {
                FunctionFrame* rc = (FunctionFrame*)malloc(sizeof(FunctionFrame));
                rc->node = frame->node->rchild;
                rc->tag = initial;
                push(stack, rc);
                frame->tag = frame->tag & 0b0001;
                continue;
            }
            
        }

        visit(frame -> node);
        pop(stack);
    }

下一篇 :

栈论 : 递归与栈式访问,如何用栈实现所有递归操作(幼儿园题目篇,题目3)

护眼绿:

没人看的结语:

首先很感谢你看到这里,辛苦了。

文章中某些地方可能不正确或不准确,代码也可能不够高效可读,希望读者能够帮忙指正,共同学习进步。