文章目录
递归遍历
递归的底层实现机制:用栈来实现
基本步骤
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历
- 要先处理空节点,也就是左右孩子为空的情况,遇到空节点的时候递归调用立刻返回
//确定递归函数参数与返回值,二叉树的参数基本就是根节点+数组(放遍历结果),返回值基本都是void
//回溯算法的参数就比较多,可以不用一次确定完
//结果直接放参数里
void preTrav(TreeNode* cur, vector<int>& vec){
//确定终止条件,没明确好会栈溢出
//遇到空节点返回
if(cur==NULL){
return;
}
//确定单层递归的逻辑
//前序遍历是中左右,中是要处理的节点,处理逻辑是数组放当前遍历过的元素
vec.push_back(cur->value);//这是中
preTrav(cur->left,vec); //遍历左孩子,传入的就是根节点
preTrav(cur->right,vec);//遍历右孩子
}
- 递归函数参数一般都是根节点+存放遍历结果的vector数组,返回值void
- 终止条件:递归是深度优先搜索的一个过程,顺着一个方向搜索,遇到空节点的时候返回。
- 单层逻辑:先根节点,再左孩子,再右孩子(前序遍历中左右)
中序遍历
//左中右
void preTrav(TreeNode* cur, vector<int>& vec){
//终止条件:遇到空节点返回
if(cur==NULL){
return;
}
//确定单层递归的逻辑
//中序遍历是左中右
preTrav(cur->left,vec); //遍历左孩子,传入的就是根节点
vec.push_back(cur->value);//这是中
preTrav(cur->right,vec);//遍历右孩子
}
后序遍历
//左右中
void preTrav(TreeNode* cur, vector<int>& vec){
//终止条件:遇到空节点返回
if(cur==NULL){
return;
}
//确定单层递归的逻辑
//后序遍历是左右中
preTrav(cur->left,vec); //遍历左孩子,传入的就是根节点
preTrav(cur->right,vec);//遍历右孩子
vec.push_back(cur->value);//这是中
}
定义了树节点的完整版
- 定义根节点结构体的时候,构造函数的意义就是在主函数构造二叉树时方便传入节点数值
#include <vector>
struct TreeNode {
int value;
TreeNode* left;
TreeNode* right;
TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};
void preTrav(TreeNode* cur,vector<int>& vec){
//终止条件
if(cur==NULL){
return;
}
//单层逻辑:中左右
vec.push_back(cur->value);//指针表示
preTrav(cur->left,vec); //注意另一个参数不能忘记传入
preTrav(cur->right,vec);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int>result;
preTrav(root,result);
return result;
}
//注意主函数的写法
int main() {
// 创建树节点对象并构建二叉树
//构造函数的意义就是在主函数构造二叉树时方便传入节点数值
TreeNode* root = new TreeNode(1); // 创建根节点,值为1
root->left = new TreeNode(2); // 创建左子节点,值为2
root->right = new TreeNode(3); // 创建右子节点,值为3
root->left->left = new TreeNode(4); // 创建左子节点的左子节点,值为4
root->left->right = new TreeNode(5); // 创建左子节点的右子节点,值为5
// 前序遍历二叉树
std::vector<int> result;
preTrav(root, result);
// 输出遍历结果
std::cout << "Preorder Traversal: ";
for (int val : result) {
std::cout << val << " ";
}
std::cout << std::endl;
// 释放内存,避免内存泄漏
delete root->left->left;
delete root->left->right;
delete root->left;
delete root->right;
delete root;
return 0;
}
前中后序的非递归遍历又称为迭代法,一般来说,面试不会要求对很复杂的递归使用迭代法,通常是比较简单的递归逻辑,会要求用迭代法实现一下。也就是用栈实现递归。这里还没太明白,回头看懂了再来补充一下。
层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。使用广度优先搜索(BFS)的方式来遍历二叉树,并按层次将节点的值保存在数组中。通过队列和指向节点的指针,可以实现按层次遍历二叉树的功能。
队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出,适合模拟深度优先遍历也就是递归的逻辑。
思路
建造队列,队列的size代表层中有几个元素,记录每一层的元素数量,弹出的时候只弹出这一层
如下图的例子,4弹出队列后,4的左右孩子加入队列,此时队列里的元素是7 1 3
由于之前记录了第二层的大小是2,size的作用就是确保每一层不会弹出多余的元素。比如此时不会弹出1和3
弹出元素之后,再把相应的左右孩子加入进去,这样第二层遍历结束之后,第三层所有的节点就都在这个队列内了。继续做while(size--)
的操作,只弹出4个元素,也就是把第三层全部弹出。
伪代码
-
确定最开始的剪枝情况
-
确定循环终止条件:没有元素进入队列了,队列是空的
-
确定输出形式:最后的结果保存形式应该是二维数组,比如上图中的二叉树,其层序遍历输出形式应该是
{{6},{4,7},{1,3,5,8}
注意二维数组的写法是套了两层大括号#include <vector> std::vector<std::vector<int>> array = {{6}, {4, 7}, {1, 3, 5, 8}};
- 输出形式是二维数组,但是仍然需要建立一维数组来存放每一层的元素
queue<TreeNode*>que;
if(root!=NULL){
que.push();
}
//终止条件:没有元素进入队列了,队列为空
while(!que.empty()){
//记录每一层的元素数量相应弹出
size = que.size();
//定义数组放一层内的元素
vector<int>vec;
while(size--){
//注意queue里面存放的是节点!节点是指针!因此接收的node应该也是指针
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
//如果有左右孩子,加入左右孩子
if(node->left){
que.push(node->left);
}
if(node->right){
que.push(node->right);
}
}
}
二叉树节点的指针访问
在C++中,二叉树的节点可以使用指针表示,但并不一定都是指针。节点可以使用指针来连接左右子节点,这是一种常见的表示方式。每个节点包含一个数据元素以及指向左子节点和右子节点的指针。
struct TreeNode {
int data;
TreeNode* left;
TreeNode* right;
};
在这个示例中,TreeNode
结构体包含一个整数类型的data
成员,以及两个指向左子节点和右子节点的指针,分别是left
和right
。
使用指针表示二叉树节点的好处是可以方便地在树中进行遍历和操作。通过指针,可以轻松地访问子节点或者向上回溯到父节点。
然而,如果你使用其他的数据结构或设计,也可以选择其他的节点表示方式。例如,也可以使用数组来表示二叉树,其中每个元素表示一个节点,通过索引关系来连接节点。这种方式在完全二叉树等特殊情况下可能更为高效。
总之,二叉树的节点可以使用指针表示,但并非一定都是指针。
类似于Node->val
是用于访问二叉树节点中的元素值的一种常见方式。在这种情况下,Node
是一个指向二叉树节点的指针,val
是节点中存储的值。
循环中的 while(size--)
为什么不能写成 while(que.size()--)
?
while循环里面的que.size()是不断进行变化的,如果有左右孩子,左右孩子会加入进来随时改变que.size(),因此必须用变量size单独存放队列没有加入子节点时的大小,也就是每一层的元素个数。
完整版
- 注意root->val是int类型,建立根节点队列的时候push应该直接push根节点而不是root->val
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*>que;
if(root!=NULL){
//这里不能push root->val,因为root->val是int类型!
que.push(root);
}
vector<vector<int>>result;
vector<int>vec;
while(!que.empty()){
int size = que.size();
while(size--){
//清除本层节点
TreeNode* node = que.front();
que.pop();
//本层节点存放进一维数组
vec.push_back(node->val);
//检查下层节点并放入
if(node->left){
que.push(node->left);
}
if(node->right){
que.push(node->right);
}
}
//本层的一维数组结果,可以直接加入二维数组之中
result.push_back(vec);
//清空本层的结果,准备放下一层元素
vec.clear();
}
//这里加个reverse就能解决107自底向上层序遍历
//reverse(result.begin(),result.end());
return result;
}
};
result.push_back(vec)
的用法
result
是一个二维向量,用于存储二叉树的层次遍历结果。vec
是一个一维向量,用于暂存每一层的节点值。
在每次内部循环中,我们将当前层的节点值存储在vec
中,然后将vec
添加到result
中,即**result.push_back(vec)
将一维向量vec
作为一行添加到二维向量result
中。这样,每个一维向量vec
都会代表二叉树的一层节点值**。
使用result.push_back(vec)
的好处是,它将一维向量按顺序添加到二维向量中,形成正确的层次遍历结果。通过将每层的节点值存储在一维向量中,然后将一维向量添加到二维向量中,可以方便地表示二叉树的层次遍历。
二维vector接收一维vector的例子
二维vector
可以直接接收一维vector
作为其中的一个元素向量。例如:
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {4, 5, 6};
std::vector<std::vector<int>> matrix;
matrix.push_back(vec1);
matrix.push_back(vec2);
// 输出二维vector的元素,注意这里的输出写法
for (const auto& row : matrix) {
for (const auto& element : row) {
std::cout << element << " ";
}
std::cout << std::endl;
}
return 0;
}
这段代码是用来遍历二维 vector
matrix
中的每个元素,并将其打印到标准输出。
在外层循环中,const auto& row
表示对 matrix
中的每个元素(一维 vector
)的引用。在每次迭代中,row
表示当前的一维 vector
,即表示 matrix
的一行数据。
在内层循环中,const auto& element
表示对 row
中的每个元素的引用。在每次迭代中,element
表示当前一维 vector
中的一个元素。
通过嵌套的循环,对每个 row
中的元素进行遍历,将每个 element
打印到标准输出,然后在一行结束后输出换行符 std::endl
。
这样,整个循环过程会遍历 matrix
的每一行和每个元素,并将每个元素打印到控制台,以空格分隔,并在每一行结束后输出换行符。
补充:堆栈对象的一些基础
如果while循环的写法写成如下情况,也可以直接不写清空一维数组的操作。
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
栈上的对象会在其所在的作用域结束时自动销毁,包括释放相关的内存。
在这种写法中,每次迭代开始时都会创建一个新的 vec
,并在迭代结束时将其添加到 result
中。当迭代结束后,vec
的作用域也随之结束,它将被销毁并释放相关的内存。因此,我们不需要手动释放 vec
,这个过程是自动进行的。
需要手动释放内存的情况通常发生在使用 new
运算符在堆上分配内存时,例如使用 new
创建的对象,就是在堆上创建的对象,称为堆上的动态对象(Dynamic Objects)。这些对象的生命周期不由作用域控制,而是需要显式地通过 delete
运算符来手动释放内存。需要使用 delete
运算符显式释放内存。
int* ptr = new int; // 堆上的动态对象
// ...
delete ptr; // 手动释放内存
但在这种情况下,使用的是栈上的自动对象(Automatic Objects),内存管理由编译器自动处理,不需要手动释放内存。(在函数执行时,局部变量通常在栈上分配内存。这些变量都被称为栈上的自动对象)