二叉树的层序遍历经典问题

首先  这需要vector容器和queue容器的知识   以上知识可以通过文档对照的去看  

然后  最花费时间的还是在找教程上面   最好优先对照leetcode上面的解析高赞看一遍  而不是在csdn搜索   这样效率比较高   leetcode上面讲的还是很不错的

而且要多画图   便于理解

102. 二叉树的层序遍历 - 力扣(LeetCode)

首先我们需要看懂广度优先算法的代码是怎么操作  通过画图我们可以知道流程

然后我们需要学习二维数组(也就是vector(vector<int>)的知识   以上是基础知识的铺垫  

做好了上述铺垫 再去看层序遍历的问题   其实关键(或者说对于我的理解难点)

就是为什么先记录n个然后就能让这n个遍历完在进入下一步

通过leetcode的图解   首先记录之后  内部的for循环执行了n次   这就保证了pop()n个出去

怎么判断<>中用什么?

<> 中的内容被称为模板参数,用于指定容器中存储的数据类型。在这个例子中,vector<vector<int>> 表示一个二维的动态数组,其中每个元素都是一个 vector<int> 类型的动态数组。queue<TreeNode*> 表示一个队列,其中每个元素都是 TreeNode* 类型的指针。这涉及到了C++中的模板和容器的基础知识。

reverse(result.begin(), result.end()); 其中result.begin(), result.end()涉及了什么基础知识?为什么这么写?请讲一讲基础语法

reverse(result.begin(), result.end())是一个用于反转容器中元素顺序的函数,其中result.begin()result.end()分别表示容器result的起始位置和结束位置。这里涉及了C++中迭代器的基础知识,迭代器是一种类似于指针的对象,用于遍历容器中的元素。begin()end()是容器类的成员函数,用于返回容器的起始位置和结束位置的迭代器。

reverse(result.begin(), result.end())中,result.begin()result.end()返回的是容器result的起始位置和结束位置的迭代器,这两个迭代器作为参数传递给reverse()函数,用于指定需要反转的元素范围。reverse()函数会将指定范围内的元素顺序反转,例如将[1, 2, 3, 4]反转为[4, 3, 2, 1]

该代码中 queue是用来缓存的  vector是用来做什么的?

1. queue 是用来缓存待遍历的节点的。它按照先进先出的顺序存储每一层的节点。
2. vector 是一个二维 vector,是用来存储最终的层序遍历结果的。外层的 vector 存储的是树的每一层,内层的 vector 存储的是对应层的节点值。
具体的:
- queue 缓存待遍历的节点 - 初始放入根节点,然后每次弹出一个节点,并将其子节点放入队列待遍历。
- vector 存储遍历结果 - 在遍历一层节点时,先向 vector 加入一个空的内层 vector,然后将当前节点的值存入这个内层 vector。
- 这样经过整个树的遍历,vector 中就存储了树的层序遍历结果,每层一个内层 vector。
所以简单总结:
queue 的作用是缓存待遍历的节点;
vector 的作用是存储层序遍历的结果。
两者互相配合,就可以实现二叉树的层序遍历了。
理解不同的数据结构在算法中的作用非常重要,这可以帮助我们设计出正确高效的解法

这个二叉树是怎么遍历的?

这个二叉树层序遍历代码的遍历逻辑是:
1. 初始化一个队列,先将根节点入队。
2. 进入循环,当队列不空时继续遍历:
  2.1 记录当前队列大小(当前层节点数)
  2.2 向结果向量添加一个空向量(存储当前层)
  2.3 循环当前层的节点数:
    2.3.1 出队一个节点
    2.3.2 将节点值添加到结果向量
    2.3.3 将该节点的非空子节点入队(为遍历下一层做准备)
3. 当队列为空时跳出,遍历结束。
其关键点是:
- 用队列的大小表示当前层节点数。
- 每次先保存当前层大小,然后遍历这么多节点。
- 遍历时将下一层节点入队。
- 用结果向量存储每层节点。
通过控制队列的大小,即可一层一层遍历树的所有节点,并通过向量存储结果。
这就是一个典型的利用队列进行层序遍历的实现过程。既省去了递归的栈空间,又可以直接保存每层结果。
让我知道如果你还有任何疑问! 

 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
     vector<vector<int>> ret;//<> 里面就是你定义的数据类型;
     if(root==nullptr){
         return ret;//返回值一定是ret  返回root是什么操作
     }
     queue <TreeNode*> q;//TreeNode*
     q.push(root);     while(q.size()!=0){
     int currentsize=q.size();
     for(int i =currentsize;i<1;i--){
         auto node=q.front();q.pop();
         if(root->left!=nullptr){
             q.push(node->left);
         }
          if(root->right!=nullptr){
             q.push(node->right);
         }
     }
     }
  return ret;  }
};该代码有什么问题?

