IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 栈模拟递归的一般性方法 二叉树非递归的后序遍历 -> 正文阅读

[数据结构与算法]栈模拟递归的一般性方法 二叉树非递归的后序遍历

先说结论,仅追求用栈模拟递归是有套路的(文章就是介绍我自己琢磨的一个套路),但是写出高质量的非递归代码是有难度的,还需要考虑问题本身的特征。

用栈模拟递归的可行性

首先要明白函数调用的汇编层面的过程。

详情参考浅析函数的调用过程

所以一次函数调用本质上就是操作系统自动的帮我们建立、维护并回收一个内存栈,并在内存栈中保存了一些函数执行的状态信息。

递归是一种特殊的函数调用,即函数自己调用自己。函数调用是有开销的,如果递归调用的层次太深,还会有爆栈的风险。

因此,通常我们希望可以把递归程序转换为非递归程序。如前所述,用栈模拟递归是可行的。

举例说明栈模拟递归的一般套路

二叉树的后序遍历是我们数据结构课都学过的例子,它的非递归写法难度是很大的。我们以此为例,供读者体会我们的套路。
递归形式的代码长这样子:

typedef struct Tnode* BinTree;
typedef int elementType;
struct Tnode
{
	elementType x;
	BinTree left, right;
};
void postOrderTravel(BinTree p)
{
	if (p)
	{
		postOrderTravel(p->left);
		postOrderTravel(p->right);
		cout << p->x << endl;
	}
}

用我们的套路写出来的非递归代码是这样的:

struct Node{
    BinTree t;
    int flag;
};
void myPostOrderTravel(BinTree p)
{
    stack<Node> stk;
    if (!p)
        return;
    stk.push({p, 0});
	while (!stk.empty())
	{
		Node& tmp = stk.top();
        
        switch (tmp.flag)
        {
        case 0:
        	tmp.flag = 1;
            if (tmp.t->left)
            {
                stk.push({tmp.t->left, 0});
                break;
            }
        case 1:
        	tmp.flag = 2;
            if (tmp.t->right)
            {
                stk.push({tmp.t->right, 0});
                break;
            }
        default:
            cout << tmp.t->x << endl;
            stk.pop();
        }
	}
}

栈中存放哪些内容

栈中的一个节点代表一个函数体。

栈的数据结构是我们自定义的结构体,结构体 = 递归调用函数的参数 + flag

为什么要有函数的参数很好理解,我们是模拟递归,递归有的我们一定要有。

flag就有些意思了。

先看这张图(作者是 BNUbeginner):
模拟栈实现手写递归

在这里插入图片描述

函数执行过程分为以下五步:

  1. 执行代码块0
  2. 执行递归调用,进入新的函数体
  3. 从调用的函数体返回,接着执行代码块1

我们可以看到,递归调用类似于一个中断的过程,我们现有的函数体被中断了,转去执行新的函数体,然后从新的函数体返回,继续执行现有的函数体。

flag的作用就是标志着当前的函数体是从代码块0开始执行还是从代码块1开始执行
对于二叉树的后序遍历:
flag == 0: 对左儿子访问
flag == 1: 对右儿子访问
flag == 2: 输出自己的节点值

在这里插入图片描述

所有我们用flag配合switch语句(我甚至怀疑最早的switch就是针对模拟递归调用提出的,它们的功能实在是太吻合了),switch捕捉flag对应的语句块,如果执行完一个语句块要相应的增加flag序号(作者将flag的修改放在每个语句块的最前面,功能是一样的)。

        switch (tmp.flag)
        {
        case 0:
        	tmp.flag = 1;
            if (tmp.t->left)
            {
                stk.push({tmp.t->left, 0});
                break;
            }
        case 1:
        	tmp.flag = 2;
            if (tmp.t->right)
            {
                stk.push({tmp.t->right, 0});
                break;
            }
        default:
            cout << tmp.t->x << endl;
            stk.pop();
        }

