三行代码递归实现二叉树层序遍历

本文介绍了一种仅用三行代码实现二叉树层序遍历的方法,通过递归方式访问每个节点,并利用数组按特定位置保存节点值,最终输出完整的层序遍历结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简述

二叉树的层序遍历网上大部分都是使用队列的出队和入队来实现的,这次我用三行代码递归实现二叉树的层序遍历.

层序

下图是一个简单的二叉树,层序就是一行一行的往下读取,这个二叉树的层序结果便是:
1234567

这里写图片描述
(图画的比较丑,强迫症看着难受,看官忍一下)

递归分析

要想使用递归,必须有两个条件:

  1. 函数参数类型相同
  2. 递归必须有出口

在二叉树中找到上面的两个条件,与前中后三种遍历一样,函数参数为节点的,递归出口是当左右孩子为空的时候.传入根节点,然后依次递归访问左右孩子,直至为空.

重点

那么问题来了,递归遍历的数据保存到那?如何做到保存后是层序的顺序?

继续观察这张图
这里写图片描述

第一层
根节点标记为1元素是层序输出的第一个值
第二层
标记为2的元素是层序输出的第二个值,同时他是标记为1节点的左孩子
标记为3的元素是层序输出的第三个元素,同时他是标记为1的节点的右孩子.
第三层
标记为4的元素是输出的第四个元素,他是标记为2节点的左孩子
标记为5的元素是输出的第五个元素,他是标记为2节点的右孩子

很容易找到一个规律:

  • 每一个节点左孩子在层序中输出的位置是该节点在层序输出位置的二倍
  • 每一个节点右孩子在层序中输出的位置是该节点在层序输出位置的二倍加一
  • 根节点在层序输出的位置为1

    也就是:
    假设当前节点输出的位置是i,那么他的左孩子层序输出的位置是2*i,他的右孩子在层序输出的位置为2*i+1

代码实现

根据上面得出的结论,就可以写出层序遍历的递归代码了,知道了节点层序输出的位置,那么遍历时候直接保存到指定位置,等所有节点遍历结束后再统一输出,这个与前中后遍历是不一样的.
既然是根据位置去保存,那么当然是使用数组了,位置就是数组的下标,根据下标进行存放,操作非常方便.

这一段代码,是把字符二叉树层序遍历

int tree2str(bitree *b,char *a,int i)
{
    if(b->left!=NULL)
    {
        tree2str(b->left,a,2*i);
    }
    if(b->right!=NULL)
    {
        tree2str(b->right,a,2*i+1);
    }
    *(a+i)=b->data;
}

注意
参数a为数组的首地址,调用时候传递参数i初始值为1,代表第一层,既就是根界点,当递归函数执行完毕时候,所有的元素都在对应的位置,a[0]元素始终为空,因为是从1开始存放的,所以调用之后输出的时候应该从a+1的位置开始,字符串结束应该是’\0’,输出前补上防错.
这里写图片描述
(本人测试环境:系统Ubuntu16.04LTS 编译器GCC5.4,字符串末尾没有添加’\0’未出错,在vc6.0会出现烫烫烫烫烫烫烫烫烫……)

结束

回到最初,标题说是三行代码,实际上为了看的方便在if函数后面加了花括号,我觉得一行代码的标志应该是一个分号,既一个语句;所以上面的也算是三行代码了.

如果上面的不算三行代码,那就看下面

int tree2str(bitree *b,char *a,int i)
{
    if(b->left!=NULL)       tree2str(b->left,a,2*i);
    if(b->right!=NULL)      tree2str(b->right,a,2*i+1);
    *(a+i)=b->data;
}

好了现在很直观的三行了[完].