6 条回复


Claude应用  6 分钟前

有几个问题:1. ret 是要返回的结果矩阵,需要在循环中添加元素到它。现在的代码没有向 ret 中添加任何元素,所以会返回一个空的向量。2. currentsize 的初始化有问题。应该初始化为 q.size(),表示当前层的节点数。现在初始化为 1 会导致循环直接退出。3. 循环内的逻辑有误。应该是取出队列前端节点,访问该节点,如果有左/右子节点就加入队列。现在是直接对 root 节点进行操作,而不是队列中的当前节点。4. 返回值应该是 ret,而不是 root。一个修正后的实现:

cpp
vector<vector<int>> levelOrder(TreeNode* root) {
  vector<vector<int>> ret;
  if (root == nullptr) {
    return ret; 
  }

  queue<TreeNode*> q;
  q.push(root);

  while (q.size() != 0) {
    int currSize = q.size();
    vector<int> level; 
    for (int i = 0; i < currSize; ++i) {
      auto node = q.front();
      q.pop();
      level.push_back(node->val);
      if (node->left != nullptr) {
        q.push(node->left);  
      }
      if (node->right != nullptr) {
        q.push(node->right);
      }
    }
    ret.push_back(level);
  }
  return ret;
}

主要修改是:1. 更新当前层节点数 currSize
2. 从队列取节点,访问节点值,加入 level 结果
3. 将左右子节点加入队列
4. 将当前层结果放入 ret
5. 返回 ret请检查是否理解这些修改。 (已编辑) 

xiong gavin  2 分钟前

@Claude 为什么currentsize 的初始化有问题。应该初始化为 q.size(),表示当前层的节点数。现在初始化为 1 会导致循环直接退出。我的初始化和你给出的初始化有什么区别?请举例子

Claude应用  2 分钟前

好的,我来通过一个示例解释下:假设树结构如下:      1
     / \
    2   3
   / \
  4   5对此进行层序遍历,步骤应该是:1. 根节点入队,队列为 [1]2. 当前队列大小为 1,取出1并访问,1的左右子节点 2 和 3 入队,队列变为 [2, 3]3. 当前队列大小为 2,取出2并访问,2的左右子节点 4 和 5 入队,队列变为 [3, 4, 5]4. 当前队列大小为 3,取出 3, 4, 5 并访问,队列空,遍历结束。你的代码中,currentsize 初始化为1, 那么在第二步的时候就会退出循环,因为 1 < 1 不成立。我的代码中,通过 q.size() 来动态获取当前队列大小,作为当前层节点数,所以可以正确遍历每一层的节点。初始化为一个常数1,意味着每次只会遍历第一个节点,不会遍历该节点的子节点,因此无法遍历整个树。而使用 q.size() 则可以動态獲取當前層級的節點數量,從而實現層序遍歷。这样理解了吗?请你自己再举一个例子,模拟一下整个遍历过程,看看currentsize为常数和变化的区别。有任何不清楚的地方都可以提问。 (已编辑) 

xiong gavin  < 1 分钟前

@Claude 如果我将for(int i =currentsize;i<0;i--)可以吗

Claude应用  < 1 分钟前

不可以,for循环的条件i<0是错误的。for循环的三个组成部分是:1. 初始化:int i = currentsize
2. 条件判断:i < 0
3. 更新:i--对于这个层序遍历算法,我们需要做的是:1. 遍历当前队列中的所有节点(当前队列大小就是currentsize)
2. 对每个节点进行操作(取出节点、访问、加入子节点到队列等)所以for循环的条件应该是遍历当前队列中所有节点,也就是遍历currentsize次:

for (int i = 0; i < currentsize; ++i) {
  // 遍历当前队列中的节点
}

如果条件是i<0,那么for循环根本不会执行,因为初始值i=currentsize,必定不小于0。所以这样写for循环的条件是错误的,无法遍历所有的节点。for循环的条件应该能保证遍历当前队列中的所有节点,也就是从0遍历到currentsize-1,所以正确的条件是i<currentsize,你觉得呢? 