因此,针对任何一个递归函数,只要我们能够将它划分成几个语句块,就可以套用模板来实现非递归方法。

效率如何

我们用栈模拟递归,减少了函数调用的开销,但是我们的代码执行速度变快了吗?答案是否定的!

现代CPU都是流水线作业的,伴有大量的指令集并行。而流水线最怕的就是分支语句,分支语句往往会造成流水线的停顿或者使得一些代码的执行浪费掉了。

而对于非分支语句,无论是编译器的代码优化还是硬件的动态调度,都可以通过乱序执行很大程度上提高代码执行速度。

我们的模板伴有一个巨大的while循环,还有大量的switch-break分支跳转语句,执行速度并不比递归方式快。但是在节省内存空间方面,我们用栈模拟递归显然是十分优秀的。

回溯问题的非递归写法

洛谷P1049 装箱问题,一个类似01背包的问题。可用回溯轻松完成。
在这里插入图片描述

对于回溯问题,我们一般不需要使用flag成员。因为回溯就是深度优先搜索,按照一颗书的形式完成搜索问题。

我们的一个函数体对应树上的一个节点。对于一个节点来说,只要它能够将它的孩子节点都扩展上,那么它的任务就完成了。

换句话说,我们不需要从子节点返回时对父节点做任何操作。

首先给出递归写法,判断边界+如果当前物品取+如果当前物品不取:

void dfs(int u, int left)
{
    if (u == n)
    {
        ans = min(ans, left);
        return;
    }
    if (left >= weight[u])
        dfs(u + 1, left - weight[u]);

    dfs(u + 1, left);
}

模板写法,没什么可说的,直接套用模板即可:

struct Node{
    int u, left;
    int flag;
};
const int maxn = 7e5;
Node stk[maxn];
int top = -1;
void solve()
{
    stk[++top] = {0, V, 0};

    while (top != -1)
    {
        Node& tmp = stk[top];
        
        switch (tmp.flag)
        {
        case 0:
            tmp.flag = 1;
            if (tmp.u == n)
            {
                ans = min(tmp.left, ans);
                top--;
                break;
            }
        case 1:
            tmp.flag = 2;
            if (tmp.left >= weight[tmp.u])
            {
                stk[++top] = {tmp.u + 1, tmp.left - weight[tmp.u], 0};
                break;
            }
            
        case 2:
            top--;
            stk[++top] = {tmp.u + 1, tmp.left, 0};
            break;
        
        default:
            break;
        }
    }
}

更优秀的非递归写法:
在这里我们不使用flag成员。

const int maxn = 7e5;
struct Node_t{
    int u, left;
};
int top = -1;
Node_t stk[maxn];
void solve()
{
    stk[++top] = {0, V};
    while (top != -1)
    {
        Node_t tmp = stk[top];
        top--;
        if (tmp.u == n)
        {
            ans = min(tmp.left, ans);
            continue;
        }           
        if (tmp.left >= weight[tmp.u])
            stk[++top] = {tmp.u + 1, tmp.left - weight[tmp.u]};

        stk[++top] = {tmp.u + 1, tmp.left};
    }
}

时间效率:

递归模板写法更优秀的非递归
36ms54ms50ms

二叉树后序遍历的非递归算法

void PostOrderTravel(BinTree t)
{
	stack<BinTree> stk;
	BinTree pLastVisit = 0;
	while (t)
	{
		stk.push(t);
		t = t->left;
	}
	while (!stk.empty())
	{
		t = stk.top();
		stk.pop();
		if (!t->right || t->right == pLastVisit)
		{													
			cout << t->x << endl;
			pLastVisit = t;
		}
		else
		{
			stk.push(t);
			t = t->right;
			while (t)
			{
				stk.push(t);
				t = t->left;
			}
		}
	}
}
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:42:57  更:2022-04-09 18:45:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 9:27:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码