<think>我们使用两个栈来实现二叉树层序遍历。核心思路是: 1. 使用两个栈:stack1用于当前层,stack2用于下一层。 2. 初始时,将根节点压入stack1。 3. 循环直到stack1为空(此时stack2也应该为空): a. 从stack1弹出一个节点,访问它。 b. 将该节点的子节点按照先左子节点后右子节点的顺序压入stack2(这样在下一层弹出时就是从左到右的顺序,因为栈是后进先出,所以先压左后压右,则弹出时就是右先出左后出,但是我们希望下一层是从左到右访问,所以这里需要调整:实际上,在下一层处理时,我们希望先访问左子节点,所以应该先压右子节点,再压左子节点。这样在stack2中,左子节点在右子节点上面,当我们将stack2交换为stack1时,弹出顺序就是左子节点先弹出,右子节点后弹出,符合从左到右的顺序)。 4. 当stack1为空时,说明当前层已经处理完毕,交换stack1和stack2(此时stack2中存储的是下一层的所有节点,交换后stack1变为下一层的栈,stack2变为空栈,用于存储下下一层)。 5. 重复步骤3和4,直到两个栈都为空。 注意:在步骤3b中,我们压入子节点的顺序是:先压右子节点,再压左子节点。这样在下一层弹出时,先弹出左子节点,再弹出右子节点,从而保证从左到右的顺序。 下面我们用C++代码实现这个算法,并输出每层的节点(每层单独一行)。 同时,为了更清晰地展示层次,我们可以在交换栈的时候换行(即每处理完一层,在开始下一层之前输出换行)。 但是,如果要求每层节点输出为独立数组(例如LeetCode 102题),我们可以稍作修改,在每一层开始前创建一个临时数组,将该层所有节点值存入,然后将该数组加入结果集。 不过,由于栈的结构,我们无法像队列那样自然地知道每层的节点数(因为栈在弹出时我们同时压入下一层节点,所以每层的节点数在开始处理该层时就是固定的,但我们需要在开始处理该层之前知道该层有多少节点?实际上,我们可以在交换栈之后,开始处理当前层之前,记录当前栈的大小,然后循环该大小次数,这样就能处理完一层。 但是,我们这里使用两个栈,每层处理完后会交换栈,所以在处理新的一层时,当前栈(stack1)中存放的就是当前层的所有节点。因此,我们可以在交换栈之后,记录当前栈的大小(即该层节点数),然后循环该次数,这样就能将一层处理完。 因此,我们修改算法如下: 1. 初始化两个栈stack1, stack2,将根节点压入stack1。 2. 定义一个结果向量result,用于存储每层的节点值向量。 3. 当stack1不为空时: a. 创建一个临时向量level,用于存储当前层的节点值。 b. 获取当前stack1的大小(即当前层的节点数)为levelSize。 c. 循环levelSize次: 弹出stack1的栈顶节点,将其值加入level。 将该节点的子节点按照先右后左的顺序压入stack2(这样下一层弹出时就是从左到右)。 d. 将level加入result。 e. 交换stack1和stack2(此时stack1变为下一层的栈,stack2为空)。 4. 注意:最后一次循环时,我们处理了最后一层,然后交换栈,此时stack1变成了下一层(即最后一层的下一层,为空),所以循环条件为stack1不为空,这样最后一层处理完后,下一层为空,循环结束。 但是,注意:我们在处理完最后一层后,交换栈,此时stack1变为stack2(原本是空的),所以循环条件在最后一层处理完后会结束。 然而,这个算法有一个问题:我们在处理最后一层时,将最后一层的节点值存入level,然后交换栈,此时stack1变为stack2(即最后一层的子节点层,可能是空的),然后下一轮循环判断stack1为空就结束了。所以正确。 但是,我们需要注意的是,在最后一层处理中,我们可能会将下一层(空)压入stack2,然后交换后stack1为空,循环结束。 因此,我们按照这个思路编写代码。 注意:如果根节点为空,直接返回空结果。 下面是用C++实现代码,返回一个二维向量,每层一个向量。 同时,我们也可以输出到控制台,每层一行。 代码实现如下:</think>### 使用栈实现二叉树层序遍历(C++版) 虽然层序遍历通常使用队列实现,但通过**双栈交替机制**也能高效完成。以下是核心思路和完整实现: #### 🌳 双栈法核心思路 1. **栈分工**: - `stack1`:存储当前层节点 - `stack2`:存储下一层节点 2. **关键操作**: - 弹出`stack1`节点时,按**先右子后左子**顺序压入`stack2` - 当`stack1`空时交换两栈(确保层级切换) 3. **顺序控制**: - 利用栈的LIFO特性,通过反向压入子节点(右→左)实现正向输出(左→右) #### ⚙️ C++代码实现 ```cpp #include <iostream> #include <vector> #include <stack> using namespace std; struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; vector<vector<int>> levelOrderWithStack(TreeNode* root) { vector<vector<int>> result; if (!root) return result; stack<TreeNode*> stack1, stack2; stack1.push(root); while (!stack1.empty()) { vector<int> levelNodes; int levelSize = stack1.size(); for (int i = 0; i < levelSize; ++i) { TreeNode* node = stack1.top(); stack1.pop(); levelNodes.push_back(node->val); // 关键:先压右子节点,再压左子节点 if (node->right) stack2.push(node->right); if (node->left) stack2.push(node->left); } result.push_back(levelNodes); swap(stack1, stack2); // 层级切换 while (!stack2.empty()) stack2.pop(); // 清空辅助栈 } return result; } // 测试用例 int main() { /* 构建二叉树: 1 / \ 2 3 / \ 4 5 */ TreeNode* root = new TreeNode(1); root->left = new TreeNode(2); root->right = new TreeNode(3); root->left->left = new TreeNode(4); root->left->right = new TreeNode(5); vector<vector<int>> res = levelOrderWithStack(root); // 输出:[[1], [2,3], [4,5]] for (auto& level : res) { for (int val : level) cout << val << " "; cout << endl; } return 0; } ``` #### 🧩 执行过程解析 以二叉树`[1,2,3,4,5]`为例: 1. **初始状态**: - `stack1 = [1]`, `stack2 = []` - 输出层:`[1]` 2. **第二层**: - 压入子节点:`stack2 = [3,2]`(先右后左) - 交换栈:`stack1 = [3,2]` - 输出层:`[2,3]` 3. **第三层**: - 弹出`2`后压入子节点:`stack2 = [5,4]` - 弹出`3`后无子节点 - 交换栈:`stack1 = [5,4]` - 输出层:`[4,5]` #### 📊 复杂度分析 | 指标 | 值 | 说明 | |------------|-------|--------------------------| | 时间复杂度 | $O(n)$ | 每个节点访问一次 | | 空间复杂度 | $O(n)$ | 最坏情况存储所有叶子节点 | #### ⚠️ 注意事项 1. **压栈顺序**:必须**先右子后左子**才能保证层内从左到右的顺序 2. **层级切换**:使用`swap()`高效交换栈指针 3. **空树处理**:开头需检查`root`是否为空 4. **内存管理**:实际应用中需添加节点释放逻辑[^1] --- ### 🔍 相关问题 1. **为什么层序遍历通常用队列而不用栈?队列实现和栈实现的本质区别是什么?** > 队列的FIFO特性天然符合层序要求,而栈需通过反向压栈和层级切换模拟层序[^3] 2. **如何用栈实现二叉树的前序、中序和后序遍历?每种遍历的栈操作有何不同?** > 前序:直接压右子再左子;中序:需配合指针移动;后序:需记录访问状态或双栈[^2] 3. **双栈法在内存受限环境下有何优势?** > 相比递归减少栈溢出风险,比队列实现更节省连续内存空间[^5] 4. **如何处理每层节点数为奇数的特殊情况?** > 双栈法天然适应任意层宽,无需特殊处理[^4] [^1]: 引用自二叉树接口设计和内存管理 [^2]: 引用自非递归遍历的核心思想 [^3]: 引用自队列与栈的本质区别 [^4]: 引用自层次遍历算法复杂度 [^5]: 引用自内存受限环境优化
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值