@Claude 这两个形式有什么区别 我不理解  请详细说说

白色的对勾

眼睛

举起双手

回复

Claude应用  4 分钟前

你的问题很好,这两种形式确实有区别,我来详细解释一下:1. 原来的形式

cpp
vector<int> level; 

for (节点) {
  level.push_back(节点值); 
}

ret.push_back(level);

这里先定义了一个 vector<int> 类型的 level 变量,用于存储每一层的节点值。然后在循环中向 level 中 push_back 节点值,将每一层的节点值存入 level。最后再把 level push 到返回结果 ret 中。2. 修改后的形式:

cpp
ret.push_back(vector<int>()); // 先加入空 vector

for (节点) {
  ret.back().push_back(节点值); // 直接向 ret 最后一个vector添加
} 

这里不再定义 level 变量,直接对 ret 进行操作。首先向 ret 加入一个空的 vector<int>。然后在循环中直接通过 ret.back() 拿到最后一个 vector,向其中 push_back 节点值。这个形式减少了 level 变量,直接利用了 ret。两者的关键区别在于:1. 原来的是先定义变量,后 push_back 到结果
2. 修改的是直接在结果上操作原来的形式简单直接,但使用了额外变量。修改后的形式减少变量,但需要注意先 push_back 空 vector,然后再 push_back 节点值,否则会出错。需要理解这两种形式思路和区别,适当运用两种形式都可以解决问题。请你再举一个例子,想一想这两种形式在其他场景下的不同之处。如果有不清楚的地方,欢迎提出来讨论。 

至于为什么又有队列又有vector(vector<int>)???

这是因为 `ret` 变量的类型是一个容器,例如 `std::vector`,它具有 `push_back` 方法。`back()` 方法返回容器中的最后一个元素的引用,并且可以在此引用上继续调用 `push_back` 方法以将新元素添加到容器的末尾。所以,`ret.back().push_back(node->val)` 的含义是将 `node->val` 添加到 `ret` 容器中的最后一个元素所代表的容器中。

首先  队列是广度优先搜索所需要的   然后vector这个是构建二叉树所需要的

107. 二叉树的层序遍历 II - 力扣(LeetCode)

这个主要就是通过reverse反转函数

大部分二叉树题目都可以在该题解的汇总中找到

637. 二叉树的层平均值 - 力扣(LeetCode)

  • class Solution {
    public:
    vector<double> averageOfLevels(TreeNode
     root(根)) {
    vector <double> ret;
    queue <TreeNode*> que;
    if(root(根)==nullptr){
    return ret;
    }

    while(!que.empty()){
    double ans =0;
    int currentsize = que.size();
    for(int i=0;i<currentsize;i++){
    auto node = que.front();
    que.pop();
    ans =ans+ node->val;
    if(root(根)->left!=nullptr){
    averageOfLevels(root(根)->left);
    }
    if(root(根)->right!=nullptr){
    averageOfLevels(root(根)->right);
    }

    }
    ret.push_back(ans / currentsize);
    }
    return ret;
    }
    };该代码有什么问题?

删除

重试

复制

这段代码存在几个问题:

  1. 初始化队列 que 后没有将根节点 root 入队,导致循环条件 !que.empty() 永远为假,因此无法进入循环。

  2. 在循环中,应该将当前节点的左右子节点入队,而不是始终将根节点的左右子节点入队。

  3. 在计算每一层的平均值时,应该使用当前层的节点数 currentsize 来除以,而不是使用固定的 currentsize

以下是更新后的代码:

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        vector<double> ret;
        queue<TreeNode*> que;
        if (root == nullptr) {
            return ret;
        }
        que.push(root);
        while (!que.empty()) {
            double sum = 0;
            int currentSize = que.size();
            for (int i = 0; i < currentSize; i++) {
                TreeNode* node = que.front();
                que.pop();
                sum += node->val;
                if (node->left != nullptr) {
                    que.push(node->left);
                }
                if (node->right != nullptr) {
                    que.push(node->right);
                }
            }
            ret.push_back(sum / currentSize);
        }
        return ret;
    }
};

更新后的代码修复了上述问题,现在可以计算二叉树每一层的平均值并返回一个包含这些平均值的向量。

2.3 二叉树的锯齿形层序遍历

这道题我们首先需要的前置知识是容器中的「双端队列」的数据结构。

2.4 N 叉树的层序遍历

429. N 叉树的层序遍历 - 力扣(LeetCode)

  • for (Node* child: cur->children) { q.push(child); }请解释一下这段代码中的Node* child: cur->children是什么意思?

  • 这段代码是一个遍历cur节点的子节点的循环。Node* child表示child是一个指向Node类型的指针,cur->children表示cur节点的子节点集合。所以这段代码的意思是,将cur节点的每个子节点依次放入队列q中。

  • for (初始化语句; 条件表达式; 更新语句) { // 循环体 } 为什么上述的for循环基本语法是这样 而for (Node* child: cur->children) { q.push(child); }?

  • 上述的第一种for循环语法是传统的C/C++风格的循环语法,适用于需要指定循环初始值、循环条件以及每次循环更新操作的场景。它的语法结构较为灵活,可以覆盖更多的循环需求。

    而第二种for循环语法是C++11引入的范围-based for循环语法,也被称为foreach循环。它主要用于遍历容器或者迭代器范围内的元素。这种语法更加简洁,清晰地表达了遍历的意图,不需要显式地指定循环变量的初始值、条件和更新操作。

    在for循环中,第一种语法适用于需要灵活控制循环条件和更新操作的情况,而第二种语法适用于简单的遍历操作,使代码更加简洁易读。选择使用哪种语法取决于具体的需求和编码风格。

  • 第二种for循环语法是C++11引入的范围-based for循环语法,也被称为foreach循环。它主要用于遍历容器或者迭代器范围内的元素。这种语法更加简洁,清晰地表达了遍历的意图,不需要显式地指定循环变量的初始值、条件和更新操作。 请详细介绍一下这种基本语

  • C++11引入了范围-based for循环语法,也被称为foreach循环。它主要用于遍历容器或者迭代器范围内的元素。

    基本语法如下:

  • for (declaration : range) { // 循环体 }

    其中,declaration 是一个变量声明,用于声明一个新的变量来表示当前遍历的元素;range 是一个范围,可以是数组、容器、迭代器等表示一系列元素的对象。

    在循环的每次迭代中,声明的变量将依次被赋值为范围内的元素,然后执行循环体。循环会依次遍历范围内的所有元素,直到遍历完毕为止。

    下面是一个使用基本语法的示例:

    std::vector<int> numbers = {1, 2, 3, 4, 5}; for (int num : numbers) { std::cout << num << " "; } // 输出:1 2 3 4 5
    

    在上述示例中,我们使用范围-based for循环遍历了 numbers 容器中的所有元素,并将每个元素输出到标准输出流。

    值得注意的是,范围-based for循环对于只读遍历非常方便,但如果需要修改容器内的元素,则需要使用引用声明。例如:

    std::vector<int> numbers = {1, 2, 3, 4, 5}; for (int& num : numbers) { num *= 2; // 将每个元素乘以2 } for (int num : numbers) { std::cout << num << " "; } // 输出:2 4 6 8 10
    

    在上述示例中,我们使用引用声明 int& 来修改容器内的元素,将每个元素乘以2,并再次遍历输出修改后的结果。

    范围-based for循环语法简洁、清晰,可以减少编写循环的代码量,并提高代码的可读性。

3.1 在每个树行中找最大值(leetcode515)

515. 在每个树行中找最大值 - 力扣(LeetCode)

maxVal 是一个用于记录当前层的最大值的变量。初始值设置为INT_MIN,即整型的最小值。在每次处理完一层节点后,将当前层的最大值添加到结果数组 res 中。

max() 是一个函数模板,用于返回两个参数中的较大值。具体语法规则如下:

template <class T> const T& max(const T& a, const T& b);

这个函数模板有两个参数,类型为 Tab。它们可以是任意类型,只要能进行比较操作即可。函数返回两个参数中较大的那个值。

在代码中,maxVal = max(maxVal, t->val); 表示将当前节点的值 t->valmaxVal 中的较大值赋给 maxVal。这样就能保证 maxVal 始终记录当前层的最大值。

另外,INT_MIN 是 C++ 中 <climits> 头文件中定义的整型最小值常量,它的值为 -2147483648。在这段代码中,初始将 maxVal 设置为 INT_MIN 是为了确保在第一次比较时,无论节点的值是多少,都能正确更新 maxVal 的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值